Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,9 @@ async def enqueue(
self._futures[txid] = tracked
tracked.add_done_callback(self._try_clean_future)
tracked.add_done_callback(self._on_future_done)
# If this context has a parent, register as a child of the parent's future.
if parent_future is not None:
# Register as a child of the parent's future; skip if the parent is
# already done (late-chained event) so the child runs instead of crashing.
if parent_future is not None and not parent_future.done():
parent_future.add_child(tracked)
await queue.put(EventQueueEntry(event=event, ctx=ev_ctx))
return tracked
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
from typing import Any

import pytest
Expand All @@ -11,6 +12,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 @@ -458,6 +460,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