Eagerly initialize last_id to avoid skipping messages#78
Merged
npezza93 merged 2 commits intorails:mainfrom Mar 17, 2026
Merged
Eagerly initialize last_id to avoid skipping messages#78npezza93 merged 2 commits intorails:mainfrom
npezza93 merged 2 commits intorails:mainfrom
Conversation
Currently, `@last_id` is lazily initialized as `MAX(id)` on the listener
thread's first call to `broadcast_messages`.
However, if a broadcast inserts a message _between_ a subscription _and_
that first call, `MAX(id)` returns a value that includes that inserted
message message, causing the query `WHERE id > last_id` to miss it
entirely.
For example:
```
Main thread Listener thread
────────── ───────────────
subscribe()
Listener.new → starts thread → thread starts, enters listen loop
add_channel:
channels["test"] = MAX(id) = 5
event_loop.post(on_success)
interruptible { executor.run! }
on_success fires → subscribed.set
subscribed.wait returns
↑ still hasn't reached broadcast_messages
broadcast("hello") → inserts id=6
broadcast_messages:
@last_id ||= MAX(id) → 6 ← INCLUDES THE MESSAGE
broadcastable(["test"], 6)
→ WHERE id > 6 → nothing returned!
message 6 is SKIPPED
```
The per-channel cursor (`channels["test"] = 5`) doesn't help, because
it's only checked as a secondary filter inside the loop over query
results, which already excluded the message.
**How to repro?**
This is also evident in the existing test suite, where `sleep` is
required after the first `subscribe` to give the listener thread time to
complete the first poll cycle and evaluate `@last_id` to a pre-broadcast
max. Removing that `sleep` causes tests to fail without this change.
**What does this do?**
This change eagerly initializes `@last_id` to avoid missing messages.
**What do I NOT like about this change?**
There is now a DB read in the constructor... The good things are:
- Listeners are already lazily initialized so this is not a boot-time
issue
- DB connection is ready: both the listener thread & `#subscribe` call
perform DB read/writes immediately
---
Thanks and open to suggestions!
xrav3nz
commented
Mar 4, 2026
Comment on lines
+178
to
+179
| 10.times do |i| | ||
| test "#broadcast immediately after #subscribe does not skip messages - iteration #{i}" do |
Contributor
Author
There was a problem hiding this comment.
I can't think of a better way to add a regression test for the race condition. This intentionally does NOT use subscribe_as_queue, to make the whole process as transparent as possible. Happy to change / remove if it feels redundant.
Collaborator
There was a problem hiding this comment.
Yea lets go ahead and remove this i think the removal of line 215 should cover us.
Contributor
Author
There was a problem hiding this comment.
Removed and thanks!
Collaborator
|
Thanks for this. Ill take a look this weekend |
npezza93
requested changes
Mar 17, 2026
Collaborator
npezza93
left a comment
There was a problem hiding this comment.
Otherwise looks good to me
Comment on lines
+178
to
+179
| 10.times do |i| | ||
| test "#broadcast immediately after #subscribe does not skip messages - iteration #{i}" do |
Collaborator
There was a problem hiding this comment.
Yea lets go ahead and remove this i think the removal of line 215 should cover us.
npezza93
approved these changes
Mar 17, 2026
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 does this do?
This change eagerly initializes
@last_idto avoid skipping messages.Why?
Currently,
@last_idis lazily initialized asMAX(id)on the listener thread's first call tobroadcast_messages.solid_cable/lib/action_cable/subscription_adapter/solid_cable.rb
Lines 119 to 121 in af98678
However, if a broadcast inserts a message between a subscription and that first call,
MAX(id)returns a value that includes that inserted message message, causing the queryWHERE id > last_idto miss it entirely.For example:
The per-channel cursor (
channels["test"] = 5) doesn't help, because it's only checked as a secondary filter inside the loop over query results, which already excluded the message.solid_cable/lib/action_cable/subscription_adapter/solid_cable.rb
Lines 134 to 148 in af98678
How to repro?
This is evident in the existing test suite, where
sleepis required after the firstsubscribeto give the listener thread time to complete the first poll cycle and evaluate@last_idto a pre-broadcast max.solid_cable/test/lib/action_cable/subscription_adapter/solid_cable_test.rb
Line 215 in af98678
Removing this
sleepcauses tests to fail without this change.What do I NOT like about this change?
There is now a DB read in the constructor... Though:
#subscribecall perform DB read/writes immediatelyThanks and open to suggestions!