@libp2p/kad-dht 13.1.1 → 13.1.2-27b2fa6b6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/index.min.js +2 -2
  2. package/dist/src/index.d.ts +28 -6
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/kad-dht.d.ts.map +1 -1
  6. package/dist/src/kad-dht.js +25 -11
  7. package/dist/src/kad-dht.js.map +1 -1
  8. package/dist/src/network.d.ts +2 -1
  9. package/dist/src/network.d.ts.map +1 -1
  10. package/dist/src/network.js +10 -3
  11. package/dist/src/network.js.map +1 -1
  12. package/dist/src/peer-distance-list.d.ts.map +1 -0
  13. package/dist/src/{peer-list/peer-distance-list.js → peer-distance-list.js} +1 -1
  14. package/dist/src/peer-distance-list.js.map +1 -0
  15. package/dist/src/peer-routing/index.js +1 -1
  16. package/dist/src/peer-routing/index.js.map +1 -1
  17. package/dist/src/query/manager.d.ts +1 -2
  18. package/dist/src/query/manager.d.ts.map +1 -1
  19. package/dist/src/query/manager.js +11 -16
  20. package/dist/src/query/manager.js.map +1 -1
  21. package/dist/src/query-self.d.ts.map +1 -1
  22. package/dist/src/query-self.js +13 -6
  23. package/dist/src/query-self.js.map +1 -1
  24. package/dist/src/routing-table/closest-peers.d.ts +43 -0
  25. package/dist/src/routing-table/closest-peers.d.ts.map +1 -0
  26. package/dist/src/routing-table/closest-peers.js +86 -0
  27. package/dist/src/routing-table/closest-peers.js.map +1 -0
  28. package/dist/src/routing-table/index.d.ts +55 -32
  29. package/dist/src/routing-table/index.d.ts.map +1 -1
  30. package/dist/src/routing-table/index.js +261 -158
  31. package/dist/src/routing-table/index.js.map +1 -1
  32. package/dist/src/routing-table/k-bucket.d.ts +65 -21
  33. package/dist/src/routing-table/k-bucket.d.ts.map +1 -1
  34. package/dist/src/routing-table/k-bucket.js +122 -66
  35. package/dist/src/routing-table/k-bucket.js.map +1 -1
  36. package/dist/src/routing-table/refresh.d.ts.map +1 -1
  37. package/dist/src/routing-table/refresh.js +4 -1
  38. package/dist/src/routing-table/refresh.js.map +1 -1
  39. package/dist/src/rpc/index.d.ts.map +1 -1
  40. package/dist/src/rpc/index.js +3 -7
  41. package/dist/src/rpc/index.js.map +1 -1
  42. package/package.json +11 -12
  43. package/src/index.ts +32 -6
  44. package/src/kad-dht.ts +29 -13
  45. package/src/network.ts +18 -5
  46. package/src/{peer-list/peer-distance-list.ts → peer-distance-list.ts} +1 -1
  47. package/src/peer-routing/index.ts +1 -1
  48. package/src/query/manager.ts +16 -21
  49. package/src/query-self.ts +14 -6
  50. package/src/routing-table/closest-peers.ts +113 -0
  51. package/src/routing-table/index.ts +302 -189
  52. package/src/routing-table/k-bucket.ts +194 -81
  53. package/src/routing-table/refresh.ts +5 -1
  54. package/src/rpc/index.ts +4 -7
  55. package/dist/src/peer-list/index.d.ts +0 -29
  56. package/dist/src/peer-list/index.d.ts.map +0 -1
  57. package/dist/src/peer-list/index.js +0 -45
  58. package/dist/src/peer-list/index.js.map +0 -1
  59. package/dist/src/peer-list/peer-distance-list.d.ts.map +0 -1
  60. package/dist/src/peer-list/peer-distance-list.js.map +0 -1
  61. package/dist/typedoc-urls.json +0 -56
  62. package/src/peer-list/index.ts +0 -54
  63. /package/dist/src/{peer-list/peer-distance-list.d.ts → peer-distance-list.d.ts} +0 -0
@@ -1,19 +1,32 @@
1
- import { InvalidMessageError, TypedEventEmitter } from '@libp2p/interface'
2
- import { PeerSet } from '@libp2p/peer-collections'
1
+ import { TypedEventEmitter, setMaxListeners, start, stop } from '@libp2p/interface'
2
+ import { AdaptiveTimeout } from '@libp2p/utils/adaptive-timeout'
3
3
  import { PeerQueue } from '@libp2p/utils/peer-queue'
4
- import { pbStream } from 'it-protobuf-stream'
5
- import { Message, MessageType } from '../message/dht.js'
4
+ import { anySignal } from 'any-signal'
5
+ import parallel from 'it-parallel'
6
+ import { EventTypes } from '../index.js'
7
+ import { MessageType } from '../message/dht.js'
6
8
  import * as utils from '../utils.js'
7
- import { KBucket, isLeafBucket, type Bucket, type PingEventDetails } from './k-bucket.js'
8
- import type { ComponentLogger, Logger, Metric, Metrics, PeerId, PeerStore, Startable, Stream } from '@libp2p/interface'
9
- import type { ConnectionManager } from '@libp2p/interface-internal'
9
+ import { ClosestPeers } from './closest-peers.js'
10
+ import { KBucket, isLeafBucket } from './k-bucket.js'
11
+ import type { Bucket, LeafBucket, Peer } from './k-bucket.js'
12
+ import type { Network } from '../network.js'
13
+ import type { AbortOptions, ComponentLogger, CounterGroup, Logger, Metric, Metrics, PeerId, PeerStore, Startable, Stream } from '@libp2p/interface'
14
+ import type { AdaptiveTimeoutInit } from '@libp2p/utils/adaptive-timeout'
10
15
 
11
- export const KAD_CLOSE_TAG_NAME = 'kad-close'
12
- export const KAD_CLOSE_TAG_VALUE = 50
13
16
  export const KBUCKET_SIZE = 20
14
- export const PREFIX_LENGTH = 32
15
- export const PING_TIMEOUT = 10000
16
- export const PING_CONCURRENCY = 10
17
+ export const PREFIX_LENGTH = 8
18
+ export const PING_NEW_CONTACT_TIMEOUT = 2000
19
+ export const PING_NEW_CONTACT_CONCURRENCY = 20
20
+ export const PING_NEW_CONTACT_MAX_QUEUE_SIZE = 100
21
+ export const PING_OLD_CONTACT_COUNT = 3
22
+ export const PING_OLD_CONTACT_TIMEOUT = 2000
23
+ export const PING_OLD_CONTACT_CONCURRENCY = 20
24
+ export const PING_OLD_CONTACT_MAX_QUEUE_SIZE = 100
25
+ export const KAD_PEER_TAG_NAME = 'kad-peer'
26
+ export const KAD_PEER_TAG_VALUE = 1
27
+ export const LAST_PING_THRESHOLD = 600000
28
+ export const POPULATE_FROM_DATASTORE_ON_START = true
29
+ export const POPULATE_FROM_DATASTORE_LIMIT = 1000
17
30
 
18
31
  export interface RoutingTableInit {
19
32
  logPrefix: string
@@ -21,16 +34,28 @@ export interface RoutingTableInit {
21
34
  prefixLength?: number
22
35
  splitThreshold?: number
23
36
  kBucketSize?: number
24
- pingTimeout?: number
25
- pingConcurrency?: number
26
- tagName?: string
27
- tagValue?: number
37
+ pingNewContactTimeout?: AdaptiveTimeoutInit
38
+ pingNewContactConcurrency?: number
39
+ pingNewContactMaxQueueSize?: number
40
+ pingOldContactTimeout?: AdaptiveTimeoutInit
41
+ pingOldContactConcurrency?: number
42
+ pingOldContactMaxQueueSize?: number
43
+ numberOfOldContactsToPing?: number
44
+ peerTagName?: string
45
+ peerTagValue?: number
46
+ closeTagName?: string
47
+ closeTagValue?: number
48
+ network: Network
49
+ populateFromDatastoreOnStart?: boolean
50
+ populateFromDatastoreLimit?: number
51
+ lastPingThreshold?: number
52
+ closestPeerSetSize?: number
53
+ closestPeerSetRefreshInterval?: number
28
54
  }
29
55
 
30
56
  export interface RoutingTableComponents {
31
57
  peerId: PeerId
32
58
  peerStore: PeerStore
33
- connectionManager: ConnectionManager
34
59
  metrics?: Metrics
35
60
  logger: ComponentLogger
36
61
  }
@@ -38,32 +63,37 @@ export interface RoutingTableComponents {
38
63
  export interface RoutingTableEvents {
39
64
  'peer:add': CustomEvent<PeerId>
40
65
  'peer:remove': CustomEvent<PeerId>
66
+ 'peer:ping': CustomEvent<PeerId>
41
67
  }
42
68
 
43
69
  /**
44
- * A wrapper around `k-bucket`, to provide easy store and
45
- * retrieval for peers.
70
+ * A wrapper around `k-bucket`, to provide easy store and retrieval for peers.
46
71
  */
47
72
  export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implements Startable {
48
73
  public kBucketSize: number
49
- public kb?: KBucket
50
- public pingQueue: PeerQueue<boolean>
51
-
74
+ public kb: KBucket
75
+ public network: Network
76
+ private readonly closestPeerTagger: ClosestPeers
52
77
  private readonly log: Logger
53
78
  private readonly components: RoutingTableComponents
54
- private readonly prefixLength: number
55
- private readonly splitThreshold: number
56
- private readonly pingTimeout: number
57
- private readonly pingConcurrency: number
58
79
  private running: boolean
80
+ private readonly pingNewContactTimeout: AdaptiveTimeout
81
+ private readonly pingNewContactQueue: PeerQueue<boolean>
82
+ private readonly pingOldContactTimeout: AdaptiveTimeout
83
+ private readonly pingOldContactQueue: PeerQueue<boolean>
84
+ private readonly populateFromDatastoreOnStart: boolean
85
+ private readonly populateFromDatastoreLimit: number
59
86
  private readonly protocol: string
60
- private readonly tagName: string
61
- private readonly tagValue: number
87
+ private readonly peerTagName: string
88
+ private readonly peerTagValue: number
62
89
  private readonly metrics?: {
63
90
  routingTableSize: Metric
64
91
  routingTableKadBucketTotal: Metric
65
92
  routingTableKadBucketAverageOccupancy: Metric
66
93
  routingTableKadBucketMaxDepth: Metric
94
+ routingTableKadBucketMinOccupancy: Metric
95
+ routingTableKadBucketMaxOccupancy: Metric
96
+ kadBucketEvents: CounterGroup<'ping_old_contact' | 'ping_old_contact_error' | 'ping_new_contact' | 'ping_new_contact_error' | 'peer_added' | 'peer_removed'>
67
97
  }
68
98
 
69
99
  constructor (components: RoutingTableComponents, init: RoutingTableInit) {
@@ -72,22 +102,61 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
72
102
  this.components = components
73
103
  this.log = components.logger.forComponent(`${init.logPrefix}:routing-table`)
74
104
  this.kBucketSize = init.kBucketSize ?? KBUCKET_SIZE
75
- this.pingTimeout = init.pingTimeout ?? PING_TIMEOUT
76
- this.pingConcurrency = init.pingConcurrency ?? PING_CONCURRENCY
77
105
  this.running = false
78
106
  this.protocol = init.protocol
79
- this.tagName = init.tagName ?? KAD_CLOSE_TAG_NAME
80
- this.tagValue = init.tagValue ?? KAD_CLOSE_TAG_VALUE
81
- this.prefixLength = init.prefixLength ?? PREFIX_LENGTH
82
- this.splitThreshold = init.splitThreshold ?? KBUCKET_SIZE
83
-
84
- this.pingQueue = new PeerQueue({
85
- concurrency: this.pingConcurrency,
86
- metricName: `${init.logPrefix.replaceAll(':', '_')}_ping_queue`,
87
- metrics: this.components.metrics
107
+ this.network = init.network
108
+ this.peerTagName = init.peerTagName ?? KAD_PEER_TAG_NAME
109
+ this.peerTagValue = init.peerTagValue ?? KAD_PEER_TAG_VALUE
110
+ this.pingOldContacts = this.pingOldContacts.bind(this)
111
+ this.verifyNewContact = this.verifyNewContact.bind(this)
112
+ this.peerAdded = this.peerAdded.bind(this)
113
+ this.peerRemoved = this.peerRemoved.bind(this)
114
+ this.populateFromDatastoreOnStart = init.populateFromDatastoreOnStart ?? POPULATE_FROM_DATASTORE_ON_START
115
+ this.populateFromDatastoreLimit = init.populateFromDatastoreLimit ?? POPULATE_FROM_DATASTORE_LIMIT
116
+
117
+ this.pingOldContactQueue = new PeerQueue({
118
+ concurrency: init.pingOldContactConcurrency ?? PING_OLD_CONTACT_CONCURRENCY,
119
+ metricName: `${init.logPrefix.replaceAll(':', '_')}_ping_old_contact_queue`,
120
+ metrics: this.components.metrics,
121
+ maxSize: init.pingOldContactMaxQueueSize ?? PING_OLD_CONTACT_MAX_QUEUE_SIZE
122
+ })
123
+ this.pingOldContactTimeout = new AdaptiveTimeout({
124
+ ...(init.pingOldContactTimeout ?? {}),
125
+ metrics: this.components.metrics,
126
+ metricName: `${init.logPrefix.replaceAll(':', '_')}_routing_table_ping_old_contact_time_milliseconds`
127
+ })
128
+
129
+ this.pingNewContactQueue = new PeerQueue({
130
+ concurrency: init.pingNewContactConcurrency ?? PING_NEW_CONTACT_CONCURRENCY,
131
+ metricName: `${init.logPrefix.replaceAll(':', '_')}_ping_new_contact_queue`,
132
+ metrics: this.components.metrics,
133
+ maxSize: init.pingNewContactMaxQueueSize ?? PING_NEW_CONTACT_MAX_QUEUE_SIZE
134
+ })
135
+ this.pingNewContactTimeout = new AdaptiveTimeout({
136
+ ...(init.pingNewContactTimeout ?? {}),
137
+ metrics: this.components.metrics,
138
+ metricName: `${init.logPrefix.replaceAll(':', '_')}_routing_table_ping_new_contact_time_milliseconds`
139
+ })
140
+
141
+ this.kb = new KBucket({
142
+ kBucketSize: init.kBucketSize,
143
+ prefixLength: init.prefixLength,
144
+ splitThreshold: init.splitThreshold,
145
+ numberOfOldContactsToPing: init.numberOfOldContactsToPing,
146
+ lastPingThreshold: init.lastPingThreshold,
147
+ ping: this.pingOldContacts,
148
+ verify: this.verifyNewContact,
149
+ onAdd: this.peerAdded,
150
+ onRemove: this.peerRemoved
88
151
  })
89
- this.pingQueue.addEventListener('error', evt => {
90
- this.log.error('error pinging peer', evt.detail)
152
+
153
+ this.closestPeerTagger = new ClosestPeers(this.components, {
154
+ logPrefix: init.logPrefix,
155
+ routingTable: this,
156
+ peerSetSize: init.closestPeerSetSize,
157
+ refreshInterval: init.closestPeerSetRefreshInterval,
158
+ closeTagName: init.closeTagName,
159
+ closeTagValue: init.closeTagValue
91
160
  })
92
161
 
93
162
  if (this.components.metrics != null) {
@@ -95,7 +164,10 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
95
164
  routingTableSize: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_size`),
96
165
  routingTableKadBucketTotal: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_total`),
97
166
  routingTableKadBucketAverageOccupancy: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_average_occupancy`),
98
- routingTableKadBucketMaxDepth: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_max_depth`)
167
+ routingTableKadBucketMinOccupancy: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_min_occupancy`),
168
+ routingTableKadBucketMaxOccupancy: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_max_occupancy`),
169
+ routingTableKadBucketMaxDepth: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_max_depth`),
170
+ kadBucketEvents: this.components.metrics.registerCounterGroup(`${init.logPrefix.replaceAll(':', '_')}_kad_bucket_events_total`)
99
171
  }
100
172
  }
101
173
  }
@@ -107,101 +179,87 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
107
179
  async start (): Promise<void> {
108
180
  this.running = true
109
181
 
110
- const kBuck = new KBucket({
111
- localPeer: {
112
- kadId: await utils.convertPeerId(this.components.peerId),
113
- peerId: this.components.peerId
114
- },
115
- kBucketSize: this.kBucketSize,
116
- prefixLength: this.prefixLength,
117
- splitThreshold: this.splitThreshold,
118
- numberOfNodesToPing: 1
119
- })
120
- this.kb = kBuck
121
-
122
- // test whether to evict peers
123
- kBuck.addEventListener('ping', (evt) => {
124
- this._onPing(evt).catch(err => {
125
- this.log.error('could not process k-bucket ping event', err)
126
- })
127
- })
182
+ await start(this.closestPeerTagger)
183
+ await this.kb.addSelfPeer(this.components.peerId)
184
+ }
128
185
 
129
- let peerStorePeers = 0
186
+ async afterStart (): Promise<void> {
187
+ // do this async to not block startup but iterate serially to not overwhelm
188
+ // the ping queue
189
+ Promise.resolve().then(async () => {
190
+ if (!this.populateFromDatastoreOnStart) {
191
+ return
192
+ }
130
193
 
131
- // add existing peers from the peer store to routing table
132
- for (const peer of await this.components.peerStore.all()) {
133
- if (peer.protocols.includes(this.protocol)) {
134
- const id = await utils.convertPeerId(peer.id)
194
+ let peerStorePeers = 0
195
+
196
+ // add existing peers from the peer store to routing table
197
+ for (const peer of await this.components.peerStore.all({
198
+ filters: [(peer) => {
199
+ return peer.protocols.includes(this.protocol) && peer.tags.has(KAD_PEER_TAG_NAME)
200
+ }],
201
+ limit: this.populateFromDatastoreLimit
202
+ })) {
203
+ if (!this.running) {
204
+ // bail if we've been shut down
205
+ return
206
+ }
135
207
 
136
- this.kb.add({ kadId: id, peerId: peer.id })
137
- peerStorePeers++
208
+ try {
209
+ await this.add(peer.id)
210
+ peerStorePeers++
211
+ } catch (err) {
212
+ this.log('failed to add peer %p to routing table, removing kad-dht peer tags - %e')
213
+ await this.components.peerStore.merge(peer.id, {
214
+ tags: {
215
+ [this.peerTagName]: undefined
216
+ }
217
+ })
218
+ }
138
219
  }
139
- }
140
-
141
- this.log('added %d peer store peers to the routing table', peerStorePeers)
142
220
 
143
- // tag kad-close peers
144
- this._tagPeers(kBuck)
221
+ this.log('added %d peer store peers to the routing table', peerStorePeers)
222
+ })
223
+ .catch(err => {
224
+ this.log.error('error adding peer store peers to the routing table %e', err)
225
+ })
145
226
  }
146
227
 
147
228
  async stop (): Promise<void> {
148
229
  this.running = false
149
- this.pingQueue.clear()
150
- this.kb = undefined
230
+ await stop(this.closestPeerTagger)
231
+ this.pingOldContactQueue.abort()
232
+ this.pingNewContactQueue.abort()
151
233
  }
152
234
 
153
- /**
154
- * Keep track of our k-closest peers and tag them in the peer store as such
155
- * - this will lower the chances that connections to them get closed when
156
- * we reach connection limits
157
- */
158
- _tagPeers (kBuck: KBucket): void {
159
- let kClosest = new PeerSet()
160
-
161
- const updatePeerTags = utils.debounce(() => {
162
- const newClosest = new PeerSet(
163
- kBuck.closest(kBuck.localPeer.kadId, KBUCKET_SIZE)
164
- )
165
- const addedPeers = newClosest.difference(kClosest)
166
- const removedPeers = kClosest.difference(newClosest)
167
-
168
- Promise.resolve()
169
- .then(async () => {
170
- for (const peer of addedPeers) {
171
- await this.components.peerStore.merge(peer, {
172
- tags: {
173
- [this.tagName]: {
174
- value: this.tagValue
175
- }
176
- }
177
- })
178
- }
179
-
180
- for (const peer of removedPeers) {
181
- await this.components.peerStore.merge(peer, {
182
- tags: {
183
- [this.tagName]: undefined
184
- }
185
- })
235
+ private async peerAdded (peer: Peer, bucket: LeafBucket): Promise<void> {
236
+ if (!this.components.peerId.equals(peer.peerId)) {
237
+ await this.components.peerStore.merge(peer.peerId, {
238
+ tags: {
239
+ [this.peerTagName]: {
240
+ value: this.peerTagValue
186
241
  }
187
- })
188
- .catch(err => {
189
- this.log.error('Could not update peer tags', err)
190
- })
191
-
192
- kClosest = newClosest
193
- })
242
+ }
243
+ })
244
+ }
194
245
 
195
- kBuck.addEventListener('added', (evt) => {
196
- updatePeerTags()
246
+ this.updateMetrics()
247
+ this.metrics?.kadBucketEvents.increment({ peer_added: true })
248
+ this.safeDispatchEvent('peer:add', { detail: peer.peerId })
249
+ }
197
250
 
198
- this.safeDispatchEvent('peer:add', { detail: evt.detail.peerId })
199
- })
200
- kBuck.addEventListener('removed', (evt) => {
201
- updatePeerTags()
251
+ private async peerRemoved (peer: Peer, bucket: LeafBucket): Promise<void> {
252
+ if (!this.components.peerId.equals(peer.peerId)) {
253
+ await this.components.peerStore.merge(peer.peerId, {
254
+ tags: {
255
+ [this.peerTagName]: undefined
256
+ }
257
+ })
258
+ }
202
259
 
203
- this.safeDispatchEvent('peer:remove', { detail: evt.detail.peerId })
204
- })
260
+ this.updateMetrics()
261
+ this.metrics?.kadBucketEvents.increment({ peer_removed: true })
262
+ this.safeDispatchEvent('peer:remove', { detail: peer.peerId })
205
263
  }
206
264
 
207
265
  /**
@@ -214,82 +272,132 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
214
272
  * `oldContacts` will not be empty and is the list of contacts that
215
273
  * have not been contacted for the longest.
216
274
  */
217
- async _onPing (evt: CustomEvent<PingEventDetails>): Promise<void> {
275
+ async * pingOldContacts (oldContacts: Peer[], options?: AbortOptions): AsyncGenerator<Peer> {
218
276
  if (!this.running) {
219
277
  return
220
278
  }
221
279
 
222
- const {
223
- oldContacts,
224
- newContact
225
- } = evt.detail
226
-
227
- const results = await Promise.all(
228
- oldContacts.map(async oldContact => {
229
- // if a previous ping wants us to ping this contact, re-use the result
230
- const pingJob = this.pingQueue.find(oldContact.peerId)
280
+ const jobs: Array<() => Promise<Peer | undefined>> = []
231
281
 
232
- if (pingJob != null) {
233
- return pingJob.join()
234
- }
282
+ for (const oldContact of oldContacts) {
283
+ if (this.kb.get(oldContact.kadId) == null) {
284
+ this.log('asked to ping contact %p that was not in routing table', oldContact.peerId)
285
+ continue
286
+ }
235
287
 
236
- return this.pingQueue.add(async () => {
237
- let stream: Stream | undefined
288
+ this.metrics?.kadBucketEvents.increment({ ping_old_contact: true })
238
289
 
239
- try {
240
- const options = {
241
- signal: AbortSignal.timeout(this.pingTimeout)
242
- }
290
+ jobs.push(async () => {
291
+ // if a previous ping wants us to ping this contact, re-use the result
292
+ const existingJob = this.pingOldContactQueue.find(oldContact.peerId)
243
293
 
244
- this.log('pinging old contact %p', oldContact.peerId)
245
- const connection = await this.components.connectionManager.openConnection(oldContact.peerId, options)
246
- stream = await connection.newStream(this.protocol, options)
294
+ if (existingJob != null) {
295
+ this.log('asked to ping contact %p was already being pinged', oldContact.peerId)
296
+ const result = await existingJob.join(options)
247
297
 
248
- const pb = pbStream(stream)
249
- await pb.write({
250
- type: MessageType.PING
251
- }, Message, options)
252
- const response = await pb.read(Message, options)
298
+ if (!result) {
299
+ return oldContact
300
+ }
253
301
 
254
- await pb.unwrap().close()
302
+ return
303
+ }
255
304
 
256
- if (response.type !== MessageType.PING) {
257
- throw new InvalidMessageError(`Incorrect message type received, expected PING got ${response.type}`)
258
- }
305
+ const result = await this.pingOldContactQueue.add(async (options) => {
306
+ const signal = this.pingOldContactTimeout.getTimeoutSignal()
307
+ const signals = anySignal([signal, options?.signal])
308
+ setMaxListeners(Infinity, signal, signals)
259
309
 
310
+ try {
311
+ return await this.pingContact(oldContact, options)
312
+ } catch {
313
+ this.metrics?.kadBucketEvents.increment({ ping_old_contact_error: true })
260
314
  return true
261
- } catch (err: any) {
262
- if (this.running && this.kb != null) {
263
- // only evict peers if we are still running, otherwise we evict
264
- // when dialing is cancelled due to shutdown in progress
265
- this.log.error('could not ping peer %p', oldContact.peerId, err)
266
- this.log('evicting old contact after ping failed %p', oldContact.peerId)
267
- this.kb.remove(oldContact.kadId)
268
- }
269
-
270
- stream?.abort(err)
271
-
272
- return false
273
315
  } finally {
274
- this.metrics?.routingTableSize.update(this.size)
316
+ this.pingOldContactTimeout.cleanUp(signal)
317
+ signals.clear()
275
318
  }
276
319
  }, {
277
- peerId: oldContact.peerId
320
+ peerId: oldContact.peerId,
321
+ signal: options?.signal
278
322
  })
323
+
324
+ if (!result) {
325
+ return oldContact
326
+ }
279
327
  })
280
- )
328
+ }
329
+
330
+ for await (const peer of parallel(jobs)) {
331
+ if (peer != null) {
332
+ yield peer
333
+ }
334
+ }
335
+ }
336
+
337
+ async verifyNewContact (contact: Peer, options?: AbortOptions): Promise<boolean> {
338
+ const signal = this.pingNewContactTimeout.getTimeoutSignal()
339
+ const signals = anySignal([signal, options?.signal])
340
+ setMaxListeners(Infinity, signal, signals)
281
341
 
282
- const responded = results
283
- .filter(res => res)
284
- .length
342
+ try {
343
+ const job = this.pingNewContactQueue.find(contact.peerId)
285
344
 
286
- if (this.running && responded < oldContacts.length && this.kb != null) {
287
- this.log('adding new contact %p', newContact.peerId)
288
- this.kb.add(newContact)
345
+ if (job != null) {
346
+ this.log('joining existing ping to add new peer %p to routing table', contact.peerId)
347
+ return await job.join({
348
+ signal: signals
349
+ })
350
+ } else {
351
+ return await this.pingNewContactQueue.add(async (options) => {
352
+ this.metrics?.kadBucketEvents.increment({ ping_new_contact: true })
353
+
354
+ this.log('pinging new peer %p before adding to routing table', contact.peerId)
355
+ return this.pingContact(contact, options)
356
+ }, {
357
+ peerId: contact.peerId,
358
+ signal: signals
359
+ })
360
+ }
361
+ } catch (err) {
362
+ this.log.trace('tried to add peer %p but they were not online', contact.peerId)
363
+ this.metrics?.kadBucketEvents.increment({ ping_new_contact_error: true })
364
+
365
+ return false
366
+ } finally {
367
+ this.pingNewContactTimeout.cleanUp(signal)
368
+ signals.clear()
289
369
  }
290
370
  }
291
371
 
292
- // -- Public Interface
372
+ async pingContact (contact: Peer, options?: AbortOptions): Promise<boolean> {
373
+ let stream: Stream | undefined
374
+
375
+ try {
376
+ this.log('pinging contact %p', contact.peerId)
377
+
378
+ for await (const event of this.network.sendRequest(contact.peerId, { type: MessageType.PING }, options)) {
379
+ if (event.type === EventTypes.PEER_RESPONSE) {
380
+ if (event.messageType === MessageType.PING) {
381
+ this.log('contact %p ping ok', contact.peerId)
382
+
383
+ this.safeDispatchEvent('peer:ping', {
384
+ detail: contact.peerId
385
+ })
386
+
387
+ return true
388
+ }
389
+
390
+ return false
391
+ }
392
+ }
393
+
394
+ return false
395
+ } catch (err: any) {
396
+ this.log('error pinging old contact %p - %e', contact.peerId, err)
397
+ stream?.abort(err)
398
+ return false
399
+ }
400
+ }
293
401
 
294
402
  /**
295
403
  * Amount of currently stored peers
@@ -306,8 +414,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
306
414
  * Find a specific peer by id
307
415
  */
308
416
  async find (peer: PeerId): Promise<PeerId | undefined> {
309
- const key = await utils.convertPeerId(peer)
310
- return this.kb?.get(key)?.peerId
417
+ const kadId = await utils.convertPeerId(peer)
418
+ return this.kb.get(kadId)?.peerId
311
419
  }
312
420
 
313
421
  /**
@@ -337,18 +445,12 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
337
445
  /**
338
446
  * Add or update the routing table with the given peer
339
447
  */
340
- async add (peerId: PeerId): Promise<void> {
448
+ async add (peerId: PeerId, options?: AbortOptions): Promise<void> {
341
449
  if (this.kb == null) {
342
450
  throw new Error('RoutingTable is not started')
343
451
  }
344
452
 
345
- const kadId = await utils.convertPeerId(peerId)
346
-
347
- this.kb.add({ kadId, peerId })
348
-
349
- this.log.trace('added %p with kad id %b', peerId, kadId)
350
-
351
- this.updateMetrics()
453
+ await this.kb.add(peerId, options)
352
454
  }
353
455
 
354
456
  /**
@@ -359,11 +461,9 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
359
461
  throw new Error('RoutingTable is not started')
360
462
  }
361
463
 
362
- const id = await utils.convertPeerId(peer)
363
-
364
- this.kb.remove(id)
464
+ const kadId = await utils.convertPeerId(peer)
365
465
 
366
- this.updateMetrics()
466
+ await this.kb.remove(kadId)
367
467
  }
368
468
 
369
469
  private updateMetrics (): void {
@@ -374,6 +474,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
374
474
  let size = 0
375
475
  let buckets = 0
376
476
  let maxDepth = 0
477
+ let minOccupancy = 20
478
+ let maxOccupancy = 0
377
479
 
378
480
  function count (bucket: Bucket): void {
379
481
  if (isLeafBucket(bucket)) {
@@ -383,6 +485,15 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
383
485
 
384
486
  buckets++
385
487
  size += bucket.peers.length
488
+
489
+ if (bucket.peers.length < minOccupancy) {
490
+ minOccupancy = bucket.peers.length
491
+ }
492
+
493
+ if (bucket.peers.length > maxOccupancy) {
494
+ maxOccupancy = bucket.peers.length
495
+ }
496
+
386
497
  return
387
498
  }
388
499
 
@@ -395,6 +506,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
395
506
  this.metrics.routingTableSize.update(size)
396
507
  this.metrics.routingTableKadBucketTotal.update(buckets)
397
508
  this.metrics.routingTableKadBucketAverageOccupancy.update(Math.round(size / buckets))
509
+ this.metrics.routingTableKadBucketMinOccupancy.update(minOccupancy)
510
+ this.metrics.routingTableKadBucketMaxOccupancy.update(maxOccupancy)
398
511
  this.metrics.routingTableKadBucketMaxDepth.update(maxDepth)
399
512
  }
400
513
  }