This is an automated email from the ASF dual-hosted git repository.

ferruzzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 07a0c4da099 Fix TypeError clearing a DAG run when a queued-at deadline 
interval is stored as JSON (#70370)
07a0c4da099 is described below

commit 07a0c4da099d28b6b387fb1c7ec416b00ba763a4
Author: Hemkumar Chheda <[email protected]>
AuthorDate: Sat Oct 10 05:06:04 2026 +0530

    Fix TypeError clearing a DAG run when a queued-at deadline interval is 
stored as JSON (#70370)
    
    * Fix TypeError clearing a DAG run when a queued-at deadline interval is 
stored as JSON
    
    Since 3.3.0 the deadline_alert.interval column is JSON (migration 0117, 
#64751),
    holding a serialized timedelta or VariableInterval. 
_recalculate_dagrun_queued_at_deadlines
    still passed that raw dict into timedelta(seconds=...), so clearing a DAG 
run or task
    instance whose DAG has a DagRunQueuedAtDeadline failed with
    "TypeError: unsupported type for timedelta seconds component: dict" (HTTP 
500).
    
    Decode the interval the same way DagRun creation does 
(decode_deadline_alert), then
    resolve a VariableInterval to a timedelta before recalculating. The 
existing test stored
    the interval as a plain float and never exercised the JSON path; it now 
stores the
    serialized envelope and is parametrized over fixed-timedelta and 
VariableInterval.
    
    closes: #70368
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: ferruzzi <[email protected]>
    Co-authored-by: Amogh Desai <[email protected]>
---
 airflow-core/src/airflow/models/taskinstance.py    |  26 +++-
 airflow-core/src/airflow/serialization/decoders.py |  44 +++++-
 .../src/airflow/serialization/definitions/dag.py   |  26 +---
 airflow-core/tests/unit/models/test_dagrun.py      |  18 +++
 .../tests/unit/models/test_taskinstance.py         | 156 +++++++++++++++++++--
 .../tests/unit/serialization/test_decoders.py      |  84 +++++++++++
 6 files changed, 313 insertions(+), 41 deletions(-)

diff --git a/airflow-core/src/airflow/models/taskinstance.py 
b/airflow-core/src/airflow/models/taskinstance.py
index 6c2745f0c70..19934254e97 100644
--- a/airflow-core/src/airflow/models/taskinstance.py
+++ b/airflow-core/src/airflow/models/taskinstance.py
@@ -263,10 +263,30 @@ def _recalculate_dagrun_queued_at_deadlines(
     if not results:
         return
 
+    # Local import to avoid a circular import between models and serialization.
+    from airflow.serialization.decoders import decode_deadline_alert_model, 
resolve_deadline_alert_interval
+
     for deadline, deadline_alert in results:
-        # We can't use evaluate_with() since the new queued_at is not written 
to the DB yet.
-        deadline_interval = timedelta(seconds=deadline_alert.interval)
-        new_deadline_time = new_queued_at + deadline_interval
+        # We can't use evaluate_with() since the new queued_at is not written 
to the DB yet, and
+        # interval is stored as JSON, so it has to be decoded rather than 
passed to timedelta().
+        try:
+            interval = resolve_deadline_alert_interval(
+                decode_deadline_alert_model(deadline_alert), session=session
+            )
+        except (ValueError, TypeError):
+            # Either step can fail: the alert may hold a payload the decoder 
refuses, or a
+            # variable-backed interval may point at a Variable that is missing 
or non-numeric.
+            # Leave this deadline alone rather than failing the whole clear.
+            log.warning(
+                "Error while recalculating deadline %s for DagRun %s.%s, 
leaving it unchanged",
+                deadline.id,
+                dagrun.dag_id,
+                dagrun.run_id,
+                exc_info=True,
+            )
+            continue
+
+        new_deadline_time = new_queued_at + interval
 
         log.debug(
             "Recalculating deadline %s for DagRun %s.%s: old=%s, new=%s",
diff --git a/airflow-core/src/airflow/serialization/decoders.py 
b/airflow-core/src/airflow/serialization/decoders.py
index fb1651bfb01..8c42ec0c987 100644
--- a/airflow-core/src/airflow/serialization/decoders.py
+++ b/airflow-core/src/airflow/serialization/decoders.py
@@ -53,6 +53,9 @@ from airflow.serialization.helpers import (
 )
 
 if TYPE_CHECKING:
+    from sqlalchemy.orm import Session
+
+    from airflow.models.deadline_alert import DeadlineAlert as 
DeadlineAlertModel
     from airflow.partition_mappers.base import PartitionMapper
     from airflow.partition_mappers.wait_policy import WaitPolicy
     from airflow.partition_mappers.window import Window
@@ -308,7 +311,7 @@ def _decode_deadline_callback(raw_callback: Any) -> 
AsyncCallback | SyncCallback
     return callback_cls(callback_callable=_SerializedCallbackPath(path), 
**fields)
 
 
-def decode_deadline_alert(encoded_data: dict):
+def decode_deadline_alert(encoded_data: dict) -> SerializedDeadlineAlert:
     """
     Decode a previously serialized deadline alert.
 
@@ -337,6 +340,45 @@ def decode_deadline_alert(encoded_data: dict):
     )
 
 
+def decode_deadline_alert_model(deadline_alert: DeadlineAlertModel) -> 
SerializedDeadlineAlert:
+    """
+    Decode a ``DeadlineAlert`` ORM row into its serialized representation.
+
+    :meta private:
+    """
+    return decode_deadline_alert(
+        {
+            DeadlineAlertFields.NAME: deadline_alert.name,
+            DeadlineAlertFields.REFERENCE: deadline_alert.reference,
+            DeadlineAlertFields.INTERVAL: deadline_alert.interval,
+            DeadlineAlertFields.CALLBACK: deadline_alert.callback_def,
+        }
+    )
+
+
+def resolve_deadline_alert_interval(
+    alert: SerializedDeadlineAlert, *, session: Session
+) -> datetime.timedelta:
+    """
+    Resolve a decoded alert's interval to a ``timedelta``.
+
+    A ``SerializedVariableInterval`` reads its Airflow Variable here, so this 
is only called at
+    the point a deadline is actually calculated. It raises ``ValueError`` if 
the Variable is
+    missing or is not an integer number of seconds.
+
+    :param alert: The decoded alert whose interval should be resolved.
+    :param session: Existing SQLAlchemy Session. Required rather than 
defaulted: both callers run
+        under the scheduler's ``prohibit_commit`` guard, so the open session 
has to reach
+        ``Variable.get`` instead of ``provide_session`` handing back the same 
scoped session and
+        rolling it back on exit.
+
+    :meta private:
+    """
+    if isinstance(alert.interval, SerializedVariableInterval):
+        return alert.interval.resolve(session=session)
+    return alert.interval
+
+
 def decode_timetable(var: dict[str, Any]) -> CoreTimetable:
     """
     Decode a previously serialized timetable.
diff --git a/airflow-core/src/airflow/serialization/definitions/dag.py 
b/airflow-core/src/airflow/serialization/definitions/dag.py
index 0acb5a2faa3..6f02d62c51e 100644
--- a/airflow-core/src/airflow/serialization/definitions/dag.py
+++ b/airflow-core/src/airflow/serialization/definitions/dag.py
@@ -50,14 +50,9 @@ from airflow.models.deadline import Deadline
 from airflow.models.deadline_alert import DeadlineAlert as DeadlineAlertModel
 from airflow.models.taskinstancekey import TaskInstanceKey
 from airflow.models.tasklog import LogTemplate
-from airflow.serialization.decoders import decode_deadline_alert
-from airflow.serialization.definitions.deadline import (
-    DeadlineAlertFields,
-    SerializedReferenceModels,
-    SerializedVariableInterval,
-)
+from airflow.serialization.decoders import decode_deadline_alert_model, 
resolve_deadline_alert_interval
+from airflow.serialization.definitions.deadline import 
SerializedReferenceModels
 from airflow.serialization.definitions.param import SerializedParamsDict
-from airflow.serialization.enums import DagAttributeTypes as DAT, Encoding
 from airflow.timetables.base import DagRunInfo, DataInterval, TimeRestriction
 from airflow.utils.helpers import prune_dict
 from airflow.utils.session import NEW_SESSION, provide_session
@@ -751,21 +746,8 @@ class SerializedDAG:
             if not deadline_alert:
                 continue
 
-            deserialized_deadline_alert = decode_deadline_alert(
-                {
-                    Encoding.TYPE: DAT.DEADLINE_ALERT,
-                    Encoding.VAR: {
-                        DeadlineAlertFields.REFERENCE: 
deadline_alert.reference,
-                        DeadlineAlertFields.INTERVAL: deadline_alert.interval,
-                        DeadlineAlertFields.CALLBACK: 
deadline_alert.callback_def,
-                    },
-                }
-            )
-
-            interval = deserialized_deadline_alert.interval
-
-            if isinstance(interval, SerializedVariableInterval):
-                interval = interval.resolve(session=session)
+            deserialized_deadline_alert = 
decode_deadline_alert_model(deadline_alert)
+            interval = 
resolve_deadline_alert_interval(deserialized_deadline_alert, session=session)
 
             if isinstance(deserialized_deadline_alert.reference, 
SerializedReferenceModels.TYPES.DAGRUN):
                 deadline_time = 
deserialized_deadline_alert.reference.evaluate_with(
diff --git a/airflow-core/tests/unit/models/test_dagrun.py 
b/airflow-core/tests/unit/models/test_dagrun.py
index ea36d44b599..c80a9dfc3ac 100644
--- a/airflow-core/tests/unit/models/test_dagrun.py
+++ b/airflow-core/tests/unit/models/test_dagrun.py
@@ -1596,6 +1596,24 @@ class TestDagRun:
         deadline = session.execute(select(Deadline)).scalars().one_or_none()
         assert deadline.deadline_time == first_deadline_time
 
+    @mock.patch.object(Deadline, "prune_deadlines")
+    def test_dagrun_deadline_variable_interval_uses_callers_session(self, _, 
session, deadline_test_dag):
+        """Caller's session must reach Variable.get."""
+        future_date = datetime.datetime.now() + datetime.timedelta(days=365)
+
+        scheduler_dag = deadline_test_dag(
+            deadline=DeadlineAlert(
+                reference=DeadlineReference.FIXED_DATETIME(future_date),
+                interval=VariableInterval("my_key"),
+                callback=AsyncCallback(empty_callback_for_deadline),
+            ),
+        )
+
+        with mock.patch.object(Variable, "get", return_value="60") as mock_get:
+            self.create_dag_run(dag=scheduler_dag, session=session)
+
+        mock_get.assert_called_once_with("my_key", session=session)
+
     def test_dagrun_deadline_logs_when_reference_column_is_null(self, session, 
deadline_test_dag, caplog):
         scheduler_dag = deadline_test_dag(
             deadline=DeadlineAlert(
diff --git a/airflow-core/tests/unit/models/test_taskinstance.py 
b/airflow-core/tests/unit/models/test_taskinstance.py
index 0820c2f7881..29f98da417a 100644
--- a/airflow-core/tests/unit/models/test_taskinstance.py
+++ b/airflow-core/tests/unit/models/test_taskinstance.py
@@ -73,6 +73,7 @@ from airflow.models.taskinstance import (
     find_relevant_relatives,
 )
 from airflow.models.taskreschedule import TaskReschedule
+from airflow.models.variable import Variable
 from airflow.models.xcom import XComModel, XComModelV2, build_xcom_read_query
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.providers.standard.operators.empty import EmptyOperator
@@ -92,10 +93,11 @@ from airflow.sdk import (
 )
 from airflow.sdk.api.datamodels._generated import AssetEventResponse, 
AssetResponse
 from airflow.sdk.definitions.callback import AsyncCallback
-from airflow.sdk.definitions.deadline import DeadlineReference
+from airflow.sdk.definitions.deadline import DeadlineReference, 
VariableInterval
 from airflow.sdk.definitions.param import process_params
 from airflow.sdk.definitions.taskgroup import TaskGroup
 from airflow.sdk.execution_time.comms import AssetEventsResult
+from airflow.sdk.serde import serialize
 from airflow.serialization.definitions.assets import SerializedAsset
 from airflow.serialization.definitions.baseoperator import 
SerializedBaseOperator
 from airflow.serialization.definitions.dag import SerializedDAG
@@ -4597,8 +4599,24 @@ async def empty_callback_for_deadline():
     pass
 
 
-def test_clear_task_instances_recalculates_dagrun_queued_deadlines(dag_maker, 
session):
-    """Test that clearing tasks recalculates all (and only) DAGRUN_QUEUED_AT 
deadlines."""
[email protected](
+    "use_variable_interval",
+    [
+        pytest.param(False, id="fixed_timedelta_interval"),
+        pytest.param(True, id="variable_interval"),
+    ],
+)
+def test_clear_task_instances_recalculates_dagrun_queued_deadlines(dag_maker, 
session, use_variable_interval):
+    """Test that clearing tasks recalculates all (and only) DAGRUN_QUEUED_AT 
deadlines.
+
+    Since Airflow 3.3 the ``deadline_alert.interval`` column is JSON (a 
serialized ``timedelta``
+    or ``VariableInterval``), so the recalculation must decode it instead of 
passing the raw value
+    to ``timedelta()``. Storing the interval via ``serialize`` here mirrors 
production and covers
+    both interval kinds.
+    """
+    variable_key = "deadline_interval_key"
+    variable_seconds = 3600
+
     with dag_maker(
         dag_id="test_recalculate_deadlines",
         schedule=datetime.timedelta(days=1),
@@ -4618,29 +4636,52 @@ def 
test_clear_task_instances_recalculates_dagrun_queued_deadlines(dag_maker, se
         select(SerializedDagModel.id).where(SerializedDagModel.dag_id == 
dag.dag_id)
     )
 
-    deadline_configs = [
-        (DeadlineReference.DAGRUN_QUEUED_AT, datetime.timedelta(hours=1)),
-        (DeadlineReference.DAGRUN_QUEUED_AT, datetime.timedelta(hours=2)),
-        (DeadlineReference.FIXED_DATETIME, datetime.timedelta(hours=1)),
+    if use_variable_interval:
+        queued_interval = VariableInterval(variable_key)
+        queued_resolved = datetime.timedelta(seconds=variable_seconds)
+        # Both DAGRUN_QUEUED_AT deadlines resolve to the same interval from 
the Variable.
+        queued_configs = [
+            (DeadlineReference.DAGRUN_QUEUED_AT, queued_interval, 
queued_resolved),
+            (DeadlineReference.DAGRUN_QUEUED_AT, queued_interval, 
queued_resolved),
+        ]
+    else:
+        queued_configs = [
+            (
+                DeadlineReference.DAGRUN_QUEUED_AT,
+                datetime.timedelta(hours=1),
+                datetime.timedelta(hours=1),
+            ),
+            (
+                DeadlineReference.DAGRUN_QUEUED_AT,
+                datetime.timedelta(hours=2),
+                datetime.timedelta(hours=2),
+            ),
+        ]
+
+    # (reference type, interval stored on the alert, timedelta it resolves to)
+    deadline_configs = queued_configs + [
+        (DeadlineReference.FIXED_DATETIME, datetime.timedelta(hours=1), 
datetime.timedelta(hours=1)),
     ]
 
-    for deadline_type, interval in deadline_configs:
+    expected_resolved_by_alert = {}
+    for deadline_type, stored_interval, resolved_interval in deadline_configs:
         if deadline_type == DeadlineReference.DAGRUN_QUEUED_AT:
             reference = 
DeadlineReference.DAGRUN_QUEUED_AT.serialize_reference()
-            deadline_time = dag_run.queued_at + interval
+            deadline_time = dag_run.queued_at + resolved_interval
         else:  # FIXED_DATETIME
             future_date = timezone.utcnow() + datetime.timedelta(days=7)
             reference = 
DeadlineReference.FIXED_DATETIME(future_date).serialize_reference()
-            deadline_time = future_date + interval
+            deadline_time = future_date + resolved_interval
 
         deadline_alert = DeadlineAlertModel(
             serialized_dag_id=serialized_dag_id,
             reference=reference,
-            interval=interval.total_seconds(),
-            callback_def={"path": f"{__name__}.empty_callback_for_deadline", 
"kwargs": {}},
+            interval=serialize(stored_interval),
+            callback_def=serialize(AsyncCallback(empty_callback_for_deadline)),
         )
         session.add(deadline_alert)
         session.flush()
+        expected_resolved_by_alert[deadline_alert.id] = resolved_interval
 
         deadline = Deadline(
             dagrun_id=dag_run.id,
@@ -4659,7 +4700,21 @@ def 
test_clear_task_instances_recalculates_dagrun_queued_deadlines(dag_maker, se
     }
 
     tis = session.scalars(select(TI).where(TI.dag_id == dag.dag_id, TI.run_id 
== dag_run.run_id)).all()
-    clear_task_instances(tis, session)
+
+    # VariableInterval.resolve() reads the Airflow Variable during 
recalculation.
+    with (
+        mock.patch.object(Variable, "get", return_value=str(variable_seconds))
+        if use_variable_interval
+        else contextlib.nullcontext()
+    ) as mock_variable_get:
+        clear_task_instances(tis, session)
+
+    if use_variable_interval:
+        # See resolve_deadline_alert_interval() for why the caller's session 
has to be forwarded.
+        assert mock_variable_get.call_args_list == [
+            mock.call(variable_key, session=session),
+            mock.call(variable_key, session=session),
+        ]
 
     dag_run = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
     assert dag_run.queued_at > original_queued_at
@@ -4672,13 +4727,84 @@ def 
test_clear_task_instances_recalculates_dagrun_queued_deadlines(dag_maker, se
     for deadline in deadlines_after:
         if deadline.deadline_time != 
deadline_times_by_alert[deadline.deadline_alert_id]:
             recalculated_count += 1
-            deadline_alert = session.get(DeadlineAlertModel, 
deadline.deadline_alert_id)
-            expected_time = dag_run.queued_at + 
datetime.timedelta(seconds=deadline_alert.interval)
+            expected_time = dag_run.queued_at + 
expected_resolved_by_alert[deadline.deadline_alert_id]
             assert deadline.deadline_time == expected_time
 
     assert recalculated_count == 2
 
 
+def 
test_clear_task_instances_skips_deadline_with_unresolvable_interval(dag_maker, 
session):
+    """A variable-backed interval that cannot be resolved must not abort the 
clear.
+
+    ``SerializedVariableInterval.resolve()`` raises ``ValueError`` when the 
Airflow Variable is
+    missing or is not an integer, and that happens while the DAG run is being 
cleared. The clear
+    should still go through, leaving the unresolvable deadline at its old time.
+    """
+    with dag_maker(
+        dag_id="test_recalculate_deadlines_unresolvable",
+        schedule=datetime.timedelta(days=1),
+    ) as dag:
+        EmptyOperator(task_id="task_1")
+
+    dag_run = dag_maker.create_dagrun()
+    ti = dag_run.get_task_instance("task_1", session=session)
+    ti.set_state(TaskInstanceState.SUCCESS, session=session)
+
+    original_queued_at = timezone.utcnow() - datetime.timedelta(hours=2)
+    dag_run.queued_at = original_queued_at
+    session.flush()
+
+    serialized_dag_id = session.scalar(
+        select(SerializedDagModel.id).where(SerializedDagModel.dag_id == 
dag.dag_id)
+    )
+
+    deadline_alert = DeadlineAlertModel(
+        serialized_dag_id=serialized_dag_id,
+        reference=DeadlineReference.DAGRUN_QUEUED_AT.serialize_reference(),
+        interval=serialize(VariableInterval("missing_deadline_interval_key")),
+        callback_def=serialize(AsyncCallback(empty_callback_for_deadline)),
+    )
+    session.add(deadline_alert)
+    session.flush()
+
+    original_deadline_time = original_queued_at + datetime.timedelta(hours=1)
+    session.add(
+        Deadline(
+            dagrun_id=dag_run.id,
+            deadline_alert_id=deadline_alert.id,
+            deadline_time=original_deadline_time,
+            callback=AsyncCallback(empty_callback_for_deadline),
+            dag_id=dag_run.dag_id,
+        )
+    )
+    session.flush()
+
+    tis = session.scalars(select(TI).where(TI.dag_id == dag.dag_id, TI.run_id 
== dag_run.run_id)).all()
+
+    with (
+        mock.patch.object(
+            Variable, "get", 
side_effect=KeyError("missing_deadline_interval_key")
+        ) as mock_variable_get,
+        mock.patch("airflow.models.taskinstance.log") as mock_log,
+    ):
+        clear_task_instances(tis, session)
+
+    dag_run = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
+    assert dag_run.queued_at > original_queued_at
+
+    deadline = session.scalar(select(Deadline).where(Deadline.dagrun_id == 
dag_run.id))
+    assert deadline.deadline_time == original_deadline_time
+
+    # The alert decoded fine and the Variable lookup is what raised, so this 
is still covering
+    # an unresolvable interval rather than an earlier failure that lands in 
the same except.
+    mock_variable_get.assert_called_once_with("missing_deadline_interval_key", 
session=session)
+
+    assert mock_log.warning.call_count == 1
+    msg, *args = mock_log.warning.call_args.args
+    assert "Error while recalculating deadline" in msg
+    assert args[0] == deadline.id
+
+
 def test_get_dagrun_loaded_but_none_returns_dagrun(dag_maker, session):
     """
     Test that `get_dagrun()` fetches `DagRun` from DB when the `dag_run`
diff --git a/airflow-core/tests/unit/serialization/test_decoders.py 
b/airflow-core/tests/unit/serialization/test_decoders.py
new file mode 100644
index 00000000000..56ab174f261
--- /dev/null
+++ b/airflow-core/tests/unit/serialization/test_decoders.py
@@ -0,0 +1,84 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import datetime
+
+from airflow.models.deadline_alert import DeadlineAlert as DeadlineAlertModel
+from airflow.models.variable import Variable
+from airflow.sdk.definitions.callback import AsyncCallback
+from airflow.sdk.definitions.deadline import DeadlineReference
+from airflow.sdk.serde import serialize
+from airflow.serialization.decoders import decode_deadline_alert_model, 
resolve_deadline_alert_interval
+from airflow.serialization.definitions.deadline import (
+    SerializedDeadlineAlert,
+    SerializedReferenceModels,
+    SerializedVariableInterval,
+)
+
+
+async def empty_callback_for_deadline():
+    pass
+
+
+def _alert(interval):
+    return SerializedDeadlineAlert(
+        reference=SerializedReferenceModels.DagRunQueuedAtDeadline(),
+        interval=interval,
+        callback=None,
+    )
+
+
+class TestResolveDeadlineAlertInterval:
+    def test_forwards_session_to_variable_lookup(self, mocker):
+        """Both callers resolve intervals while holding an open transaction, 
so the session they
+        pass has to reach ``Variable.get``. See 
``resolve_deadline_alert_interval()``."""
+        mock_get = mocker.patch.object(Variable, "get", return_value="42")
+        session = mocker.MagicMock()
+
+        resolved = resolve_deadline_alert_interval(
+            _alert(SerializedVariableInterval(key="test_interval")), 
session=session
+        )
+
+        assert resolved == datetime.timedelta(seconds=42)
+        mock_get.assert_called_once_with("test_interval", session=session)
+
+    def test_fixed_interval_is_returned_without_a_variable_lookup(self, 
mocker):
+        mock_get = mocker.patch.object(Variable, "get")
+
+        interval = datetime.timedelta(hours=1)
+
+        assert resolve_deadline_alert_interval(_alert(interval), 
session=mocker.MagicMock()) == interval
+        mock_get.assert_not_called()
+
+
+class TestDecodeDeadlineAlertModel:
+    def test_row_name_is_carried_into_the_serialized_alert(self):
+        """The ORM row has a ``name`` column, so a general "decode this row" 
helper has to read it.
+
+        Neither caller looks at ``.name`` today, which is exactly why dropping 
it here would go
+        unnoticed until a third caller trusts the helper's name.
+        """
+        row = DeadlineAlertModel(
+            name="my_deadline",
+            reference=DeadlineReference.DAGRUN_QUEUED_AT.serialize_reference(),
+            interval=serialize(datetime.timedelta(hours=1)),
+            callback_def=serialize(AsyncCallback(empty_callback_for_deadline)),
+        )
+
+        assert decode_deadline_alert_model(row).name == "my_deadline"

Reply via email to