Skip to content

feat: gossip improvements - #5520

Open
sbackend123 wants to merge 15 commits into
masterfrom
feat/gossip-improvements
Open

feat: gossip improvements#5520
sbackend123 wants to merge 15 commits into
masterfrom
feat/gossip-improvements

Conversation

@sbackend123

@sbackend123 sbackend123 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • I have read the coding guide.
  • My change requires a documentation update, and I have done it.
  • I have added tests to cover my changes.
  • I have filled out the description and linked the related issues.

Description

Adds write coalescing for hive outbound gossip. Single-peer BroadcastPeers calls are buffered per addressee and flushed as one batched message after ~1s (configurable via GossipCoalesceInterval), or immediately when the buffer reaches maxBatchSize (30). Calls with 2+ peers are sent without coalescing

Open API Spec Version Changes (if applicable)

Motivation and Context (Optional)

Related Issue (Optional)

#5490

Screenshots (if appropriate):

AI Disclosure

  • This PR contains code that has been generated by an LLM.
  • I have reviewed the AI generated code thoroughly.
  • I possess the technical expertise to responsibly review the code generated in this PR.

sbackend123 and others added 3 commits June 29, 2026 11:48
Discard buffered gossip on shutdown instead of flushing, restore quit
checks in broadcastNow, document the async BroadcastPeers contract, use
fixed 100ms jitter, and clean up tests and metrics.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sbackend123
sbackend123 marked this pull request as ready for review June 29, 2026 12:43
Comment thread pkg/hive/hive.go Outdated
Comment thread pkg/hive/gossip_buffer.go Outdated
Comment thread pkg/hive/gossip_buffer.go Outdated
Comment thread pkg/hive/gossip_buffer.go Outdated
type pendingGossip struct {
addressee swarm.Address
peers map[string]swarm.Address // peer bytestring -> address (set semantics)
deadline time.Time

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder whether there's a benefit of keeping a deadline per peer. usually, write coalescing is simple enough:

  • have an interval fire at a constant rate
  • if new records arrived by interval fire
  • when new entries arrived, optionally, postpone the sending the sending until the next interval firing (and so also you could extend up to a set upper bound, so that entries don't keep collecting forever but also guarantee that information goes out still relatively quickly)
  • send all the pending sends

also: usually, when a peer arrives - we gossip that peer to all peers (full nodes) and gossip to that peer all of our connected peers.
this in turn means that sending is almost always involving all connected peers. which in turn also means that the timestamps on the individual pendingGossip entries would be almost identical (making the field even more so redundant)

@sbackend123
sbackend123 requested a review from acud July 16, 2026 11:13

@gacevicljubisa gacevicljubisa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work.

One idea: instead of deciding what to do based on how many peers are passed in, it might be cleaner to add a separate method like GossipPeer(addressee, peer) on discovery.Driver just for the buffered case. Make BroadcastPeers plain => send now and return an error method, and use GossipPeer as async (fire and forget) in kademlia.go:1082-1090 and flush in startGossipCoalescer worker. This allows you to drop coalesceThreshold. Also, the "buffer is full" logic, you can also move to the background worker with a wakeup channel so buffering never blocks or sends directly.

Comment thread pkg/hive/gossip_buffer.go Outdated

key := addressee.ByteString()
peerSet, ok := b.pending[key]
if !ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since maps leak memory by design, it would be better to:

  1. check whether the b.pending key exists
  2. merge its contents with peers if it does before writing the key to the map
  3. check the max batch size and if the entry really exceeds max batch - return early without writing to the map
  4. finally if we are within the bounds of the max batch - write to map

Comment thread pkg/hive/hive.go Outdated
if err != nil {
s.logger.Debug("coalesced gossip flush failed", "addressee", addressee, "reason", reason, "batch_size", len(peers), "error", err)
}
cancel()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this should be a defer call just after it gets created.

Comment thread pkg/hive/hive.go Outdated
select {
case <-ticker.C:
for _, batch := range s.gossipBuf.takeAll() {
s.flushGossipBatch(batch.addressee, batch.peers, coalesceFlushReasonTimer)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - this is a blocking call that makes slower peers to block other peers from getting the information. i would tend to turn this into go s.flushGossipBatch. iirc the latest go compilers make sure the values get copied correctly such that when the iterator changes batch values it doesn't change the underlying value for the goroutines already dispatched with that same variable name. but maybe also putting this into a closure won't hurt too much.

Comment thread pkg/hive/gossip_buffer.go Outdated
)

const (
defaultGossipCoalesceInterval = time.Second

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i think this can be higher (like 5 sec)? the same for the coalesce threshold - we want to have bigger messages and less often. the timer fires every 5 seconds anyway.

Comment thread pkg/hive/gossip_buffer.go Outdated
peers: slices.Collect(maps.Values(peerSet)),
})
}
b.pending = make(map[string]map[string]swarm.Address)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clear(b.pending)

@sbackend123

Copy link
Copy Markdown
Contributor Author

Nice work.

One idea: instead of deciding what to do based on how many peers are passed in, it might be cleaner to add a separate method like GossipPeer(addressee, peer) on discovery.Driver just for the buffered case. Make BroadcastPeers plain => send now and return an error method, and use GossipPeer as async (fire and forget) in kademlia.go:1082-1090 and flush in startGossipCoalescer worker. This allows you to drop coalesceThreshold. Also, the "buffer is full" logic, you can also move to the background worker with a wakeup channel so buffering never blocks or sends directly.

Nice idea, but looks like more changes than we need (?)
@acud wdyt?

@sbackend123
sbackend123 requested a review from acud August 20, 2026 09:50
@acud

acud commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

wdyt

not sure... also having a buffered vs unbuffered kinda defeats the purpose of batching the writes together. not sure if i see the case of non-buffered broadcast as needed. the thresholds should take care of that already - when the group is big enough - it is urgent enough (and that would always be the case for a peer that connects and gets a bunch of peers via gossip). so we can use the implicit behavior in this case instead of expanding the interfaces. my 2 cents

@acud acud left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Comment thread pkg/hive/hive.go
s.metrics.BroadcastPeersPeers.Add(float64(len(peers)))

// Already-batched messages go out immediately; single-peer gossips are coalesced.
if len(peers) >= coalesceThreshold {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the addressee already has some peers which are queued for sending through the buffer - they are silently skipped here. not a big issue, can be handled later too. flagging this nevertheless.

also, you might want to gossip, but the rate limiter won't allow you to send the whole batch together because you can't get enough tokens from the bucket. this puts things as a best effort. i'm not sure we should handle all those edge cases right away but they are definitely worth documenting at least inline and perhaps as a follow up issue.

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.

3 participants