@@ -19,6 +19,7 @@ proc new*(
1919 hasher: Opt [XorDHasher ] = NoneHasher ,
2020 maxBuckets: int = DefaultMaxBuckets ,
2121 selfIdPreHashed = false ,
22+ usefulnessGracePeriod: Duration = DefaultUsefulnessGracePeriod ,
2223): T =
2324 doAssert maxBuckets > 0 and maxBuckets <= MaxBucketsLimit ,
2425 " maxBuckets must be in 1 .. " & $ MaxBucketsLimit
@@ -27,6 +28,7 @@ proc new*(
2728 hasher: hasher,
2829 maxBuckets: maxBuckets,
2930 selfIdPreHashed: selfIdPreHashed,
31+ usefulnessGracePeriod: usefulnessGracePeriod,
3032 )
3133
3234proc `$` * (rt: RoutingTable ): string =
@@ -72,18 +74,42 @@ proc oldestPeer*(bucket: Bucket): (NodeEntry, int) =
7274 oldestIdx = i
7375 (oldest, oldestIdx)
7476
75- proc replaceOldest (bucket: var Bucket , newNodeId: Key , replication: int ): bool =
76- if bucket.peers.len < replication:
77+ func isReplaceable * (entry: NodeEntry , gracePeriod: Duration , now: Moment ): bool =
78+ # # A peer is replaceable once it has spent longer than `gracePeriod` in the
79+ # # table without proving useful. A peer that answered a query within the grace
80+ # # period — or that was only just added — is retained.
81+ now - entry.lastUsefulAt.get (entry.addedAt) > gracePeriod
82+
83+ proc replaceableCandidate (bucket: Bucket , gracePeriod: Duration ): Opt [int ] =
84+ # # Least-recently-seen peer that is past the usefulness grace period, i.e. the
85+ # # best candidate to evict. `Opt.none` when every peer is still useful/fresh.
86+ let now = Moment .now ()
87+ var candidateIdx = - 1
88+ var oldestSeen: Moment
89+ for i, p in bucket.peers:
90+ if not p.isReplaceable (gracePeriod, now):
91+ continue
92+ if candidateIdx == - 1 or p.lastSeen < oldestSeen:
93+ candidateIdx = i
94+ oldestSeen = p.lastSeen
95+ if candidateIdx == - 1 :
96+ return Opt .none (int )
97+ Opt .some (candidateIdx)
98+
99+ proc replaceReplaceable (
100+ bucket: var Bucket , newNodeId: Key , config: RoutingTableConfig
101+ ): bool =
102+ if bucket.peers.len < config.replication:
77103 trace " Skipping replace: bucket is not full" , newNodeId = newNodeId
78104 return false
79105
80- let (oldest, oldestIdx) = bucket.oldestPeer ()
81-
82- if oldest.nodeId == newNodeId:
83- trace " Failed to replace: same nodeId" , newNodeId = newNodeId
106+ let idx = bucket.replaceableCandidate (config.usefulnessGracePeriod).valueOr:
107+ trace " Skipping replace: no peer past usefulness grace period" ,
108+ newNodeId = newNodeId
84109 return false
85110
86- bucket.peers[oldestIdx] = NodeEntry (nodeId: newNodeId, lastSeen: Moment .now ())
111+ let now = Moment .now ()
112+ bucket.peers[idx] = NodeEntry (nodeId: newNodeId, lastSeen: now, addedAt: now)
87113 true
88114
89115proc updateRoutingTableMetrics * (rtable: RoutingTable ) =
@@ -113,12 +139,14 @@ proc insert*(rtable: RoutingTable, nodeId: Key): bool =
113139 if keyx.isSome:
114140 bucket.peers[keyx.unsafeValue].lastSeen = Moment .now ()
115141 elif bucket.peers.len < rtable.config.replication:
116- bucket.peers.add (NodeEntry (nodeId: nodeId, lastSeen: Moment .now ()))
142+ let now = Moment .now ()
143+ bucket.peers.add (NodeEntry (nodeId: nodeId, lastSeen: now, addedAt: now))
117144 kad_routing_table_insertions.inc ()
118145 else :
119- # eviction policy: replace oldest key
120- if not bucket.replaceOldest (nodeId, rtable.config.replication):
121- debug " Cannot insert, failed to replace oldest key in bucket" ,
146+ # eviction policy: replace a peer past the usefulness grace period, keeping
147+ # peers that recently answered a query.
148+ if not bucket.replaceReplaceable (nodeId, rtable.config):
149+ debug " Cannot insert, no replaceable peer in bucket" ,
122150 bucket = idx, nodeId = nodeId
123151 return false
124152 kad_routing_table_replacements.inc ()
@@ -130,6 +158,24 @@ proc insert*(rtable: RoutingTable, nodeId: Key): bool =
130158proc insert * (rtable: RoutingTable , peerId: PeerId ): bool =
131159 insert (rtable, peerId.toKey ())
132160
161+ proc markUseful * (rtable: RoutingTable , nodeId: Key ) =
162+ # # Records that `nodeId` answered a query: refreshes its usefulness and
163+ # # last-seen timestamps so it is retained through bucket eviction. No-op when
164+ # # the peer is not in the table.
165+ let idx = rtable.bucketIndex (nodeId)
166+ if idx >= rtable.buckets.len:
167+ return
168+ var bucket = rtable.buckets[idx]
169+ let pos = peerIndexInBucket (bucket, nodeId).valueOr:
170+ return
171+ let now = Moment .now ()
172+ bucket.peers[pos].lastUsefulAt = Opt .some (now)
173+ bucket.peers[pos].lastSeen = now
174+ rtable.buckets[idx] = bucket
175+
176+ proc markUseful * (rtable: RoutingTable , peerId: PeerId ) =
177+ rtable.markUseful (peerId.toKey ())
178+
133179proc findClosest * (rtable: RoutingTable , targetId: Key , count: int ): seq [Key ] =
134180 # # Returns up to `count` nodes in the table with the smallest XOR distance to `targetId`.
135181 var allNodes: seq [Key ] = @ []
0 commit comments