daemon/libnetwork/networkdb: compare Lamport times when invalidating queued broadcasts - #53479
Merged
Merged
Conversation
tableEventMessage.Invalidates matched on (table, network, key) alone, so whichever event for a key reached the broadcast queue last replaced the one already there, regardless of which described the newer state. Nothing keeps the queue order and the Lamport order together. Every path which queues a table event does so after releasing the lock it mutated the entry under: CreateEntry, UpdateEntry and DeleteEntry each Unlock before calling sendTableEvent, and handleTableMessage queues its relay after handleTableEvent has returned. Two writers to one key therefore take the lock in Lamport order and can reach the queue in the opposite order, and the straggler then evicts the fresher event and puts a superseded value on the wire in its place. The node's own state is right either way -- the Lamport check ran under the lock -- so what is affected is what it tells everyone else, until anti-entropy corrects it a sync interval later. Carry the event's Lamport time on the message and refuse to supersede an event fresher than the one being queued. Equal times still collapse, so duplicate relays of one event do not accumulate. Signed-off-by: Cory Snider <csnider@mirantis.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nodeEventMessage invalidated nothing at all, so a peer which flaps filled every other node's broadcast queue with its churn: a join and a leave per cycle, of which only the newest tells a receiver anything, since handleNodeEvent discards any event no fresher than what it already holds. Invalidation could not simply be switched on, because the type was doing two jobs. sendNodeEvent waits to learn that this node's own announcement went out and Finished is how it is told, but memberlist calls Finished on whatever it invalidates -- so an invalidatable own message would release that wait as though the event had been broadcast when it had in fact been dropped. Only relayed messages carried no notify channel, which made the distinction implicit and easy to lose. Split the type instead. ownNodeEventMessage is this node's own join or leave and is a memberlist.UniqueBroadcast, which the queue neither deduplicates nor offers up for invalidation, so the hazard is structural rather than guarded; there are only ever a handful, since sendNodeEvent is called on cluster join, rejoin and leave. relayedNodeEventMessage is a peer's event being passed on, and supersedes an event for the same node which is no fresher than itself. Lamport times, not last-one-wins: handleNodeMessage queues its relay after releasing the lock handleNodeEvent applied the event under, so two handlers can apply in order and reach the queue reversed. That is also why this cannot be a memberlist.NamedBroadcast, whose deduplication is unconditional and would drop the fresher event. Signed-off-by: Cory Snider <csnider@mirantis.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member
I think there were some PRs for this, but we had to check if it's only the test, or an actual issue. |
corhere
requested review from
thaJeztah and
vvoland
and
a lite review from Copilot
August 27, 2026 17:06
There was a problem hiding this comment.
Pull request overview
This PR fixes NetworkDB’s use of memberlist.TransmitLimitedQueue invalidation so that queued broadcasts respect Lamport time ordering, preventing newer state from being displaced by older events when concurrent writers reach the broadcast queue out of order.
Changes:
- Add Lamport time to
tableEventMessagequeue entries and invalidate only when the new event is at least as fresh as the queued one. - Split node broadcasts into
ownNodeEventMessage(markedUniqueBroadcastto avoid unsafe invalidation/earlyFinished()) andrelayedNodeEventMessage(Lamport-aware invalidation for peer churn). - Add queue-level tests that exercise the real
TransmitLimitedQueuebehavior for both table and node events.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| daemon/libnetwork/networkdb/delegate.go | Propagates Lamport time into queued rebroadcast messages and switches node rebroadcast to the relayed-message type. |
| daemon/libnetwork/networkdb/broadcast.go | Implements Lamport-aware invalidation for table/node relays and splits own vs relayed node broadcasts to preserve sendNodeEvent semantics. |
| daemon/libnetwork/networkdb/broadcast_test.go | Adds targeted tests that validate queue behavior (freshest survives, churn collapses, own-node broadcasts aren’t invalidated). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
vvoland
approved these changes
Aug 27, 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.
Summary
memberlist.TransmitLimitedQueueasks a broadcast whether it invalidates one already queued, and both of NetworkDB's answers ignored Lamport time. Nothing keeps queue order and Lamport order together, so "the last one queued wins" is not the same as "the newest one wins".Every path which queues an event does so after releasing the lock it mutated state under:
CreateEntry,UpdateEntryandDeleteEntryeachUnlock()before callingsendTableEvent.handleTableMessageandhandleNodeMessagequeue their relay afterhandleTableEvent/handleNodeEventhas returned.Two writers to one key can therefore take the lock in Lamport order and reach the queue in the opposite order. The node's own state is right either way — the Lamport check ran under the lock — so what is affected is what it tells everyone else.
tableEventMessageMatched on
(table, network, key)alone, so a straggler evicted the fresher event and put a superseded value on the wire in its place, until anti-entropy corrected it a sync interval later. Now carries the event's Lamport time and refuses to supersede anything fresher. Equal times still collapse, so duplicate relays of one event do not accumulate.nodeEventMessageInvalidated nothing at all, so a peer which flaps filled every other node's queue with its churn — a join and a leave per cycle, of which only the newest tells a receiver anything, since
handleNodeEventdiscards any event no fresher than what it already holds.Invalidation could not simply be switched on, because the type was doing two jobs.
sendNodeEventwaits to learn that this node's own announcement went out andFinished()is how it is told, but memberlist callsFinished()on whatever it invalidates — so an invalidatable own message would release that wait as though the event had been broadcast when it had in fact been dropped. Only relayed messages carried no notify channel, which made the distinction implicit and easy to lose.So the type is split:
ownNodeEventMessage— this node's own join or leave, and amemberlist.UniqueBroadcast, which the queue neither deduplicates nor offers up for invalidation. The hazard becomes structural rather than guarded. There are only ever a handful in flight;sendNodeEventis called on cluster join, rejoin and leave.relayedNodeEventMessage— a peer's event being passed on, superseding an event for the same node which is no fresher than itself.memberlist.NamedBroadcastwould have given O(1) deduplication instead of theInvalidatesscan, but its deduplication is unconditionally last-one-wins and would drop the fresher event — the very bug being fixed here.Testing
Each change is pinned by a test that fails without it, and the queue-level tests drive the real
TransmitLimitedQueuerather than callingInvalidatesdirectly:(table, network, key)matchingstaler_must_not_evict_fresher,TestTableEventQueueKeepsFreshest(queue dropped the fresher event: [stale])UniqueBroadcast, with the unchecked type assertion this file's other types useTestOwnNodeEventSurvivesRelayedEventspanics inside the queueTwo notes for reviewers, since the tests do not make either obvious:
TestOwnNodeEventSurvivesRelayedEventspasses if theUniqueBroadcastmarker is dropped but the checked type assertion is kept — the two mechanisms are independent, and either alone prevents the invalidation. Thevar _ memberlist.UniqueBroadcast = (*ownNodeEventMessage)(nil)assertion is what pins the marker.relayedNodeEventMessage.Invalidatesuses a checked type assertion because two message types now sharenDB.nodeBroadcasts. The queue does not in fact offer aUniqueBroadcastup for invalidation, but with the unchecked form the marker becomes load-bearing for memory safety rather than just for correctness.Full package green, plain and under
-race.Release notes (optional)
A picture of a cute animal (not mandatory but encouraged)
🦔
Created with: Claude Code (Opus 5)