Skip to content

fix: rehydrate state trees that expire during a background task - #7585

Open
FarhanAliRaza wants to merge 4 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/7478-background-rehydrate
Open

FarhanAliRaza wants to merge 4 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/7478-background-rehydrate

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A background task releases the state lock while it runs. If the session expires in that time, async with self gets a recreated tree. The delta then carries only the handler's changes, and the browser keeps the values of the expired session (#7478).

A foreground event already recovers from this. When the rx.State root has empty router_data, _execute_event runs _rehydrate. This PR makes StateProxy.__aenter__ use the same signal:

  • It restores the router from the event context.
  • It calls BaseStateEventProcessor._rehydrate, now a staticmethod because it did not use self.

The hydrate snapshot carries is_hydrated=false, so the frontend does not write client storage. The router, the dynamic route args and rx.LocalStorage/rx.Cookie values stay correct. The result is the same as a foreground event on main today.

Supersedes #7543. That PR tracks recreated states in every manager and adds a Redis manifest key. This PR needs no new tracking state, and it also covers a Redis tree that expired completely.

Closes #7478

Test plan

  • New unit test test_background_task_rehydrates_state_tree_expired_while_unlocked, for the memory and Redis managers. It fails on main and passes with this change.
  • tests/units/istate, tests/units/reflex_base, test_state.py, test_event.py and test_app.py pass. The istate and event processor tests also pass against a live Redis.
  • A manual run in a small app with a root state, a child state, a cross-state computed var, a ComponentState, an rx.LocalStorage var and a dynamic route. The background task sleeps longer than the token TTL.
    • Memory, full expiry: the expired values reset. pid, the router, is_hydrated and the browser's localStorage are kept.
    • Redis, full expiry and partial expiry: the expired values reset, and pid and localStorage are kept.
    • Control, no expiry: the background update sends no rehydrate snapshot.

Known limit, the same as the foreground path on main: on Redis, hydrate sends only the states loaded in the tree, so a sibling state that the task does not load keeps its old value.

View guided diff

A background task releases the state lock while it runs. If the session
expires in that time, `async with self` gets a recreated tree, and the
delta carries only the handler's changes. The browser keeps the values
of the expired session.

A recreated `rx.State` root has empty `router_data`. A foreground event
already uses this signal to rehydrate. `StateProxy.__aenter__` now uses
the same signal: it restores the router from the event context and runs
`BaseStateEventProcessor._rehydrate`. The hydrate snapshot carries
`is_hydrated=false`, so the browser keeps its client storage.

Closes reflex-dev#7478
@FarhanAliRaza
FarhanAliRaza requested a review from a team as a code owner October 16, 2026 19:34

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread reflex/istate/proxy.py
Comment thread news/+background-task-expired-state-rehydrate.bugfix.md Outdated
@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] Fixes state rehydration when background tasks resume after session expiry.

The PR still needs the old-route recovery issue fixed before merging.

Findings

  1. P1 Old page values return ▶

Summary

Background tasks now rehydrate loaded states when they re-enter an expired session.

  • Background tasks restore expired state trees when they re-enter.

Reviews (3) · Last reviewed commit: "test: disable oplock when simulating Red..." · Reviewed by Greptile

Comment thread reflex/istate/proxy.py
Comment thread reflex/istate/proxy.py
Comment on lines +277 to +281
if ctx.router_data:
mutable_state.router_data = mutable_state._update_router_vars(
ctx.router_data, {}
)
await BaseStateEventProcessor._rehydrate(mutable_state)

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.

P1 Old page values return

ctx.router_data belongs to the event that started the background task, not necessarily the page the user is viewing now. If the task starts on /a, the user visits /b, and the session then expires before the task resumes, this code restores /a and sends its router values to the browser. _rehydrate also selects /a’s load handlers.

Recover the current browser route before rehydrating instead of treating the task’s old route as current.

Mark the proxy mutable before the rehydrate, so a failure or a
cancellation goes through __aexit__ and closes the state manager
context.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0593158a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread reflex/istate/proxy.py
Comment thread reflex/istate/proxy.py
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T19:43:28.539049Z d059315 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codspeed

codspeed Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 12 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 148 untouched benchmarks


Comparing FarhanAliRaza:farhan/7478-background-rehydrate (1792b36) with main (5ae5dcd)

Open in CodSpeed

Oplock keeps the tree cached in the process, so deleting the Redis keys
does not expire it.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

1 participant