Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/reflex-base/news/6801.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enqueuing an event chained from a parent event that has already finished no longer raises `RuntimeError: Cannot add a child to an EventFuture that is already done.`. Such a late-chained event skips registration under the completed parent and is processed normally.
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,10 @@ async def enqueue(
# tracker since this event will never enter the queue.
tracked.cancel()
return tracked
parent_future.add_child(tracked)
# Skip registration if the parent is already done (late-chained
# event) so the child runs instead of crashing.
if not parent_future.done():
parent_future.add_child(tracked)
if parent_future is None:
self._supersede_previous(token=token, event=event, tracked=tracked)
await queue.put(EventQueueEntry(event=event, ctx=ev_ctx))
Expand Down
34 changes: 34 additions & 0 deletions tests/units/reflex_base/event/processor/test_event_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import contextlib
import dataclasses
import logging
from typing import Any

Expand All @@ -13,6 +14,7 @@
QueueShutDown,
_stream_queue_until_done,
)
from reflex_base.event.processor.future import EventFuture
from reflex_base.registry import RegistrationContext

from reflex.event import Event, EventHandler
Expand Down Expand Up @@ -622,6 +624,38 @@ async def test_chained_event_processed(token: str):
assert _CALL_LOG == [{"value": "chained"}]


async def test_enqueue_child_of_done_parent_does_not_crash(
mock_event_processor: EventProcessor,
token: str,
):
"""Regression: a late-chained event whose parent future already completed
still runs instead of crashing when registered as the parent's child.

Args:
mock_event_processor: The event processor with mock root context.
token: The client token.
"""
async with mock_event_processor as ep:
done_parent = EventFuture(txid="parent-txid")
done_parent.set_result(None)
ep._futures["parent-txid"] = done_parent

assert ep._root_context is not None
child_ctx = dataclasses.replace(
ep._root_context.fork(token=token), parent_txid="parent-txid"
)
future = await ep.enqueue(
token,
Event.from_event_type(logging_event("late-child"))[0],
ev_ctx=child_ctx,
)
await future

assert _CALL_LOG == [{"value": "late-child"}]
# The child is not registered under the already-done parent.
assert done_parent.children == []


async def test_join_when_not_started(processor: EventProcessor):
"""join() when not started is a no-op (queue is None).

Expand Down
Loading