Skip to content

airflow dags test fails with AttributeError: 'dict' object has no attribute 'hex' for DAGs with deadline #73428

Description

@wislertt

Apache Airflow version

3.3.2 (latest). I reproduced the bug with the DAG below on 3.3.1 and 3.3.2. The same code is also present on main.

What happened?

Running airflow dags test on a DAG that sets deadline=DeadlineAlert(...) completes the run successfully (tasks succeed, DagRun is marked success), then crashes with a traceback during the final DagRun.update_state() call:

Traceback (most recent call last):
  ...
  File "airflow/cli/commands/dag_command.py", line 834, in dag_test
    dr: DagRun = dag.test(...)
  File "airflow/sdk/definitions/dag.py", line 1397, in test
    schedulable_tis, _ = dr.update_state(session=session)
  File "airflow/models/dagrun.py", line 1270, in update_state
    DeadlineAlertModel.get_by_id(alert_id, session=session) for alert_id in dag.deadline
  File "airflow/models/deadline_alert.py", line 115, in get_by_id
    result = session.scalar(select(cls).where(cls.id == deadline_alert_id))
  ...
  File "sqlalchemy/sql/sqltypes.py", line 3738, in process
    value = value.hex
AttributeError: 'dict' object has no attribute 'hex'

The exit code is non-zero, so the command reports failure for a run that actually succeeded.

Root cause

DAG.test() builds its "scheduler DAG" with an in-memory serialization round trip:

scheduler_dag = DagSerialization.deserialize_dag(DagSerialization.serialize_dag(self))

(airflow/sdk/definitions/dag.py, DAG.test())

Serialization encodes deadline alerts as plain dicts:

serialized_dag["deadline"] = [encode_deadline_alert(d) for d in dag.deadline]

(airflow/serialization/serialized_objects.py, around line 1742)

but deserialization stores the field back without decoding it:

dag.deadline = encoded_dag.get("deadline")

(airflow/serialization/serialized_objects.py, around line 1889)

So scheduler_dag.deadline is a list of encoded alert dicts.

When the run then succeeds, DagRun.update_state() prunes deadlines that are no longer needed:

deadline_alerts = [
    DeadlineAlertModel.get_by_id(alert_id, session=session) for alert_id in dag.deadline
]

(airflow/models/dagrun.py, around line 1270; same code around line 1371 on main)

get_by_id expects str | UUID, but each element here is an encoded alert dict, so SQLAlchemy's Uuid bind processor fails on value.hex.

The regular DB write path does not hit this because SerializedDagModel._generate_deadline_uuids (airflow/models/serialized_dag.py, around line 403) rewrites the serialized deadline field from alert dicts into a list of deadline_alert UUID strings before persisting. The in-memory round trip in DAG.test() bypasses that conversion.

How to reproduce

Save as deadline_dagtest_repro.py and run airflow dags test deadline_dagtest_repro. Tested as-is on 3.3.1 and 3.3.2.

from datetime import timedelta

from airflow.providers.standard.operators.empty import EmptyOperator
from airflow.sdk import DAG
from airflow.sdk.definitions.callback import SyncCallback
from airflow.sdk.definitions.deadline import DeadlineAlert, DeadlineReference


def _cb(**kwargs):
    pass


with DAG(
    dag_id="deadline_dagtest_repro",
    schedule=None,
    deadline=DeadlineAlert(
        reference=DeadlineReference.DAGRUN_LOGICAL_DATE,
        interval=timedelta(hours=2),
        callback=SyncCallback(_cb),
    ),
):
    EmptyOperator(task_id="task")

The task succeeds and the DagRun is marked success, then the traceback above is raised and the CLI exits with a non-zero code.

What you think should happen instead?

airflow dags test should complete cleanly (exit code 0) for DAGs with deadlines. Some possible fixes:

  1. Decode the deadline entries in deserialize_dag so dag.deadline holds proper objects, matching how other serialized fields are decoded, or
  2. Make the deadline pruning block in DagRun.update_state() tolerant of both shapes (encoded dicts and UUID strings), or
  3. Apply the same dict-to-UUID conversion that write_dag performs inside the DAG.test() round trip (though no DB rows exist in the test path, so 1 or 2 is probably cleaner).

Anything else?

Operating System

macOS (Darwin), Python 3.12 and 3.14. Not OS specific.

Versions of Apache Airflow Providers

apache-airflow-providers-standard (installed from the constraints file of each release).

Deployment

Virtualenv install, local CLI testing. Not deployment specific.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions