fix(replication): stop connecting to removed nodes after remove_node#220
Merged
Conversation
Three related bugs caused replication to keep trying to connect to a
removed node's hostname indefinitely after remove_node:
1. NodeReplicationConnection.connect() did not check
intentionallyUnsubscribed at entry. If a retry timer fired after
unsubscribe() was called, connect() would open a new socket to the
removed node, reset the close handler's intentional flag, and loop
again. Now connect() returns immediately when
intentionallyUnsubscribed is true.
2. The once('finished') cleanup in getSubscriptionConnection deleted
dbConnections[dbName] unconditionally. When a node is removed and
re-added (same URL), the old connection's deferred 'finished' event
would delete the new connection's entry, making future
unsubscribeFromNode() calls unable to find and stop it. Fixed with
an identity check before deleting.
3. onNodeUpdate's delete path never called nodeMap.delete(hostname), so
removed nodes accumulated in nodeMap indefinitely.
Also added socket null-safety in unsubscribe() (?.close instead of .close)
for the edge case where unsubscribe() is called before connect() runs.
Adds integration test: kill peer → remove_node → restart peer → verify
node1 does NOT reconnect, directly exercising the retry-loop halt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
|
Reviewed; no blockers found. |
cb1kenobi
approved these changes
May 22, 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.
Fixes #189.
Changes
Three related bugs allowed replication to keep retrying connections to a removed node's hostname indefinitely.
connect()missing early-return guard (replicationConnection.ts):connect()did not checkintentionallyUnsubscribedat entry. If a retry timer was already scheduled whenunsubscribe()ran, the timer would fire, open a new socket, and the node kept connecting. Nowconnect()returns immediately whenintentionallyUnsubscribedis true.once('finished')race ingetSubscriptionConnection(replicator.ts): The cleanup handler unconditionally deleteddbConnections[dbName]. When a node is removed and re-added with the same URL, the old connection's deferred'finished'event deleted the new connection's entry — making futureunsubscribeFromNode()calls unable to find and stop it. Fixed with an identity check (dbConnections.get(dbName) === connection) before deleting.nodeMapnot cleaned up on delete (subscriptionManager.ts):onNodeUpdate's delete path never callednodeMap.delete(hostname), so removed nodes accumulated innodeMapindefinitely.Also added null-safety to
unsubscribe()(?.closeinstead of.close) in case it's called before the firstconnect().Test
New test in
replicationReconnect.test.mjs: kill peer →remove_node→ restart peer → verify node1 does not reconnect. Without theconnect()guard, node1 would reconnect once node0 is reachable again; with the fix the retry loop is permanently halted.🤖 Generated by Claude Sonnet 4.6 on behalf of Kris.