Check single-subscription channels before queueing, not during replay - #319
Open
pucedoteth wants to merge 1 commit into
Open
Check single-subscription channels before queueing, not during replay#319pucedoteth wants to merge 1 commit into
pucedoteth wants to merge 1 commit into
Conversation
`userEvents` and `orderUpdates` cannot be multiplexed, and `subscribe` rejects a
second one with `NotImplementedError`. That check only ran on the connected
path, so subscribing twice before the socket opened was accepted, queued, and
only rejected later while `on_open` replayed the queue.
The exception then escapes inside the websocket callback, where the caller
cannot catch it, and it aborts the replay loop. Every subscription queued behind
the duplicate is silently dropped:
ws_manager.subscribe({"type": "userEvents"}, cb) # queued
ws_manager.subscribe({"type": "userEvents"}, cb) # queued, no error
ws_manager.subscribe({"type": "l2Book", "coin": "ETH"}, cb)
# on_open -> NotImplementedError on the second entry
# frames sent to the server: 1
# l2Book:eth registered: False
The same two calls after the socket is open raise at the call site, so identical
user code either raises where it is written or loses an unrelated market data
feed, depending only on connection timing.
Run the check in `subscribe` for both paths, counting queued entries as well as
active ones, so the duplicate is refused where it is requested. `on_open` now
takes the queue before replaying it: `subscribe` consults that list, and leaving
entries in place would also replay them again on a later `on_open`.
Behaviour on the connected path is unchanged, and channels that do multiplex
still accept several callbacks.
Tests: `tests/websocket_manager_test.py` covers the duplicate on both paths, the
dropped-subscription case, queue replay and clearing, and multiplexing. Against
the unmodified file three of the five fail; the two that pass either way are the
connected-path duplicate and the multiplexing case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
userEventsandorderUpdatescannot be multiplexed, andsubscriberejects a second one withNotImplementedError. That check only ran on the connected path, so subscribing twice before the socket opens is accepted and queued, then rejected later whileon_openreplays the queue.Why it matters
The exception escapes inside the websocket callback, where the caller cannot catch it, and it aborts the replay loop. Every subscription queued behind the duplicate is silently dropped.
Against
master:The ETH order book feed is never subscribed and never reported as missing.
The same two calls after the socket is open raise at the call site:
So identical user code either raises where it is written, or loses an unrelated market-data feed, depending only on whether the socket happened to be open yet.
Info(skip_ws=False)subscribes during construction, so the queued path is the common one at startup.How
Run the check in
subscribefor both paths, counting queued entries as well as active ones, so the duplicate is refused where it is requested.on_opennow takes the queue before replaying it.subscribeconsults that list, so it has to be drained first, and leaving entries in place would also replay them again on any lateron_open.Behaviour on the connected path is unchanged, and channels that do multiplex still accept several callbacks on the same identifier.
Tests
Added
tests/websocket_manager_test.py, covering the duplicate on both paths, the dropped-subscription case, queue replay and clearing, and multiplexing. The stub socket means nothing connects.Against the unmodified
websocket_manager.py:The two that pass either way are the connected-path duplicate and the multiplexing case, so the change is scoped to the broken path. With the fix the full suite is green: 41 passed.