Skip to content

fix: make WebSocket subscribe and unsubscribe thread-safe - #20

Draft
chipkent wants to merge 1 commit into
fix/ws-keep-subscriptions-on-reconnectfrom
fix/ws-thread-safe-subscribe
Draft

chipkent wants to merge 1 commit into
fix/ws-keep-subscriptions-on-reconnectfrom
fix/ws-thread-safe-subscribe

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-race>

Builds on #19: its test uses the mock server's inbox.

subscribe(), unsubscribe() and unsubscribe_all() are called while connect() runs, often from another thread, with no lock. The receive loop read scheduled_subs, awaited two sends, then copied scheduled_subs into subs and cleared schedule_resub. A change made during either await was recorded as sent but never sent, so the server streamed different topics from the ones requested.

  • A lock now guards scheduled_subs and schedule_resub. The three methods hold it for their whole body, through a small decorator, so their code is not re-indented.
  • The loop copies scheduled_subs and clears schedule_resub under the lock, sends from the copy, and records only the copy in subs. A change made during the sends goes out on the next turn.
  • Code that changes scheduled_subs itself and sets schedule_resub = True still works.
# threads call subscribe()/unsubscribe() while connect() runs
# before: the server can end up streaming a different set from scheduled_subs
# now: it streams exactly scheduled_subs

Tests

  • New test_threads.py makes a change from inside the subscribe send, the window another thread can hit, and checks that the server receives it. It fails every time without the fix.
  • 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.

subscribe(), unsubscribe() and unsubscribe_all() are called while
connect() runs, often from another thread, but they changed
scheduled_subs and schedule_resub with no lock. The receive loop read
scheduled_subs, awaited the subscribe and unsubscribe sends, and then
copied scheduled_subs into subs and cleared schedule_resub. A change
made during either await was copied into subs without being sent, and
its schedule_resub was cleared, so it was never sent. The server then
streamed different topics from the ones requested, while subs said
everything was sent.

Guard both attributes with a lock, which the three methods take for
their whole body. The receive loop now copies scheduled_subs and clears
schedule_resub under the lock, sends the difference from that copy, and
records the copy in subs. A change made during the sends sets
schedule_resub again and goes out on the next turn.

Code that changes scheduled_subs itself and sets schedule_resub keeps
working: the loop still checks the flag on every turn.
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