Repository: apache/airflow

Issue:
Running airflow dags backfill --reset-dagruns <dag_id> -s  <execution_start_dt> -e <execution_end_dt> results in error when run twice. 
### Apache Airflow version

2.2.3 (latest released)

### What happened

It's the same situation as https://github.com/apache/airflow/issues/21023.
Only change to `airflow dags backfill` from `airflow dags test`.

``` bash
(airflow) [www@np-data-eng-airflow-sync001-lde-jp2v-prod ~]$ airflow dags backfill tutorial --reset-dagruns -s 2022-01-20 -e 2022-01-23
You are about to delete these 9 tasks:
<TaskInstance: tutorial.print_date scheduled__2022-01-20T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.print_date scheduled__2022-01-21T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.print_date scheduled__2022-01-22T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.sleep scheduled__2022-01-20T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.sleep scheduled__2022-01-21T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.sleep scheduled__2022-01-22T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.templated scheduled__2022-01-20T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.templated scheduled__2022-01-21T07:48:54.720148+00:00 [success]>
<TaskInstance: tutorial.templated scheduled__2022-01-22T07:48:54.720148+00:00 [success]>

Are you sure? (yes/no):
y
Traceback (most recent call last):
  File "/home1/www/venv3/airflow/bin/airflow", line 8, in <module>
    sys.exit(main())
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/__main__.py", line 48, in main
    args.func(args)
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/cli/cli_parser.py", line 48, in command
    return func(*args, **kwargs)
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/utils/cli.py", line 92, in wrapper
    return f(*args, **kwargs)
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/cli/commands/dag_command.py", line 108, in dag_backfill
    dag_run_state=State.NONE,
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/models/dag.py", line 1948, in clear_dags
    dry_run=False,
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/utils/session.py", line 70, in wrapper
    return func(*args, session=session, **kwargs)
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/models/dag.py", line 1887, in clear
    dag_run_state=dag_run_state,
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/models/taskinstance.py", line 270, in clear_task_instances
    dr.state = dag_run_state
  File "<string>", line 1, in __set__
  File "/home1/www/venv3/airflow/lib/python3.7/site-packages/airflow/models/dagrun.py", line 194, in set_state
    raise ValueError(f"invalid DagRun state: {state}")
ValueError: invalid DagRun state: None
```

### What you expected to happen

_No response_

### How to reproduce

1. Setup Airflow 2.2.3
2. Run any dag (in my case, using [tutorial dag file)](https://airflow.apache.org/docs/apache-airflow/stable/tutorial.html).
3. Run again same dag by `airflow dags backfill` command.

### Operating System

CentOS Linux 7.9

### Versions of Apache Airflow Providers

Providers info
apache-airflow-providers-celery          | 2.1.0
apache-airflow-providers-cncf-kubernetes | 3.0.1
apache-airflow-providers-ftp             | 2.0.1
apache-airflow-providers-http            | 2.0.2
apache-airflow-providers-imap            | 2.1.0
apache-airflow-providers-mysql           | 2.1.1
apache-airflow-providers-redis           | 2.0.1
apache-airflow-providers-slack           | 4.1.0
apache-airflow-providers-sqlite          | 2.0.1
apache-airflow-providers-ssh             | 2.3.0

### Deployment

Virtualenv installation

### Deployment details

_No response_

### Anything else

_No response_

### Are you willing to submit PR?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
Feel free to fix ! If you are fast, we can even cherry-pick to 2.2.4 :)
We should also add a check in `clear_task_instances` to catch this mistake, otherwise future regressions are bound to happen.
> We should also add a check in `clear_task_instances` to catch this mistake, otherwise future regressions are bound to happen.

Good point!

Resolving patch:
diff --git a/airflow/models/taskinstance.py b/airflow/models/taskinstance.py
--- a/airflow/models/taskinstance.py
+++ b/airflow/models/taskinstance.py
@@ -112,7 +112,7 @@
 from airflow.utils.retries import run_with_db_retries
 from airflow.utils.session import NEW_SESSION, create_session, provide_session
 from airflow.utils.sqlalchemy import ExtendedJSON, UtcDateTime
-from airflow.utils.state import DagRunState, State
+from airflow.utils.state import DagRunState, State, TaskInstanceState
 from airflow.utils.timeout import timeout
 
 try:
@@ -201,11 +201,11 @@ def clear_task_instances(
         lambda: defaultdict(lambda: defaultdict(set))
     )
     for ti in tis:
-        if ti.state == State.RUNNING:
+        if ti.state == TaskInstanceState.RUNNING:
             if ti.job_id:
                 # If a task is cleared when running, set its state to RESTARTING so that
                 # the task is terminated and becomes eligible for retry.
-                ti.state = State.RESTARTING
+                ti.state = TaskInstanceState.RESTARTING
                 job_ids.append(ti.job_id)
         else:
             task_id = ti.task_id
@@ -220,7 +220,7 @@ def clear_task_instances(
                 # outdated. We make max_tries the maximum value of its
                 # original max_tries or the last attempted try number.
                 ti.max_tries = max(ti.max_tries, ti.prev_attempted_tries)
-            ti.state = State.NONE
+            ti.state = None
             ti.external_executor_id = None
             session.merge(ti)
 
@@ -258,7 +258,7 @@ def clear_task_instances(
         from airflow.jobs.base_job import BaseJob
 
         for job in session.query(BaseJob).filter(BaseJob.id.in_(job_ids)).all():
-            job.state = State.RESTARTING
+            job.state = TaskInstanceState.RESTARTING
 
     if activate_dag_runs is not None:
         warnings.warn(
@@ -287,10 +287,11 @@ def clear_task_instances(
             )
             .all()
         )
+        dag_run_state = DagRunState(dag_run_state)  # Validate the state value.
         for dr in drs:
             dr.state = dag_run_state
             dr.start_date = timezone.utcnow()
-            if dag_run_state == State.QUEUED:
+            if dag_run_state == DagRunState.QUEUED:
                 dr.last_scheduling_decision = None
                 dr.start_date = None
 
diff --git a/airflow/utils/state.py b/airflow/utils/state.py
--- a/airflow/utils/state.py
+++ b/airflow/utils/state.py
@@ -30,9 +30,11 @@ class TaskInstanceState(str, Enum):
     Note that None is also allowed, so always use this in a type hint with Optional.
     """
 
+    # The scheduler sets a TaskInstance state to None when it's created but not
+    # yet run, but we don't list it here since TaskInstance is a string enum.
+    # Use None instead if need this state.
+
     # Set by the scheduler
-    # None - Task is created but should not run yet
-    NONE = None
     REMOVED = "removed"  # Task vanished from DAG before it ran
     SCHEDULED = "scheduled"  # Task should run and will be handed to executor soon
 


Test patch: