Skip to content

fix(vars): use monotonic time for computed var intervals#6768

Open
anxkhn wants to merge 1 commit into
reflex-dev:mainfrom
anxkhn:fix/computed-var-monotonic-interval
Open

fix(vars): use monotonic time for computed var intervals#6768
anxkhn wants to merge 1 commit into
reflex-dev:mainfrom
anxkhn:fix/computed-var-monotonic-interval

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Cached computed vars used datetime.now() to decide when an update interval had
elapsed. Wall-clock adjustments could therefore refresh a value early or keep it
cached longer than requested.

This change:

  • measures update intervals with time.monotonic() for synchronous and
    asynchronous computed vars
  • tags each reading with a process-local clock identifier, expiring legacy or
    cross-process persisted timestamps instead of comparing incompatible clocks
  • adds regression coverage for forward and backward wall-clock changes and
    incompatible persisted timestamp formats

Because monotonic clocks cannot be compared across processes, an interval-cached
value restored in another worker now recomputes on first access. Subsequent interval
checks use elapsed time within that process.

Testing

  • uv run pytest tests/units/vars/test_base.py -q (18 passed)
  • uv run pytest tests/units --cov --no-cov-on-fail --cov-report= (6630 passed,
    17 skipped, 78.07% coverage)
  • uv run ruff check .
  • uv run ruff format --check .
  • uv run pyright reflex tests (0 errors)
  • uv run towncrier check --config pyproject.toml --dir packages/reflex-base --compare-with origin/main

The regression tests fail against the previous implementation and pass with this
change.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn anxkhn requested a review from a team as a code owner July 15, 2026 04:48
@codspeed-hq

codspeed-hq Bot commented Jul 15, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing anxkhn:fix/computed-var-monotonic-interval (9339a6c) with main (65a2889)2

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (127e4d8) during the generation of this report, so 65a2889 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes interval-cached computed vars to use time.monotonic() instead of datetime.now(), preventing wall-clock adjustments (NTP syncs, DST, manual changes) from causing premature or delayed cache refreshes. A process-local UUID tag (_MONOTONIC_CLOCK_ID) is stored alongside each monotonic timestamp so that persisted timestamps from a different process or a legacy format are safely expired on first access rather than producing an incompatible comparison.

  • needs_update() in ComputedVar now validates the stored value is a (_MONOTONIC_CLOCK_ID, float) tuple before comparing elapsed seconds; any other shape (old datetime, different-process UUID) triggers an unconditional recompute.
  • Both the synchronous __get__ and the async _awaitable_result paths are updated symmetrically to write (_MONOTONIC_CLOCK_ID, time.monotonic()).
  • New tests verify that only the monotonic delta controls expiry (wall-clock shifts are irrelevant) and that legacy/cross-process timestamps are correctly expired.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to the interval-cache path, both sync and async computed var paths are updated symmetrically, and cross-process serialization falls back gracefully to a full recompute.

The core logic is correct: the UUID guard cleanly handles legacy and cross-process timestamps, elapsed-time arithmetic is straightforward, and the async path mirrors the sync path exactly. The two pre-existing style observations were already raised in a prior review thread and do not affect correctness.

No files require special attention.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/vars/base.py Replaces datetime.now() with time.monotonic() plus a process-local UUID tag for interval-cache expiry; logic is correct and symmetric across sync/async paths
tests/units/vars/test_base.py Adds regression coverage for wall-clock immunity and incompatible persisted timestamp expiration; the datetime mock is inert (already noted in a prior review thread)
packages/reflex-base/news/6740.bugfix.md New changelog entry describing the monotonic clock fix

Reviews (2): Last reviewed commit: "fix(vars): use monotonic time for comput..." | Re-trigger Greptile

Comment on lines +129 to +139
datetime_mock = mocker.patch("reflex_base.vars.base.datetime.datetime")
datetime_mock.now.return_value = initial_wall_time

state = StateTest()
if is_async:
assert await state.async_cv == 1
else:
assert state.cv == 1

monotonic_time += monotonic_delta
datetime_mock.now.return_value = initial_wall_time + wall_clock_delta

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.

P2 Dead datetime mock — wall-clock immunity not actually verified

reflex_base.vars.base.datetime.datetime is patched here, but the updated production code never calls datetime.datetime.now() in the interval-check path anymore. The mock has no effect on execution, so this test does not actually verify that wall-clock changes are ignored — it only verifies that time.monotonic() controls expiry. The wall_clock_delta parameter is therefore inert: the test passes identically whether or not the datetime patch is applied, and a future regression that re-introduces a datetime.datetime.now() call alongside time.monotonic() would not be caught by this test. Either drop the datetime mock (and rename the test parameters/docstring accordingly) or add an assertion that datetime_mock.now was not called after the interval check.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +2499 to +2500
elapsed = time.monotonic() - last_updated[1]
return elapsed < 0 or elapsed > self._update_interval.total_seconds()

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.

P2 elapsed < 0 is unreachable once the UUID guard passes. Within a single process, time.monotonic() is guaranteed to be non-decreasing, so subtracting a previously recorded value from the same process's clock will always yield a non-negative result. Consider removing the dead condition to keep the expression clean.

Suggested change
elapsed = time.monotonic() - last_updated[1]
return elapsed < 0 or elapsed > self._update_interval.total_seconds()
elapsed = time.monotonic() - last_updated[1]
return elapsed > self._update_interval.total_seconds()

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@Alek99 Alek99 closed this Jul 15, 2026
@Alek99 Alek99 reopened this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants