11# SPDX-License-Identifier: Apache-2.0 OR MIT
22# Copyright (c) Status Research & Development GmbH
33
4- import std/ [tables, sequtils, algorithm, net]
4+ import std/ [tables, sequtils, algorithm, net, sets ]
55import chronos, chronicles, results
66import ../../ [peerid, peerinfo, switch, multihash, peeraddrpolicy, wire]
77import ../ protocol
@@ -21,7 +21,6 @@ type LookupState* = ref object
2121 shortlist* : Table [PeerId , XorDistance ]
2222 responded* : Table [PeerId , RespondedStatus ]
2323 attempts* : Table [PeerId , int ]
24- inflight* : Table [PeerId , seq [FutureBase ]]
2524
2625type DispatchProc * = proc (
2726 kad: KadDHT , peer: PeerId , target: Key
@@ -366,77 +365,88 @@ proc findNodeDispatch*(
366365): Future [Result [Message , string ]] {.async : (raises: [CancelledError ]), gcsafe .} =
367366 return await dispatchFindNode (kad, peer, target)
368367
369- proc lookOnce * (
370- kad: KadDHT ,
371- state: LookupState ,
372- rtable: RoutingTable ,
373- dispatch: DispatchProc ,
374- onReply: ReplyHandler ,
375- ): Future [bool ] {.async : (raises: [CancelledError ]).} =
376- # # Run a single round of the iterative lookup against ``state.target``.
377- # # Returns ``false`` when no further peers can be queried, signalling the
378- # # caller to stop driving the lookup.
379- let toQuery = state.selectCloserPeers (kad.config.alpha)
380- if toQuery.len () == 0 :
381- return false
382-
383- for peerId in toQuery:
384- state.attempts[peerId] = state.attempts.getOrDefault (peerId, 0 ) + 1
385-
386- debug " Lookup queries" , peersToQuery = toQuery.mapIt (it.shortLog ())
387-
368+ type DispatchOutcome = enum
369+ Completed
370+ Errored
371+
372+ type DispatchResult = object
373+ peer: PeerId
374+ outcome: DispatchOutcome
375+ msg: Message
376+
377+ type RpcFuture = Future [DispatchResult ].Raising ([CancelledError ])
378+
379+ type Attempt = object
380+ peer: PeerId
381+ fut: RpcFuture
382+ deadline: Moment
383+ abandoned: bool
384+ # # its ``timeout`` elapsed; the slot is freed but the RPC
385+ # # keeps running so it can still deliver, and its late result is ignored.
386+
387+ proc runDispatch (
388+ kad: KadDHT , peerId: PeerId , target: Key , dispatch: DispatchProc
389+ ): Future [DispatchResult ] {.async : (raises: [CancelledError ]).} =
390+ let res = await dispatch (kad, peerId, target)
391+ if res.isErr ():
392+ error " Kad lookup: RPC error" , peer = peerId.shortLog (), msg = res.error ()
393+ return DispatchResult (peer: peerId, outcome: Errored )
394+ return DispatchResult (peer: peerId, outcome: Completed , msg: res.value ())
395+
396+ func activePeers (pending: seq [Attempt ]): HashSet [PeerId ] {.raises : [].} =
397+ var peers = initHashSet [PeerId ]()
398+ for a in pending:
399+ if not a.abandoned:
400+ peers.incl (a.peer)
401+ peers
402+
403+ proc refill (
404+ kad: KadDHT , state: LookupState , pending: var seq [Attempt ], dispatch: DispatchProc
405+ ) {.raises : [].} =
406+ # # Keep up to ``alpha`` RPCs in flight by dispatching the next-closest
407+ # # not-yet-active peers into any free slots.
408+ var active = pending.activePeers ()
388409 let target = state.target
389- let dispatchWithPeer = proc (
390- peerId: PeerId
391- ): Future [(PeerId , Result [Message , string ])] {.
392- async : (raises: [CancelledError ]), gcsafe
393- .} =
394- let msg = await dispatch (kad, peerId, target)
395- return (peerId, msg)
396-
397- let rpcBatch = toQuery.mapIt (dispatchWithPeer (it))
398- for (fut, peerId) in zip (rpcBatch, toQuery):
399- state.inflight.mgetOrPut (peerId, @ []).add (FutureBase (fut))
400- let completedRPCBatch = await rpcBatch.collectCompleted (kad.config.timeout)
401-
402- for (fut, peerId) in zip (rpcBatch, toQuery):
403- if not fut.finished () or fut.cancelled ():
404- continue
405- if fut.failed ():
406- state.responded[peerId] = RespondedStatus .Failed
407- else :
408- let (_, res) = fut.value ()
409- if res.isErr ():
410- state.responded[peerId] = RespondedStatus .Failed
411- error " Kad lookup: RPC error" , peer = peerId.shortLog (), msg = res.error ()
412- else :
413- state.responded[peerId] = RespondedStatus .Success
414-
415- var toCancel: seq [FutureBase ]
416- for (peerId, res) in completedRPCBatch:
417- let reply = res.valueOr:
410+ for (peerId, _) in state.sortedShortlist ():
411+ if active.len >= kad.config.alpha:
412+ break
413+ if peerId in active:
418414 continue
419- let newPeerInfos = state.updateShortlist (reply)
420- kad.switch.updatePeers (
421- kad.config.addressPolicy, rtable, newPeerInfos, kad.config.limits.maxPeersPerIp,
422- kad.config.limits.maxPeersPerIpv4Subnet, kad.config.limits.maxPeersPerIpv6Subnet,
415+ state.attempts[peerId] = state.attempts.getOrDefault (peerId, 0 ) + 1
416+ debug " Lookup query" , peer = peerId.shortLog ()
417+ pending.add (
418+ Attempt (
419+ peer: peerId,
420+ fut: kad.runDispatch (peerId, target, dispatch),
421+ deadline: Moment .now () + kad.config.timeout,
422+ abandoned: false ,
423+ )
423424 )
424- await onReply (peerId, Opt .some (reply), state)
425-
426- # Evicted peers are no longer eligible for retries, so cancel any abandoned RPCs.
427- for peerId in state.inflight.keys.toSeq:
428- if not state.shortlist.hasKey (peerId):
429- toCancel.add (state.inflight.getOrDefault (peerId).filterIt (not it.finished ()))
430- state.inflight.del (peerId)
431-
432- for peerId in toQuery:
433- if state.responded.hasKey (peerId) or
434- state.attempts.getOrDefault (peerId, 0 ) > kad.config.retries:
435- toCancel.add (state.inflight.getOrDefault (peerId).filterIt (not it.finished ()))
436- state.inflight.del (peerId)
437- await toCancel.cancelAndWait ()
425+ active.incl (peerId)
426+
427+ proc awaitProgress (pending: seq [Attempt ]) {.async : (raises: [CancelledError ]).} =
428+ # # Wake as soon as any in-flight RPC finishes or the earliest active slot's
429+ # # ``timeout`` elapses, whichever comes first.
430+ var earliest = Opt .none (Moment )
431+ for a in pending:
432+ if not a.abandoned and (earliest.isNone or a.deadline < earliest.get ()):
433+ earliest = Opt .some (a.deadline)
434+
435+ let timer = sleepAsync (
436+ if earliest.isSome:
437+ max (earliest.get () - Moment .now (), ZeroDuration )
438+ else :
439+ InfiniteDuration
440+ )
441+ defer :
442+ timer.cancelSoon ()
438443
439- return true
444+ var futs = pending.mapIt (FutureBase (it.fut))
445+ futs.add (FutureBase (timer))
446+ try :
447+ discard await race (futs)
448+ except ValueError :
449+ raiseAssert " race() cannot raise ValueError on a non-empty future list"
440450
441451proc iterativeLookup * (
442452 kad: KadDHT ,
@@ -446,17 +456,71 @@ proc iterativeLookup*(
446456 onReply: ReplyHandler ,
447457 stopCond: StopCond ,
448458): Future [LookupState ] {.async : (raises: [CancelledError ]).} =
459+ # # Drive an iterative lookup as a continuous ``alpha``-concurrency pipeline:
460+ # # keep up to ``alpha`` RPCs in flight and re-dispatch the next-closest
461+ # # unqueried peer as soon as any slot frees, so wall-clock tracks throughput
462+ # # rather than the slowest peer in a synchronized round. A slow peer only
463+ # # stalls its own slot for ``timeout`` before it is abandoned and retried.
449464 let state = LookupState .init (kad, target)
465+ var pending: seq [Attempt ]
450466
451- while not stopCond (state):
452- if not await kad.lookOnce (state, rtable, dispatch, onReply):
453- break
467+ defer :
468+ await pending.mapIt (it.fut).cancelAndWait ()
469+
470+ while true :
471+ # Resolve finished RPCs; time out overdue ones by freeing their slot while
472+ # leaving the RPC running (its late reply is ignored).
473+ let now = Moment .now ()
474+ var completed: seq [DispatchResult ]
475+ var stillPending: seq [Attempt ]
476+ for a in pending:
477+ if a.fut.finished ():
478+ if not a.abandoned and not a.fut.cancelled ():
479+ completed.add (a.fut.value ())
480+ continue
481+ if not a.abandoned and now >= a.deadline:
482+ stillPending.add (
483+ Attempt (peer: a.peer, fut: a.fut, deadline: a.deadline, abandoned: true )
484+ )
485+ else :
486+ stillPending.add (a)
487+ pending = stillPending
488+
489+ var toCancel: seq [RpcFuture ]
490+ for res in completed:
491+ case res.outcome
492+ of Errored :
493+ state.responded[res.peer] = RespondedStatus .Failed
494+ of Completed :
495+ state.responded[res.peer] = RespondedStatus .Success
496+ let newPeerInfos = state.updateShortlist (res.msg)
497+ kad.switch.updatePeers (
498+ kad.config.addressPolicy, rtable, newPeerInfos,
499+ kad.config.limits.maxPeersPerIp, kad.config.limits.maxPeersPerIpv4Subnet,
500+ kad.config.limits.maxPeersPerIpv6Subnet,
501+ )
502+ await onReply (res.peer, Opt .some (res.msg), state)
503+
504+ # A responded peer needs no duplicate retries, and a newly discovered closer
505+ # peer can evict an in-flight target from the shortlist; cancel both.
506+ var keep: seq [Attempt ]
507+ for a in pending:
508+ if state.responded.hasKey (a.peer) or not state.shortlist.hasKey (a.peer):
509+ toCancel.add (a.fut)
510+ else :
511+ keep.add (a)
512+ pending = keep
513+ await toCancel.cancelAndWait ()
514+
515+ # Once the stop condition holds, dispatch no new peers but keep draining the
516+ # replies already in flight, mirroring the old round that finished its batch
517+ # before terminating.
518+ if not stopCond (state):
519+ kad.refill (state, pending, dispatch)
454520
455- var leftover: seq [FutureBase ]
456- for futs in state.inflight.values:
457- leftover.add (futs.filterIt (not it.finished ()))
458- if leftover.len > 0 :
459- await leftover.cancelAndWait ()
521+ if pending.activePeers ().len == 0 :
522+ break
523+ await awaitProgress (pending)
460524
461525 state
462526
0 commit comments