-
Notifications
You must be signed in to change notification settings - Fork 0
fix(phx_channel): buffer pushes that arrive before on() is registered #2
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 |
|---|---|---|
|
|
@@ -37,6 +37,14 @@ def __init__(self, socket: Socket, topic: str, params: dict[str, Any]): | |
| self._event_handlers: dict[str, list[Callable[..., Any]]] = {} | ||
| self._pending_replies: dict[str, asyncio.Future[dict[str, Any]]] = {} | ||
| self._push_buffer: list[tuple[str, Any, asyncio.Future[dict[str, Any]]]] = [] | ||
| # Inbound pushes that arrived before any handler was registered. | ||
| # Server scenarios can `autoPush` in response to a join frame, and | ||
| # that push can land on the asyncio queue before the caller has | ||
| # had a chance to register a handler post-join. We replay these | ||
| # to the first matching handler registered within the window. | ||
| # Bounded so a forgotten handler doesn't leak unbounded memory. | ||
| self._pending_pushes: dict[str, list[Any]] = {} | ||
| self._pending_pushes_cap: int = 32 | ||
|
|
||
| @property | ||
| def topic(self) -> str: | ||
|
|
@@ -195,11 +203,22 @@ def on(self, event: str, callback: Callable[..., Any]) -> Callable[[], None]: | |
| """ | ||
| Register a callback for a channel event. | ||
|
|
||
| Returns an unsubscribe function. | ||
| Returns an unsubscribe function. If pushes for this event already | ||
| arrived before any handler was registered (e.g. server-side | ||
| autoPush fired on join), they're replayed to this callback in | ||
| arrival order so the caller can't miss them due to scheduling. | ||
| """ | ||
| handlers = self._event_handlers.setdefault(event, []) | ||
| handlers.append(callback) | ||
|
|
||
| pending = self._pending_pushes.pop(event, None) | ||
| if pending: | ||
| for payload in pending: | ||
| try: | ||
| callback(payload) | ||
| except Exception: | ||
| logger.exception("Error in handler for %s:%s (replayed)", self._topic, event) | ||
|
|
||
| def unsubscribe() -> None: | ||
| handlers.remove(callback) | ||
|
|
||
|
|
@@ -225,13 +244,22 @@ def _on_message( | |
| elif event == "phx_error": | ||
| self._handle_error(payload) | ||
| else: | ||
| # User event — dispatch to handlers | ||
| handlers = self._event_handlers.get(event, []) | ||
| for handler in handlers: | ||
| try: | ||
| handler(payload) | ||
| except Exception: | ||
| logger.exception("Error in handler for %s:%s", self._topic, event) | ||
| # User event — dispatch to handlers, or buffer if none yet. | ||
| handlers = self._event_handlers.get(event) | ||
| if handlers: | ||
| for handler in handlers: | ||
| try: | ||
| handler(payload) | ||
| except Exception: | ||
| logger.exception("Error in handler for %s:%s", self._topic, event) | ||
| else: | ||
| buf = self._pending_pushes.setdefault(event, []) | ||
| if len(buf) < self._pending_pushes_cap: | ||
| buf.append(payload) | ||
| else: | ||
| # Drop the oldest to bound memory for orphan events. | ||
|
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.
|
||
| buf.pop(0) | ||
|
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. Silent drop — when the buffer is full, the oldest payload is discarded with no observable signal; a user seeing mysteriously missing events will have zero clue why. Add |
||
| buf.append(payload) | ||
|
|
||
| def _handle_reply(self, ref: str | None, payload: Any) -> None: | ||
| if ref and ref in self._pending_replies: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Buffer replay is cleared on the first
on()registration — a secondch.on("evt", cb2)gets nothing from the buffer even though_on_messagefans out to all handlers once any are registered. Socb1gets buffered payloads andcb2silently doesn't, which is asymmetric with live-delivery behavior. Either fan-out replay to all current registrations at the timeon()is called (iterate all handlers in the list, not justcallback), or document that_pending_pushesis drained by the first subscriber and intentionally not delivered to late ones.