Skip to content

Fix DateTimeSensorAsync crash on templated target_time with start_from_trigger (real fix, closes #70284) - #70576

Open
nanjeshramesh wants to merge 10 commits into
apache:mainfrom
nanjeshramesh:nanjeshramesh-patch-1
Open

Fix DateTimeSensorAsync crash on templated target_time with start_from_trigger (real fix, closes #70284)#70576
nanjeshramesh wants to merge 10 commits into
apache:mainfrom
nanjeshramesh:nanjeshramesh-patch-1

Conversation

@nanjeshramesh

@nanjeshramesh nanjeshramesh commented Jul 28, 2026

Copy link
Copy Markdown

DateTimeSensorAsync.__init__ calls timezone.parse(self.target_time) at Dag-parse time
when start_from_trigger=True, before Jinja rendering has happened. target_time is a
documented template field (e.g. "{{ data_interval_end.tomorrow().replace(hour=1) }}"), so
passing a template — the field's normal, documented use case — raises a raw
pendulum.parsing.exceptions.ParserError right there, crashing parsing of the entire Dag
file
, not just the one task.

Relation to #70310 and #70469

Both prior attempts at this issue (#70310 by @haseebmalik18, #70469 by @burakcoleman) catch
the parse failure in __init__ and raise a clearer ValueError instead of the raw
ParserError. That's a real improvement to the error message, but as reviewers on both PRs
pointed out, it doesn't fix the crash — the Dag still fails to parse whenever
start_from_trigger=True and target_time is a template.

This PR takes the approach @haseebmalik18 outlined in the #70310 review discussion: defer
resolving target_time until the triggerer can render it, the same way FileSensor already
handles a templated filepath.

What this PR does

On Airflow >= 3.3, instead of parsing target_time at Dag-parse time, the raw (unrendered)
template string is passed straight through to DateTimeTrigger via
start_trigger_args.trigger_kwargs under the key "target_time" — which matches
DateTimeSensor.template_fields. BaseTrigger's task_instance setter already picks up any
trigger_kwargs key that matches an operator template field (see
airflow.triggers.base.BaseTrigger), and TriggererJobRunner.run_trigger calls
render_template_fields() on the trigger before invoking run() — this is exactly the
mechanism introduced in #55068 and that FileSensor already relies on for filepath.

DateTimeTrigger now accepts either a resolved moment (unchanged, existing behavior) or a
raw target_time string. In the latter case, moment starts as None and is lazily parsed
from target_time the first time it's needed (run() or serialize()), by which point the
triggerer has already rendered the template in place.

On Airflow < 3.3, that triggerer-side rendering mechanism doesn't exist yet, so a templated
target_time can never be resolved via start_from_trigger. Rather than crash Dag parsing on
every parse cycle, start_from_trigger is disabled with a log.warning, and the task falls
back to deferring from the worker via execute() instead — at that point target_time has
already been rendered normally by the scheduler/worker, so it works correctly, just without
the trigger-only optimization.

Static/ISO target_time values are completely unaffected on any version.

Testing

  • Added unit tests in test_temporal.py covering: DateTimeTrigger accepting an unrendered
    target_time at construction without raising, resolving it once rendered, raising a clear
    error if it's still unrendered when needed, rejecting both/neither of moment/target_time,
    and a full simulated triggerer flow (construct with raw template → render_template_fields
    run()) that fires at the correct time.
  • Added unit tests in test_date_time.py covering DateTimeSensorAsync.__init__ branching on
    AIRFLOW_V_3_3_PLUS for a templated target_time, and confirming a static target_time
    behaves identically regardless of that flag.
  • Ran the exact reproduction script from DateTimeSensorAsync crashes Dag parsing with templated target_time + start_from_trigger=True #70284: confirmed it raises
    pendulum.parsing.exceptions.ParserError on main, and parses cleanly with this fix.
  • Full existing test suites for both files pass (32/32), no regressions.
  • ruff check / ruff format --diff clean on all changed files.

closes: #70284
Related: #70310, #70469, #55068, #69610

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Sonnet 5 following the guidelines

…m_trigger

Real fix for apache#70284 (not just a nicer error message like apache#70310/apache#70469 -- see PR description). Defers target_time resolution to the triggerer via template-field rendering, same mechanism FileSensor uses for filepath (apache#55068).
@boring-cyborg

boring-cyborg Bot commented Jul 28, 2026

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

@SameerMesiah97 SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me overall. But I would go through the diff again and see if any docstrings/comments could be shortened/removed. I have pointed a few of them that I felt were a bit too verbose but there are certainly others that can be more concise.

CI needs to be triggered

# target_time couldn't be parsed as a static datetime at Dag-parse time. This is
# the normal, documented case of target_time being a Jinja template, e.g.
# "{{ data_interval_end.tomorrow().replace(hour=1) }}" -- not necessarily bad input.
moment = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catches all ValueErrors and treats them as if the value were simply "not yet renderable". Is that intentional? For example, an invalid static value like "not-a-date" would now follow the same path as an unresolved Jinja template. Should those cases be distinguished so genuinely invalid input still fails eagerly?

This requires either a static ``target_time`` (a datetime or ISO-8601 string) or, on
Airflow >= 3.3, a templated ``target_time`` that the triggerer can render before the trigger
runs. On earlier Airflow versions a templated ``target_time`` cannot be resolved this way, so
``start_from_trigger`` is disabled with a warning and the task defers from the worker instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be more concise. Please see the below:

:param start_from_trigger: Start the task directly from the triggerer instead of a worker.
    Supports static ``target_time`` values and, on Airflow >= 3.3, templated
    ``target_time`` values.

# operator's template field ("target_time"). The triggerer renders any
# start_trigger_args kwarg whose name matches an operator template field before
# running the trigger (see BaseTrigger.task_instance / render_template_fields),
# the same mechanism FileSensor relies on for a templated `filepath`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is too long. Please see the below;

# Pass the unresolved template to the trigger. On Airflow >= 3.3 the
# triggerer renders trigger kwargs that correspond to template fields before
# starting the trigger.

# runs, so a templated target_time can never be resolved via start_from_trigger.
# Falling back to the normal worker-deferred path (execute()) avoids crashing Dag
# parsing on every parse cycle; execute() runs after the scheduler/worker has
# already rendered target_time normally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment needed? Looks like the warning message communicates the necessary information. I think this comment can be removed or trimmed.

``template_fields``, so the triggerer renders it in place (see
``BaseTrigger.render_template_fields``) before ``run()`` is invoked; it is then parsed into
``moment`` on first use. This mirrors how ``FileSensor`` defers rendering of a templated
``filepath`` to the triggerer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too long as well. Please see the below:

:param target_time: Templated ``target_time`` value to resolve when the trigger
    runs. Used instead of ``moment`` when the target time cannot be determined
    during operator initialization. Mutually exclusive with ``moment``.

if self.moment is not None:
return self.moment
if not self.target_time:
raise TypeError("DateTimeTrigger requires either 'moment' or 'target_time' to be set")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why duplicate the validation logic in the constructor here? Will it not be guaranteed that moment or target_time is set by the time _resolve_moment is called?

# scripts/ci/prek/check_trigger_serialize_init.py). By the time serialize() is called,
# target_time has either already been rendered by the triggerer (see
# BaseTrigger.render_template_fields) or resolution raises a clear error -- there is no
# unrendered template left to preserve.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this comment is needed. The code is self-explanatory.

dag=self.dag,
)
assert op.start_from_trigger is True
assert op.start_trigger_args.trigger_kwargs["moment"] == pendulum.parse("2020-01-01T00:00:00+00:00")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth adding a test for a non-templated bt invalid target_time (e.g. "not-a-date") so the expected behaviour for genuinely invalid input is explicit?

@nanjeshramesh

Copy link
Copy Markdown
Author

Thanks for the thorough review!

On the except ValueError catch-all — good catch, that was a real gap. A genuinely invalid static target_time (e.g. "not-a-date") was being treated the same as an unrendered Jinja template and silently deferred instead of failing at Dag-parse time like before. Fixed by checking for actual Jinja delimiters ({{/{%) before assuming "unrendered template" — anything else re-raises immediately. Added test_async_start_from_trigger_invalid_static_target_time_fails_fast to cover it.

On _resolve_moment's validation check__init__ does guarantee one of moment/target_time is set, so this is only a defensive guard against target_time being cleared after construction (rather than surfacing a confusing AttributeError later). Added a comment explaining that instead of removing it.

Docstrings/comments — trimmed all of them to roughly your suggested wording (the start_from_trigger param doc, the template-hand-off comment, the pre-3.3 fallback comment, the target_time param doc on DateTimeTrigger, and the serialize() comment).

Pushed as three commits: fail-fast fix + trimmed docs, further comment trims, and the new test.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DateTimeSensorAsync crashes Dag parsing with templated target_time + start_from_trigger=True

2 participants