Use monotonic clock for OpenAIBatchTrigger polling timeout - #69534
Conversation
3e9bc0f to
94aeeb2
Compare
|
Fixed the CI failures:
Verified the trigger under a real event loop locally. Drafted-by: GitHub Copilot (Claude Opus 4.6); reviewed by @YAshhh29 before posting |
94aeeb2 to
82da305
Compare
82da305 to
7049699
Compare
7049699 to
c1ac129
Compare
potiuk
left a comment
There was a problem hiding this comment.
Good catch, and it fixes a real violation of our own standard — a deferred trigger is exactly the long-lived thing most exposed to wall-clock jumps, so measuring its deadline against time.time() could either cut a batch short or let it run far past the intended timeout.
The rolling-upgrade handling is the part I most appreciate. Keeping end_time accepted and serializing whichever argument the trigger was constructed with means a trigger serialized by the previous operator and still deferred through an upgrade keeps deserializing correctly, instead of failing on an unexpected keyword. Converting the legacy deadline into a remaining duration exactly once and tracking the rest monotonically is the right shape, and it is the detail most changes like this skip.
It also quietly fixes a misleading message: the old text interpolated time.time() - self.end_time, which is the overshoot past the deadline rather than elapsed time, while claiming "has not reached a terminal status after N seconds". A batch running an hour past a sixty second deadline reported a number unrelated to either.
test_timeout_uses_monotonic_not_wall_clock is the test that earns its place — patching both clocks proves the wall clock is genuinely ignored, rather than just that some timeout eventually fires.
One small thing, not worth holding this for: end_time is documented as deprecated but raises no AirflowProviderDeprecationWarning. I suspect that is deliberate, since warning on every deserialization of an in-flight trigger would be noise during exactly the upgrade window the compatibility exists for — but a direct user of the trigger gets no signal that they should move.
Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting
Two PRs merged three hours apart on 2026-08-01 collided semantically: #69506 added a test using the class attribute END_TIME, and #69534 renamed that attribute to LEGACY_END_TIME while branched off a main that predated #69506. Git merged both cleanly, so the dangling reference reached main unnoticed and every job that collects the OpenAI provider tests now fails. The case asserts that a terminal batch emits exactly one event, which has nothing to do with the deprecated wall-clock deadline, so it moves to the timeout constant that the rest of the behavioural cases use rather than to LEGACY_END_TIME. The end_time path stays covered by test_serialization_with_legacy_end_time.
OpenAIBatchTriggermeasures its polling timeout withtime.time()(wallclock) in two places, and
OpenAITriggerBatchOperatorsets the deadlinewith
time.time() + self.timeout. Because wall-clock time can jump — NTPcorrections, DST, container clock skew, VM pause/resume — a deferred batch
task can either time out early or run far past its intended deadline.
This isn't tied to an existing issue — I spotted it while auditing
time.time()/time.monotonic()usage across the AI/ML providers, in thesame spirit as the dep-audit that produced #69408.
Airflow's own coding standards flag this exact pattern:
Why the fix isn't a one-line swap
time.monotonic()values are only meaningful within a single process. Theoperator (in the worker) and the trigger (in the Triggerer) run in
different processes, so the operator cannot pre-compute a monotonic
deadline and hand it to the trigger. The trigger must call
time.monotonic()itself.To make that possible the trigger's preferred constructor argument
changed from
end_time(absolute wall-clock deadline) totimeout(aduration in seconds). The trigger now records
time.monotonic()at thestart of
run()and reports the elapsed monotonic duration on timeout.Backward compatibility
OpenAIBatchTrigger.__init__still accepts the legacyend_timeargument so that triggers serialized by the previous version of the
operator continue to run after an upgrade. When a legacy
end_timeispresent the trigger derives a best-effort remaining duration from the
wall clock once, then tracks the rest with the monotonic clock — so even
the legacy path is no longer fully at the mercy of wall-clock jumps
inside the polling loop.
serialize()preserves whichever argument thetrigger was constructed with, so a rolling upgrade never rewrites an
in-flight trigger's schema.
What changes
providers/openai/src/.../triggers/openai.pytimeout: float | Noneconstructor arg and validation(exactly one of
timeout/end_timemust be given).run()measures elapsed time viatime.monotonic().serialize()emits whichever field the trigger was constructed with.it reported
time.time() - self.end_time, which is seconds pastthe deadline — misleading and sometimes negative).
providers/openai/src/.../operators/openai.pytimeout=self.timeoutinstead of computingend_time.import time.providers/openai/tests/.../test_openai.pytimeout=.test_serialization_with_legacy_end_timeproves thebackward-compat serialization path.
test_timeout_uses_monotonic_not_wall_clock— the regressiontest — patches
time.monotonicwith an ever-increasing counter andasserts the timeout fires while
time.timeis never consulted. Itfails against the pre-fix code, which decided the timeout from the
wall clock. (
time.monotonicis mocked withitertools.countrather than a fixed list because the asyncio event loop also calls
time.monotonicinternally and would exhaust a finite side_effect.)providers/openai/docs/changelog.rst: bug-fix note explaining thebehavior change and the
end_time→timeoutmigration.How to reproduce the original bug
OpenAITriggerBatchOperator(deferrable=True, timeout=600).backward wall-clock adjustment on the Triggerer host (e.g.
sudo date -s '10 minutes ago', or a large NTP step).self.end_time < time.time()is no longer true.The same pattern also fires early on forward clock jumps.
Related
.github/instructions/code-review.instructions.mdWas generative AI tooling used to co-author this PR?
Generated-by: GitHub Copilot (Claude Opus 4.6) following the guidelines