Skip to content

feat: send WebSocket subscription changes without waiting for recv - #21

Draft
chipkent wants to merge 1 commit into
fix/ws-thread-safe-subscribefrom
feat/ws-immediate-subscriptions
Draft

chipkent wants to merge 1 commit into
fix/ws-thread-safe-subscribefrom
feat/ws-immediate-subscriptions

Conversation

@chipkent

@chipkent chipkent commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Preview only. This is a draft in the fork showing the exact text of the PR planned for massive-com/client-python. It will be closed, not merged, once the upstream PR is opened.

Fixes #<issue: F1a-delay>

Builds on #20.

connect() sent subscription changes only when its receive loop came round again, after the next message or the 1-second receive timeout. On a quiet feed, a subscribe() took up to a second to reach the server.

  • Each connection now has one task that sends subscription changes. It starts after authentication by sending the whole scheduled set.
  • subscribe(), unsubscribe() and unsubscribe_all() wake it through loop.call_soon_threadsafe(), which never blocks the caller.
  • As the only sender, it never interleaves messages, and it merges a burst of changes into one subscribe and one unsubscribe message.
  • The receive loop still checks schedule_resub on every turn, so setting scheduled_subs and schedule_resub directly still works.
  • Sending while another coroutine waits in recv() is safe in websockets 14.0 through 17.3, the current release; the commit message cites the lines.

Throughput is unchanged: with a local server sending 20 trades per message, the median time per message is within 1% of master on 3.10 and 3.14 (benchmark script available on request).

Tests

  • New test_sender.py: with the receive timeout removed, a change made from another thread still reaches an idle connection (fails on master), and direct writes to scheduled_subs still work.
  • make test and make lint pass on 3.10 and 3.14.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

connect() sent subscription changes only when its receive loop came
round again, after the next message or the one-second recv timeout. On
a quiet feed, a subscribe() took up to a second to reach the server.

Each connection now has one task that sends subscription changes. It
starts after authentication by sending the whole scheduled set, then
waits on an asyncio.Event. subscribe(), unsubscribe() and
unsubscribe_all() set the event through call_soon_threadsafe(), which
never blocks the caller. Being the only sender, the task never
interleaves messages, and it merges a burst of changes into one
subscribe and one unsubscribe message.

The receive loop still checks schedule_resub on every turn, now to wake
the task, so code that changes scheduled_subs itself and sets the flag
keeps working. An error other than a closed connection ends the task
and is raised by connect(), as before. The task is stopped when the
connection ends. The receive path is otherwise unchanged.

Sending while another coroutine waits in recv() on the same connection
is safe in websockets 14.0 through 17.3, the current release
(websockets/asyncio/connection.py). recv() only reads from the message
assembler (14.0 L270, 14.2 L294, 15.0.1 L303, 16.1.1 L305, 17.3 L298),
whose only concurrency check rejects a second concurrent reader
(asyncio/messages.py, Assembler.get: 14.0 L142, 14.2 L141, 15.0.1 L150,
16.1.1 L149, 17.3 L148). send() waits only for a fragmented send in
progress (fragmented_send_waiter, named send_in_progress in 16.1.1 and
17.3: 14.0 L427, 14.2 L451, 15.0.1 L469, 16.1.1 L472, 17.3 L466) before
writing through send_context() (14.0 L434, 14.2 L458, 15.0.1 L476,
16.1.1 L479, 17.3 L473).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant