redis: re-issue SUBSCRIBE after an auto-reconnect - #33450
Conversation
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughAdds a JSMap ChangesResubscribe on reconnect
Sequence Diagram(s)sequenceDiagram
participant ValkeyClient
participant JSValkeyClient
participant SubscriptionCtx
participant Server
ValkeyClient->>JSValkeyClient: on_open (reset resubscribe_pending=0)
JSValkeyClient->>JSValkeyClient: on_valkey_connect
JSValkeyClient->>JSValkeyClient: resubscribe()
JSValkeyClient->>SubscriptionCtx: subscribed_channels(global_object)
SubscriptionCtx-->>JSValkeyClient: channel keys array
JSValkeyClient->>Server: SUBSCRIBE channels
JSValkeyClient->>ValkeyClient: set resubscribe_pending = channel count
Server-->>ValkeyClient: PUSH Subscribe confirmation
ValkeyClient->>ValkeyClient: decrement resubscribe_pending
Related PRs: None identified. Suggested labels: valkey, redis, jsc, tests, documentation Suggested reviewers: None identified. Poem A rabbit hops through wires anew, 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 2:05 AM PT - Jul 7th, 2026
❌ @robobun, your commit 11855d4 has some failures in 🧪 To try this PR locally: bunx bun-pr 33450That installs a local version of the PR into your bun-33450 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
Not adding #21622 is the npm This PR only changes |
There was a problem hiding this comment.
I didn't find any bugs, but this touches the Redis reconnect state machine, adds promise-pair accounting via a new resubscribe_pending counter, and introduces a new C++ JSC binding — worth a human look at the lifetime handling in resubscribe() and the interaction with the in-flight queue.
Extended reasoning...
Overview
This PR fixes Bun.RedisClient silently going deaf after an auto-reconnect by replaying SUBSCRIBE for every channel in the callback map once the new connection's HELLO handshake completes. It touches:
src/runtime/valkey_jsc/js_valkey.rs— newresubscribe()that iterates the JS-side callback map, serializes a promise-lessSUBSCRIBEstraight into the write buffer, and setsresubscribe_pending; plus asubscribed_channels()helper onSubscriptionCtx.src/runtime/valkey_jsc/valkey.rs— newresubscribe_pending: u32field onValkeyClient, reset inon_open, and a new match arm inhandle_responsethat decrements it and skips promise-pair consumption for the corresponding subscribe-push replies.src/jsc/bindings/bindings.cpp+src/jsc/JSMap.rs— newJSC__JSMap__keysbinding that materializes aJSMap's keys into aJSArrayviaJSMapIterator.docs/runtime/redis.mdx— one bullet documenting the new behavior.test/js/valkey/reliability/subscriber-reconnect.test.ts— 257-line scripted RESP3 server harness with three tests asserting the exact wire-level command sequence on reconnect and the negative contracts.
Security risks
None apparent. The change reads from an internally-managed JSMap (populated only via .subscribe()) and writes RESP-encoded channel names to the socket. No new user-controlled input parsing, no auth/crypto/permission surface.
Level of scrutiny
Moderate-to-high. This is not a mechanical change:
- The
resubscribe_pendingcounter interacts with the existing promise-pair consumption logic inhandle_response. The PR description itself calls it "load-bearing" — removing it makes a queuedPINGresolve with the subscribe-confirmation count instead of"PONG". It also notes this accounting may be subsumed by pending PR #32858, so a maintainer should confirm the two don't conflict. resubscribe()iterates JS values (to_bun_string→to_utf8()→channel_str.deref()) and holds the resultingZigStringSlices in aVecacross the loop. Whether the slice borrows from the deref'dBunStringor owns its allocation is a lifetime detail worth a maintainer's eye.- A new C++ JSC binding (
JSC__JSMap__keys) with aThrowScope,constructEmptyArray, iterator loop, andputDirectIndex— standard shape, but new JSC glue always deserves review for exception-check correctness. - The write goes into the buffer during
on_valkey_connect(post-HELLO), which fordatabase > 0clients is before theSELECTreply is processed; the ordering looks correct but is subtle.
Other factors
The tests are well-constructed (scripted in-process RESP3 server, no Docker dependency, assert exact command lists, cover both positive and negative contracts, verified to fail on USE_SYSTEM_BUN=1). No prior human reviews on the PR. The bug-hunting system found nothing. Given the state-machine subtlety and new JSC binding, deferring to a human reviewer.
A reconnect opens a brand-new server-side connection, which carries none of the previous connection's subscriptions. The client only sent HELLO, so a subscriber reported connected === true with its listeners still registered while no message ever arrived again. Replay SUBSCRIBE for every channel in the subscription callback map as soon as the HELLO handshake completes. The command is written directly to the write buffer, ahead of whatever the offline queue still holds, and carries no promise; resubscribe_pending counts off the confirmations it produces so they do not consume a queued command's promise pair.
3f4a78a to
a547a48
Compare
With a database in the URL, the replayed SUBSCRIBE is written while SELECT's reply is still outstanding. Assert it lands behind SELECT on the wire, or it would subscribe on database 0.
|
Answering the two things flagged as worth a closer look, and rebased onto main (the earlier red CI was a stale base: #33425 fixed the Slice lifetimes in let channel_str = channel.to_bun_string(&global_object)?;
args.push(channel_str.to_utf8());
channel_str.deref();
Ordering against
That was previously untested, so 953d661 pins it: Fails on the unfixed build as |
… slot The replayed SUBSCRIBE carries no promise pair. If the server refuses it (an ACL change during the outage, say) the reply is an Error rather than a confirmation, so the push-kind accounting never ran and the error consumed the pair of whatever user command was parked behind it in the in-flight queue. The subscriber error path then dropped that pair without settling it, hanging the caller's await. Skip pair consumption for an error reply while confirmations are still owed, and stop expecting them: the server rejects SUBSCRIBE as a whole.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/runtime/redis.mdx`:
- Around line 420-421: Clarify the reconnect behavior in the Redis docs by
noting that the replay logic only applies to channels registered via
`.subscribe()` and does not restore pattern subscriptions from `psubscribe()`.
Update the surrounding reconnect/resubscribe guidance in the Redis runtime docs
to explicitly call out this exception so users can distinguish how
`.subscribe()` and `psubscribe()` behave after a reconnect.
In `@src/runtime/valkey_jsc/js_valkey.rs`:
- Around line 1276-1277: The connect flow in `JsValkey::connect` / the
default-database path is leaving `connection_promise` unresolved when
`self.resubscribe()?` fails before the promise-handling block runs. Move the
resubscribe step to after the cached promise is resolved/rejected, or explicitly
reject and clear `connection_promise` on this failure path so `client.fail(...)`
cannot strand `.connect()`.
In `@test/js/valkey/reliability/subscriber-reconnect.test.ts`:
- Around line 190-318: These reconnect tests are independent and should be
parallelized to reduce runtime. Update the `describe("Valkey: subscriber
reconnect", ...)` block or each `test(...)` case to use concurrent execution via
`test.concurrent` or `describe.concurrent`, since each case creates its own
`startRespServer()` and isolated `RedisClient` instances. Make sure any shared
setup/teardown remains per-test so the `subscriber reconnect` scenarios stay
isolated under concurrency.
- Around line 222-225: The cleanup in these subscriber reconnect tests closes
`subscriber` and `publisher` while the client may still be subscribed, which can
leave lingering subscription state. In each affected `finally` block in
`subscriber-reconnect.test.ts`, explicitly unsubscribe the `subscriber` from its
channel(s) before calling `close()`, then close both clients as usual. Apply
this to the test cases around the reconnect scenarios that use
`subscriber`/`publisher` and the `"news"` subscription.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c26dd519-e201-47b0-8cde-f1de104b8518
📒 Files selected for processing (6)
docs/runtime/redis.mdxsrc/jsc/JSMap.rssrc/jsc/bindings/bindings.cppsrc/runtime/valkey_jsc/js_valkey.rssrc/runtime/valkey_jsc/valkey.rstest/js/valkey/reliability/subscriber-reconnect.test.ts
Running resubscribe() ahead of the promise meant an exception from it (building the channel list allocates) returned early and left .connect() pending forever. It still lands ahead of the offline queue, which only drains once the microtask scope and on_writable run. Also note in the docs that psubscribe patterns are not replayed, run the reconnect tests concurrently, and unsubscribe before close() so teardown does not depend on GC to release the event loop.
subscribe() without an explicit connect() populates the handler map and queues the SUBSCRIBE, so the map is non-empty before the handshake finishes. Replaying it there would put SUBSCRIBE on the wire twice. Nothing caught that, so assert the lazy-connect path sends it exactly once.
There was a problem hiding this comment.
Both of my earlier comments are addressed (the error-reply path is now handled, and the is_subscriber guard is pinned by a test), and this pass found nothing new — but the reply-slot accounting and new JSC binding are worth a human look before merge.
Extended reasoning...
Overview
This PR makes Bun.RedisClient re-issue SUBSCRIBE for every channel in the callback map after an auto-reconnect, so listeners keep receiving messages instead of going silently deaf. It touches:
src/jsc/bindings/bindings.cpp+src/jsc/JSMap.rs— newJSC__JSMap__keysbinding (JSMapIterator → JSArray) so Rust can enumerate the callback map's channels.src/runtime/valkey_jsc/js_valkey.rs— newresubscribe()called fromon_valkey_connect(), plusSubscriptionCtx::subscribed_channels()andresubscribe_pendinginit sites.src/runtime/valkey_jsc/valkey.rs— newresubscribe_pending: u32field;handle_response()now skips promise-pair consumption for the replay'sPush(Subscribe)confirmations and for aRESPValue::Errorreply while the counter is nonzero;on_open()resets it.test/js/valkey/reliability/subscriber-reconnect.test.ts— 6 tests against a scripted RESP3 server (no Docker), asserting the exact command sequence on the reconnected socket, the SELECT/SUBSCRIBE ordering, the -NOPERM error path, and two negative contracts (never-subscribed / lazy-connect dedup).docs/runtime/redis.mdx— one bullet noting the resubscribe behavior and thatpsubscribepatterns are not replayed.
Prior review status
I left two inline comments on earlier revisions; both threads are now resolved:
- valkey.rs:1129 — the replay's error reply eating an unrelated command's promise pair. Fixed in 4961856 with the
resubscribe_pending > 0 && matches!(value, Error(_))guard and a dedicated test that hangs without it. - js_valkey.rs:1331 — the
!is_subscriber()early-return skipping the drop-before-first-confirmation window. Author kept the guard with a well-reasoned rebuttal (that window'sawait subscribe()is rejected, not silent; dropping the clause double-sends on lazy connect) and added a test in 45ff3b0 that fails if the clause is removed.
CodeRabbit's four comments (docs note on psubscribe, resubscribe ordering vs. connect promise, describe.concurrent, unsubscribe-before-close in test cleanup) are all marked addressed in d044c4e. This bug-hunt pass found nothing.
Security risks
None identified. No auth, crypto, permission, or path-handling changes. The new C++ binding iterates a JSMap the client itself created; user code cannot reach it.
Level of scrutiny
Medium-high, human required. This is not a mechanical change: it modifies the RESP reply-slot accounting in handle_response(), which is the exact spot where an off-by-one silently misroutes a promise to the wrong command. The resubscribe_pending counter's correctness depends on wire ordering (replay is written before the offline queue drains, after HELLO/SELECT) and on Redis's whole-command error semantics for SUBSCRIBE. It also adds a new C++ → Rust JSC export. All of that is well-argued and well-tested here, but it is squarely in the "a maintainer who knows this state machine should sign off" category, not auto-approvable.
Other factors
- Test coverage is strong: 6 concurrent tests, 4 of which fail on the unfixed build per the PR body, exact-command-list assertions, and a test that specifically pins the load-bearing
is_subscriberguard. - The PR body explicitly calls out that #32858 will subsume the
resubscribe_pendingcounter — a maintainer may want to weigh sequencing. - The pre-existing subscriber
Error-arm hang (dropspair_maybeunsettled) is acknowledged and deliberately left for #32858 / #33290; that scoping decision seems reasonable but is a maintainer call.
Six of them at once drove six reconnects through six clients, and the
Windows agents blew the client's 10s connection timeout on three of them:
✗ an error reply to the replay ... [10081ms]
Received message: "Connection timeout reached after 10000ms"
✗ replays SUBSCRIBE after SELECT ... [10080ms]
RedisError: Connection has failed
The same file passed on windows x64, x64-baseline and win11-aarch64 before
it was made concurrent. Serial, it runs in 3s.
There was a problem hiding this comment.
Both of my earlier inline concerns were addressed (the error-reply guard in 4961856 and the is_subscriber rationale + pinning test in 45ff3b0), and I found nothing new this pass. Still deferring: this touches the reply-slot accounting in handle_response, adds a new JSC binding, and explicitly interacts with #32858/#33290 — worth a maintainer's eyes on the merge ordering.
Extended reasoning...
Overview
This PR makes Bun.RedisClient re-issue SUBSCRIBE for every channel in the callback map after an auto-reconnect, so subscribers don't go silently deaf. It touches:
src/jsc/bindings/bindings.cpp+src/jsc/JSMap.rs: newJSC__JSMap__keysbinding (JSMapIterator over Keys → JSArray)src/runtime/valkey_jsc/js_valkey.rs:SubscriptionCtx::subscribed_channels(), newresubscribe()called fromon_valkey_connect, field init in two constructorssrc/runtime/valkey_jsc/valkey.rs: newresubscribe_pending: u32field, reply-slot accounting inhandle_response(skip promise-pair consumption for the replay's confirmations and its error reply), reset inon_opentest/js/valkey/reliability/subscriber-reconnect.test.ts: 6 tests against a scripted RESP3 server (no Docker)docs/runtime/redis.mdx: 3-line doc note
Security risks
None identified. No auth/crypto/permission changes; the new C++ binding iterates an internal JSMap the client itself created (keys are channel-name strings), with proper RETURN_IF_EXCEPTION after each fallible JSC call.
Level of scrutiny
Medium-high. The mechanism is straightforward but the correctness rests on subtle invariants: the replayed SUBSCRIBE is written directly into the write buffer with no promise pair, and resubscribe_pending must exactly account for its replies so they don't consume another command's slot. My earlier review found one gap there (error reply eating a slot → hung await), which was fixed with a guard and a regression test. The ordering against SELECT and the offline queue is now pinned by tests. This is protocol-state-machine code where an off-by-one in reply accounting hangs a user's await forever.
Other factors
- All prior review threads (mine and CodeRabbit's) are resolved with code changes and pinning tests.
- The PR description explicitly calls out two adjacent bugs left out of scope because #32858 and #33290 rewrite the same code — a maintainer should sanity-check merge ordering and whether
resubscribe_pendingshould be dropped once #32858 lands. - New C++ JSC binding: small and follows the neighboring pattern, but bindings changes conventionally get a human glance.
- Tests are thorough (6 cases, 4 fail on unfixed build, negative contracts covered), hermetic (in-process RESP3 server), and the file documents why it's serial.
Not approving because this is a non-mechanical change to reply-slot accounting in a stateful protocol client, with cross-PR coupling the author flagged themselves.
Redis always confirms UNSUBSCRIBE, sending a single nil-channel reply when the connection is subscribed to nothing. The scripted server stayed silent in that case, so closeSubscriber()'s `await client.unsubscribe()` hung in the teardown of every test whose reconnect carried no subscriptions. That only bit on a build without the fix, where the replay never happens: the four failing tests timed out after 5s apiece instead of reporting the assertion that actually failed. Now they fail in 0.5s with the real diff.
There was a problem hiding this comment.
My earlier concerns are addressed and I found nothing new, but this changes the reply-slot accounting in the RESP handler and adds a new JSC binding — worth a human pass on the protocol state-machine bits.
Extended reasoning...
Overview
Adds subscription replay after auto-reconnect for Bun.RedisClient. Touches six files: a new JSC__JSMap__keys C++ binding and its Rust wrapper, a resubscribe() method on JSValkeyClient that writes a promise-less SUBSCRIBE directly into the write buffer after the HELLO handshake, a resubscribe_pending counter on ValkeyClient that gates whether an incoming subscribe push (or its error reply) consumes an in-flight promise pair, a docs note, and a 360-line scripted-RESP3-server test file with six cases.
Security risks
None identified. The new binding iterates a JS Map's keys via JSMapIterator with proper RETURN_IF_EXCEPTION after every fallible call. No untrusted-input parsing, no auth/crypto surface.
Level of scrutiny
Medium-high. The core change is small but sits in the RESP reply-dispatch state machine: resubscribe_pending is a new piece of implicit per-connection state that decides whether a reply consumes a queued promise pair. Getting this wrong misattributes replies (my earlier comment caught one such case on the error path, now fixed and pinned by a test). The ordering invariant — replay is written after HELLO/SELECT but before the offline queue drains, and its confirmations are therefore the first replies on the new socket — is load-bearing and only enforced by call-site placement in on_valkey_connect. The PR description explicitly notes overlap with two other open PRs (#32858, #33290) that rewrite adjacent code, so a maintainer should confirm the sequencing.
Other factors
Both of my prior inline comments were addressed: the error-reply-eats-a-slot case was fixed in 4961856 with a regression test, and the is_subscriber() guard was kept with a clear justification plus a new test pinning why removing it would double-send on lazy connect. All CodeRabbit findings are resolved (including reverting describe.concurrent after it flaked on Windows CI). Test coverage is thorough — six cases against a scripted server, four of which fail on the unfixed build. The new C++ binding follows house style. I'm deferring rather than approving because reply-slot accounting in a pipelined protocol handler is exactly the kind of subtle state a maintainer familiar with the valkey subsystem should sign off on, and because of the stated interaction with #32858/#33290.
socket.write is unbuffered: it reports how many bytes it took, 0 when the
send buffer is full and -1 once the socket is closing. The server ignored
that and dropped the remainder, so a reply could vanish.
A freshly accepted socket is not always writable yet on Windows, which lost
the HELLO reply outright. The client then sat in Connecting until its 10s
timeout, and the NOPERM case failed on windows 11 aarch64 every attempt:
✗ an error reply to the replay ... [10073.15ms]
Expected pattern: /NOPERM/
Received message: "Connection timeout reached after 10000ms"
Queue what the socket will not take and flush it again on drain.
windows 11 aarch64 fails this case on every attempt with Expected pattern: /NOPERM/ Received message: "Connection timeout reached after 10000ms" which can only happen if the reconnect never finished its handshake, since the -NOPERM path sets flags.failed and that suppresses the timeout. The other five reconnect cases pass on the same agent, so the error text alone does not say which half broke. Assert the ping outcome, the connected flag and the server's per-connection command log together, so a failure shows whether HELLO, SUBSCRIBE and PING reached the server at all. Also catches the no-replay case immediately instead of waiting for PING to hang.
…t drop The socket-drop tests relied on the auto-reconnect backoff timer firing and the handshake finishing inside the client's 10s connection timeout. On the loaded Windows CI agents that raced, and the subscriber tests flaked (win11 aarch64 most often). close() + await connect() exercises the identical replay path (on_valkey_connect -> resubscribe) and the connect() promise resolves only once HELLO completes, so there is no timer to race. The per-connection wire assertions are unchanged, 4 of 6 still fail on a build without the fix, and the no-longer-needed waitForConnections barrier is removed.
|
Ready for review. The diff is green on every test lane, including all Windows and Linux. The one remaining red check is No test runs on that job when it fails (it dies fetching the build artifact). It is flaky across this branch's builds, independent of the diff: it passed on builds 68842 and 69165 and timed out on 68998, 69227, 69335, and 69625 (after one empty retrigger), with no change to the code in between. The subscriber-reconnect tests themselves pass everywhere now, including win11-aarch64 (8/8 shards green on the latest builds). They earlier flaked on loaded Windows agents; that was fixed by triggering the reconnect with |
What
Bun.RedisClientwithautoReconnectsilently goes deaf after the first dropped connection. The client reconnects and sendsHELLO 3, but it never re-issuesSUBSCRIBEfor the channels its listeners are registered on. The app seesconnected === true, the callbacks stay registered, and no message ever arrives again.oncloseis not fired on a reconnecting drop either, so the app is never told it needs to resubscribe.node-redis, ioredis and go-redis all re-subscribe on reconnect.
Repro
Against a scripted RESP3 server that logs every command per connection:
Cause
on_valkey_connectruns when theHELLOhandshake completes. It caches the hello value, firesonconnect, resolves the connection promise, and then drains the offline queue. Nothing looks at the subscription callback map, even though the map is still populated andis_subscriberstaystrueacross a reconnect. The server, meanwhile, has a brand-new connection with no subscriptions on it.Fix
Replay
SUBSCRIBEfor every channel in the callback map as soon as the handshake completes, before anything queued goes out.Two details make it safe:
HELLO/SELECTdo), so it is the first thing on the wire on the new connection, ahead of whatever the offline queue still holds.resubscribe_pendingcounts off the confirmations it produces instead, so they do not consume a queued command's promise pair.That counter is load-bearing today: removing it makes
SUBSCRIBE news sportsconsume the queuedPING's reply slot, andawait client.ping()resolves with2instead of"PONG". It also covers the error case, where the server refuses the replay (an ACL change during the outage) and replies with a single-ERRand no confirmations at all: that error must not eat a reply slot either, or the command behind it is dropped unsettled and itsawaithangs. (If #32858 lands, its per-pairremaining_repliesaccounting subsumes the counter and it can be dropped.)Pattern subscriptions are untouched:
psubscribeis a raw pass-through that registers no listener, so the client has no pattern set to replay.Also adds
JSC__JSMap__keys(aJSMapIteratoroverIterationKind::Keysinto aJSArray) since the callback map had no key-iteration binding.Verification
test/js/valkey/reliability/subscriber-reconnect.test.tsdrives a scripted RESP3 server, so it runs without Docker. It asserts the exact command list the reconnected connection receives, that the original listener keeps getting messages, and the two negative contracts (unsubscribed channels are not replayed; a client that never subscribed sends noSUBSCRIBE).4 of the 6 fail on the unfixed build. The other two pin negative contracts: a client that never subscribed sends no
SUBSCRIBE, and a lazy-connectsubscribe()(no explicitconnect()) is not replayed on top of the copy the offline queue already holds.Without the fix (
USE_SYSTEM_BUN=1)The existing docker-free valkey tests (
resp-nesting-depth,valkey-gc,valkey-incremental-scan,valkey-tls-verify) still pass: 21 pass, 0 fail.Deliberately out of scope
Two adjacent bugs in the same subsystem are left alone, since fixing them here would mean rewriting code that two other open PRs already rewrite:
Errorarm dropspair_maybewithout settling it, so a user's ownawait client.subscribe(ch)hangs on an error reply. Predates this PR; redis: fix reply-desynchronization bugs that delivered wrong values to wrong commands #32858 rewrites that arm.subscribe()registers the listener before the server confirms, so a drop in that window leaves a stale map entry (and a duplicate callback if the user retries). Called out in the TODO atjs_valkey_functions.rs:1678; redis: register the subscribe() listener only after the server confirms the subscription #33290 fixes the root.If #32858 lands first, its per-pair
remaining_repliesaccounting subsumesresubscribe_pendingand the counter can be deleted.