-
Notifications
You must be signed in to change notification settings - Fork 81
fix: address review comments from PRs #2604 and #2619 #2634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,6 +57,15 @@ class WSBroadcaster(EventConsumer): | |
| consumer_id: str = "ws_broadcaster" | ||
|
|
||
| def __init__(self, consumer_id: str | None = None) -> None: | ||
| """Initialize the broadcaster. | ||
|
|
||
| Args: | ||
| consumer_id: Optional unique identifier used when subscribing to | ||
| the session event bus. Defaults to the class-level | ||
| ``"ws_broadcaster"``. Pass a unique value (e.g. per | ||
| reconnect-replay connection) so concurrent broadcasters on the | ||
| same bus do not overwrite each other's subscriptions. | ||
| """ | ||
| self.consumer_id = consumer_id or self.consumer_id | ||
| self._connections: dict[str, _ConnectionInfo] = {} | ||
| self._bus: SessionEventBus | None = None | ||
|
|
@@ -195,3 +204,92 @@ def _passes_filter(kind: AgentEventKind, verbosity: str) -> bool: | |
| return kind in _SUMMARY_KINDS | ||
| # Unknown verbosity -- default to full | ||
| return True | ||
|
|
||
|
|
||
| async def start_reconnect_replay( | ||
| bus: SessionEventBus, | ||
| *, | ||
| conn_id: str, | ||
| send_callback: SendCallback, | ||
| from_sequence: int, | ||
| ) -> WSBroadcaster: | ||
| """Create and start a dedicated broadcaster that replays missed events. | ||
|
|
||
| Used on WebSocket reconnect: a per-connection broadcaster (unique | ||
| ``consumer_id`` so concurrent reconnects don't clobber each other's bus | ||
| subscriptions) is subscribed to *bus* and replays buffered events from | ||
| *from_sequence* to *send_callback*. Callers must pair this with | ||
| :func:`stop_reconnect_replay` on disconnect. If registration or replay | ||
| fails after the broadcaster started, the broadcaster is stopped before | ||
| the error propagates, so no bus subscription leaks. | ||
|
|
||
| Args: | ||
| bus: Session event bus to subscribe the replay broadcaster to. | ||
| conn_id: Unique connection identifier; also seeds the broadcaster's | ||
| ``consumer_id`` (``ws_broadcaster:<conn_id>``). | ||
| send_callback: Awaitable callback that delivers each serialized event | ||
| to the reconnected WebSocket. | ||
| from_sequence: Sequence number after which buffered events are | ||
| replayed to the new connection. | ||
|
|
||
| Returns: | ||
| The started :class:`WSBroadcaster`, already registered with the | ||
| connection and mid-replay/live on the bus. | ||
|
|
||
| Raises: | ||
| Exception: Whatever :meth:`WSBroadcaster.start` or | ||
| :meth:`WSBroadcaster.add_connection` raises; the broadcaster is | ||
| stopped (best-effort) first. | ||
| """ | ||
| broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{conn_id}") | ||
| await broadcaster.start(bus) | ||
| try: | ||
| await broadcaster.add_connection( | ||
| conn_id=conn_id, | ||
| send_callback=send_callback, | ||
| from_sequence=from_sequence, | ||
| ) | ||
| except Exception: | ||
| # replay/send can fail mid-registration; without this stop() the | ||
| # bus subscription and consume task would leak (caller never gets | ||
| # the broadcaster reference to clean up) | ||
| try: | ||
| await broadcaster.stop() | ||
| except Exception as stop_exc: | ||
| logger.bind(conn_id=conn_id, consumer_id=broadcaster.consumer_id).warning( | ||
| "Failed to stop reconnect replay broadcaster after registration error: {}", | ||
| stop_exc, | ||
| ) | ||
| raise | ||
| return broadcaster | ||
|
Comment on lines
+209
to
+264
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If async def start_reconnect_replay(
bus: SessionEventBus,
*,
conn_id: str,
send_callback: SendCallback,
from_sequence: int,
) -> WSBroadcaster:
"""Create and start a dedicated broadcaster that replays missed events.
Used on WebSocket reconnect: a per-connection broadcaster (unique
``consumer_id`` so concurrent reconnects don't clobber each other's bus
subscriptions) is subscribed to *bus* and replays buffered events from
*from_sequence* to *send_callback*. Callers must pair this with
:func:`stop_reconnect_replay` on disconnect.
"""
broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{conn_id}")
await broadcaster.start(bus)
try:
await broadcaster.add_connection(
conn_id=conn_id,
send_callback=send_callback,
from_sequence=from_sequence,
)
except Exception:
await broadcaster.stop()
raise
return broadcaster
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in dd8660b: add_connection is now wrapped so a mid-replay failure stops the broadcaster (best-effort, logged) before re-raising; regression test test_start_reconnect_replay_cleans_up_when_registration_fails asserts no leaked bus subscriber.
qodo-code-review[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| async def stop_reconnect_replay(broadcaster: WSBroadcaster, conn_id: str | None) -> None: | ||
| """Tear down a reconnect-replay broadcaster created by :func:`start_reconnect_replay`. | ||
|
|
||
| Cleanup failures are logged (never silently suppressed) but do not | ||
| propagate: this runs in disconnect paths where raising would mask the | ||
| original connection error. | ||
|
|
||
| Args: | ||
| broadcaster: The broadcaster returned by :func:`start_reconnect_replay`. | ||
| conn_id: Connection identifier to unregister first, or ``None`` to | ||
| skip connection removal and only stop the broadcaster. | ||
| """ | ||
| if conn_id is not None: | ||
| try: | ||
| broadcaster.remove_connection(conn_id) | ||
| except Exception as exc: | ||
| logger.bind( | ||
| action="reconnect_replay_cleanup", | ||
| conn_id=conn_id, | ||
| consumer_id=broadcaster.consumer_id, | ||
| ).warning("Failed to remove reconnect replay connection: {}", exc) | ||
| try: | ||
| await broadcaster.stop() | ||
| except Exception as exc: | ||
| logger.bind( | ||
| action="reconnect_replay_cleanup", | ||
| conn_id=conn_id, | ||
| consumer_id=broadcaster.consumer_id, | ||
| ).warning("Failed to stop reconnect replay broadcaster: {}", exc) | ||
Uh oh!
There was an error while loading. Please reload this page.