feat(addressbook): prune stale entries by last-seen time#5513
feat(addressbook): prune stale entries by last-seen time#5513martinconic wants to merge 3 commits into
Conversation
155323e to
1716cdb
Compare
|
Here, prune here runs once, at startup only, and it can lead that if we have some stable connections over bigger period of time, where we haven't updated LastSeen, it will remove those on next restart. But those are our most valuable connections. Maybe we could use One gap is left, what do to with the peer we only hear over hive gossip, but we didn't connect to? |
| now := time.Now().Unix() | ||
|
|
||
| for _, e := range batch { | ||
| var fields map[string]json.RawMessage |
There was a problem hiding this comment.
this is not needed. you can shadow the type which is currently on master as a local type scoped to the function, and use it directly to unmarshal and conversion onto the new type. when i think of it, you could even have the latest version (just annotate the last seen field with the json omitempty instruction), and you could unmarshal the existing old serialization format into it, just set the timestamp and write it back into the db.
| k.connectedPeers.Add(peer.addr) | ||
|
|
||
| if err := k.addressBook.UpdateLastSeen(peer.addr); err != nil { | ||
| k.logger.Debug("could not update last seen for peer", "peer_address", peer.addr, "error", err) |
| k.connectedPeers.Add(addr) | ||
| k.waitNext.Remove(addr) | ||
| if err := k.addressBook.UpdateLastSeen(addr); err != nil { | ||
| k.logger.Debug("could not update last seen for peer", "peer_address", addr, "error", err) |
This is what we've converged to with @janos as a naive starting point to ship.
For now let's leave them in. The first line of defense is to get rid of garbage which is in the statestore. A peer which is seen over hive and is not connected to is still a potential peer you'd like to connect to. I agree that over time yes, probably you don't wanna keep those. I suggest to ship this first and see it works, then evolve into more well defined scenarios. |
|
Added two fixes:
|
| // Hive reports the same peer on every gossip round, while pruning operates on | ||
| // a scale of weeks, so a write is skipped when the stored value is already | ||
| // this fresh. | ||
| const lastSeenUpdateInterval = 24 * time.Hour |
There was a problem hiding this comment.
don't understand whether this is truly needed. if i see a peer overlay at time.Now(), i set the time last seen to now(). why do we need to add all of this buffering and logic complications? unless there's really an issue with IO resulting from so many writes (i suspect not) then this is a non-issue.
| GetPutter | ||
| Remover | ||
| // UpdateLastSeen marks the overlay as seen at the current time. | ||
| UpdateLastSeen(overlay swarm.Address) error |
|
|
||
| // mu serializes the read-modify-write in UpdateLastSeen against Put, so a | ||
| // concurrent Put is not rolled back by a stale copy of the entry. | ||
| mu sync.Mutex |
There was a problem hiding this comment.
i don't see why this is needed. this is not business critical information and i think it is fine with this hypothetical use case happening every now and then, if ever.
| // Entries without a recorded last-seen time (LastSeen == 0) are kept, leaving | ||
| // pruning to a later run once they have been observed. | ||
| func (s *store) Prune(before time.Time) error { | ||
| cutoff := before.Unix() |
There was a problem hiding this comment.
you added a lock but you don't use it here. i suggest to remove the lock because the edge case it tries to handle is very hypothetical and inconsequential
| // look stale to the pruner. Both errors imply a known peer, since | ||
| // an unknown one short-circuits inside CheckTimestamp. | ||
| if errors.Is(err, bzz.ErrTimestampStale) || errors.Is(err, bzz.ErrTimestampTooSoon) { | ||
| if err := s.addressBook.UpdateLastSeen(overlayAddr); err != nil { |
There was a problem hiding this comment.
i suggest to update the timestamp outside this error check block, because you anyway update the seen time even on a rejected timestamp here, then why not do it anyway before the check timestamp?
| // TestGossipDoesNotRefreshLastSeenOnInvalidRecord makes sure the refresh is | ||
| // scoped to records that merely are not newer. A record rejected for any other | ||
| // reason is not evidence that the peer is alive. | ||
| func TestGossipDoesNotRefreshLastSeenOnInvalidRecord(t *testing.T) { |
There was a problem hiding this comment.
it would be good to avoid WritingWholeSentencesInTestCaseNames. that's what we have comments for :)
|
|
||
| // Prune addressbook entries whose overlays have not been seen recently, so | ||
| // the address book does not accumulate stale peers indefinitely. | ||
| if err := addressbook.Prune(time.Now().Add(-addressbookPruneAfter)); err != nil { |
There was a problem hiding this comment.
since you prune only on startup - there's no need to expose the method over an interface - just make sure that addressbook ctor runs the pruning internally.
|
|
||
| k.connectedPeers.Add(peer.addr) | ||
|
|
||
| if err := k.addressBook.UpdateLastSeen(peer.addr); err != nil { |
There was a problem hiding this comment.
the changes in kademlia only handle updating on connection event, however this is only partially true. since peers we are connected to are "constantly seen". i suggest to have one update location inside the kademlia's manage loop that fires every now and then, and just iterate over the whole list of connected peers and mark them as seen. in that same breath, it might be useful to make Seen to be variadic, like Seen(...peer)
Checklist
Description
Implements address book pruning (#5491).
The address book recently gained a wrapping verifiedAddress{Address, Verified} struct (v2.8.0, #5477). This PR extends it with a last-seen timestamp and uses it to drop peers we have not seen for over a month, preventing the address book from accumulating stale, unreachable peers indefinitely.
Changes
AI Disclosure