Summary
metaflow/plugins/cards/card_creator.py measures the elapsed runtime of async card-rendering subprocesses using time.time() — a wall-clock that can jump backward (NTP step, manual settimeofday, DST transitions on non-UTC systems). The correct API for monotonically-increasing elapsed-time measurement is time.monotonic().
Locations
In metaflow/plugins/cards/card_creator.py (at HEAD 90ea1d3):
- L31 —
CardProcessManager._register_card_process() stores the start time:
cls.async_card_processes[carduuid] = {
"proc": proc,
"started": time.time(),
}
- L200 —
_wait_for_async_processes_to_finish() reads it:
if time.time() - _async_started > async_timeout:
CardProcessManager._remove_card_process(card_uuid)
break
- L231 —
_run_command() (async branch) reads it again:
if time.time() - _async_started > async_timeout:
CardProcessManager._remove_card_process(card_uuid)
Why this matters
time.time() reflects the system wall-clock; under NTP correction, container clock-skew rebase, or a manual time adjustment, two time.time() calls separated in real-time may differ in either direction. The two failure modes both bite metaflow users running Cards in long-lived environments (Kubernetes, Batch):
- Clock jumps backward during a long-running card render →
time.time() - _async_started becomes small or negative → the timeout never fires → the card subprocess leaks indefinitely (held in CardProcessManager.async_card_processes and never cleaned up).
- Clock jumps forward → the elapsed delta inflates → the subprocess is killed prematurely and the user sees a missing/incomplete card.
time.monotonic() is the documented stdlib API for this case: it cannot go backward and is unaffected by system-time adjustments. Python docs are explicit: "Use this function to monitor differences between calls."
Proposed fix
Three-line change — replace time.time() at L31 / L200 / L231 with time.monotonic(). No other behavioral change; the comparison is still an interval, the stored value is still opaque to all readers.
Test plan
Unit test in test/unit/ that monkeypatches time.time to simulate a backward jump, registers a card process with the patched start time, then verifies the timeout check still fires correctly when the implementation is time.monotonic()-based. (Will include in the PR.)
AI tool disclosure
Bug surfaced by an audit grep for time.time() used in interval contexts (R5 rule in our local PR-audit toolkit). The analysis, fix, and PR will be authored by me — I understand both time.time and time.monotonic semantics and have verified the call sites read as written above.
Happy to take this on if accepted.
Summary
metaflow/plugins/cards/card_creator.pymeasures the elapsed runtime of async card-rendering subprocesses usingtime.time()— a wall-clock that can jump backward (NTP step, manualsettimeofday, DST transitions on non-UTC systems). The correct API for monotonically-increasing elapsed-time measurement istime.monotonic().Locations
In
metaflow/plugins/cards/card_creator.py(at HEAD90ea1d3):CardProcessManager._register_card_process()stores the start time:_wait_for_async_processes_to_finish()reads it:_run_command()(async branch) reads it again:Why this matters
time.time()reflects the system wall-clock; under NTP correction, container clock-skew rebase, or a manual time adjustment, twotime.time()calls separated in real-time may differ in either direction. The two failure modes both bite metaflow users running Cards in long-lived environments (Kubernetes, Batch):time.time() - _async_startedbecomes small or negative → the timeout never fires → the card subprocess leaks indefinitely (held inCardProcessManager.async_card_processesand never cleaned up).time.monotonic()is the documented stdlib API for this case: it cannot go backward and is unaffected by system-time adjustments. Python docs are explicit: "Use this function to monitor differences between calls."Proposed fix
Three-line change — replace
time.time()at L31 / L200 / L231 withtime.monotonic(). No other behavioral change; the comparison is still an interval, the stored value is still opaque to all readers.Test plan
Unit test in
test/unit/that monkeypatchestime.timeto simulate a backward jump, registers a card process with the patched start time, then verifies the timeout check still fires correctly when the implementation istime.monotonic()-based. (Will include in the PR.)AI tool disclosure
Bug surfaced by an audit grep for
time.time()used in interval contexts (R5 rule in our local PR-audit toolkit). The analysis, fix, and PR will be authored by me — I understand bothtime.timeandtime.monotonicsemantics and have verified the call sites read as written above.Happy to take this on if accepted.