Skip to content

fix(event): send a failed handler's writes after its last yield - #7696

Open
FarhanAliRaza wants to merge 2 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/flush-delta-on-failed-handler
Open

FarhanAliRaza wants to merge 2 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/flush-delta-on-failed-handler

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Related to #7248. Pairs with #7412.

Problem

A foreground handler can change state after its last yield, for example in an except asyncio.CancelledError block when a supersedes=True call cancels it, or just before it raises. Nothing sends those changes to the frontend.

  • Memory and disk send them late, with the next handler's first delta. The next handler gets the same state object, and that object still marks the vars as dirty.
  • Redis without a cached lease (the default, REFLEX_OPLOCK_ENABLED=False) never sends them. The next handler unpickles a fresh copy, and _init_bookkeeping clears the dirty marks. The frontend shows the old value until the var changes again or the page reloads.

Fix

When process_event raises or is cancelled, _execute_event flushes the delta with chain_updates(None, root_state=...) before the exception propagates. The flush runs under the state lock, so it reaches the frontend before any delta from the next handler. If the flush fails, it is logged and the handler's own exception propagates. The background branch uses the same pattern.

Background handlers are not changed. They already flush in their finally.

Tests

  • test_failed_handler_emits_writes_after_last_yield: memory, disk and redis, each cancelled and raising. All 6 cases fail on main and pass with the fix.
  • test_failed_handler_flush_error_keeps_handler_exception: a computed var makes the flush fail. The handler's RuntimeError is reported and the flush error is logged. It fails when the except in _flush_failed_handler is removed.

Notes

View guided diff

A foreground handler that is cancelled or raises could leave changes it
made after its last yield unsent. Memory and disk carried them in the
next handler's delta, but a state manager that loads a fresh copy, as
redis does, dropped the dirty marks, so the frontend never received
them until the var changed again or the page reloaded.

Flush the delta under the state lock before the exception propagates.
A flush failure is logged so the handler's own exception still
propagates.
@FarhanAliRaza
FarhanAliRaza requested a review from a team as a code owner October 29, 2026 17:08
@FarhanAliRaza FarhanAliRaza added on deck PRs lined up to review / merge next breaking Public API facing change considered for minor release bump pre-v1.0 labels Oct 29, 2026

@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.

No issues found across 3 files

View guided diff | Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with the existing non-blocking cancellation-test concern still open.

Findings

  1. P2 Cancellation test skips completion ▶

Summary

Sends state changes made by a foreground handler before it fails or is cancelled.

  • Foreground handlers send their final state writes when they fail.

Reviews (2) · Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." · Reviewed by Greptile

Comment on lines +1481 to +1483
future.cancel()
with contextlib.suppress(BaseException):
await asyncio.wait_for(future.wait_all(), timeout=5)

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 Cancellation test skips completion

future.cancel() has already completed the future, so wait_all() raises immediately instead of waiting for the running handler. Leaving the processor context then starts shutdown, which can cancel the task again if its flush suspends. The test therefore does not establish that normal cancellation sends cleanup before shutdown.

Wait for a handler-completion signal or a following event before leaving the context, then check the delta.

@codspeed

codspeed Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 18 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 148 untouched benchmarks


Comparing FarhanAliRaza:farhan/flush-delta-on-failed-handler (4fd54f2) with main (df3eb52)

Open in CodSpeed

…on-failed-handler

# Conflicts:
#	tests/units/reflex_base/event/processor/test_base_state_processor.py

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

breaking Public API facing change considered for minor release bump pre-v1.0 on deck PRs lined up to review / merge next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant