@libp2p/kad-dht 13.1.2-35b48025c → 13.1.2-661d6586a

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