Repository: apache/airflow

Issue:
ExternalTaskSensor does not fail when failed_states is set along with a execution_date_fn
**Apache Airflow version**: 2.x including main

**What happened**:

I am using an `execution_date_fn` in an `ExternalTaskSensor` that also sets `allowed_states=['success']` and `failed_states=['failed']`. When one of the N upstream tasks fails, the sensor will hang forever in the `poke` method because there is a bug in checking for failed_states.

**What you expected to happen**:

I would expect the `ExternalTaskSensor` to fail.

I think this is due to a bug in the `poke` method where it should check if `count_failed > 0` as opposed to checking `count_failed == len(dttm_filter)`. I've created a fix locally that works for my case and have submitted a PR #16205 for it as reference.

**How to reproduce it**:

Create any `ExternalTaskSensor` that checks for `failed_states` and have one of the external DAGs tasks fail while others succeed.

E.g.
```
ExternalTaskSensor(
        task_id='check_external_dag',                                                     
        external_dag_id='external_dag',                                                               
        external_task_id=None,                              
        execution_date_fn=dependent_date_fn,
        allowed_states=['success'],
        failed_states=['failed'],                                                                     
        check_existence=True)
```



Discussion:

Resolving patch:
diff --git a/airflow/sensors/external_task.py b/airflow/sensors/external_task.py
--- a/airflow/sensors/external_task.py
+++ b/airflow/sensors/external_task.py
@@ -224,7 +224,8 @@ def poke(self, context, session=None):
         if self.failed_states:
             count_failed = self.get_count(dttm_filter, session, self.failed_states)
 
-        if count_failed == len(dttm_filter):
+        # Fail if anything in the list has failed.
+        if count_failed > 0:
             if self.external_task_ids:
                 if self.soft_fail:
                     raise AirflowSkipException(


Test patch:
diff --git a/tests/sensors/test_external_task_sensor.py b/tests/sensors/test_external_task_sensor.py
--- a/tests/sensors/test_external_task_sensor.py
+++ b/tests/sensors/test_external_task_sensor.py
@@ -418,6 +418,30 @@ def test_external_task_sensor_fn_multiple_execution_dates(self):
         with pytest.raises(AirflowSensorTimeout):
             task_with_failure.run(start_date=DEFAULT_DATE, end_date=DEFAULT_DATE, ignore_ti_state=True)
 
+        # Test to ensure that if one task in a chain of tasks fails, the
+        # ExternalTaskSensor will also report a failure and return without
+        # waiting for a timeout.
+        task_chain_with_failure = ExternalTaskSensor(
+            task_id="task_chain_with_failure",
+            external_dag_id=dag_external_id,
+            external_task_id="task_external_with_failure",
+            execution_date_fn=lambda dt: [dt + timedelta(seconds=i) for i in range(3)],
+            allowed_states=["success"],
+            failed_states=["failed"],
+            retries=0,
+            timeout=5,
+            poke_interval=1,
+            dag=dag,
+        )
+
+        # We need to test for an AirflowException explicitly since
+        # AirflowSensorTimeout is a subclass that will be raised if this does
+        # not execute properly.
+        try:
+            task_chain_with_failure.run(start_date=DEFAULT_DATE, end_date=DEFAULT_DATE, ignore_ti_state=True)
+        except AirflowException as ex:
+            assert type(ex) == AirflowException
+
     def test_external_task_sensor_delta(self):
         self.add_time_sensor()
         op = ExternalTaskSensor(