@libp2p/kad-dht 13.1.2-b4f02a637 → 14.0.0-d4da56961

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) 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/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-self.d.ts.map +1 -1
  18. package/dist/src/query-self.js +13 -6
  19. package/dist/src/query-self.js.map +1 -1
  20. package/dist/src/routing-table/closest-peers.d.ts +43 -0
  21. package/dist/src/routing-table/closest-peers.d.ts.map +1 -0
  22. package/dist/src/routing-table/closest-peers.js +86 -0
  23. package/dist/src/routing-table/closest-peers.js.map +1 -0
  24. package/dist/src/routing-table/index.d.ts +54 -32
  25. package/dist/src/routing-table/index.d.ts.map +1 -1
  26. package/dist/src/routing-table/index.js +256 -172
  27. package/dist/src/routing-table/index.js.map +1 -1
  28. package/dist/src/routing-table/k-bucket.d.ts +65 -21
  29. package/dist/src/routing-table/k-bucket.d.ts.map +1 -1
  30. package/dist/src/routing-table/k-bucket.js +122 -66
  31. package/dist/src/routing-table/k-bucket.js.map +1 -1
  32. package/dist/src/routing-table/refresh.d.ts.map +1 -1
  33. package/dist/src/routing-table/refresh.js +4 -1
  34. package/dist/src/routing-table/refresh.js.map +1 -1
  35. package/dist/src/rpc/index.d.ts.map +1 -1
  36. package/dist/src/rpc/index.js +3 -7
  37. package/dist/src/rpc/index.js.map +1 -1
  38. package/package.json +11 -12
  39. package/src/index.ts +30 -2
  40. package/src/kad-dht.ts +29 -15
  41. package/src/network.ts +1 -4
  42. package/src/{peer-list/peer-distance-list.ts → peer-distance-list.ts} +1 -1
  43. package/src/peer-routing/index.ts +1 -1
  44. package/src/query-self.ts +14 -6
  45. package/src/routing-table/closest-peers.ts +113 -0
  46. package/src/routing-table/index.ts +296 -205
  47. package/src/routing-table/k-bucket.ts +194 -81
  48. package/src/routing-table/refresh.ts +5 -1
  49. package/src/rpc/index.ts +4 -7
  50. package/dist/src/peer-list/index.d.ts +0 -29
  51. package/dist/src/peer-list/index.d.ts.map +0 -1
  52. package/dist/src/peer-list/index.js +0 -45
  53. package/dist/src/peer-list/index.js.map +0 -1
  54. package/dist/src/peer-list/peer-distance-list.d.ts.map +0 -1
  55. package/dist/src/peer-list/peer-distance-list.js.map +0 -1
  56. package/src/peer-list/index.ts +0 -54
  57. /package/dist/src/{peer-list/peer-distance-list.d.ts → peer-distance-list.d.ts} +0 -0
@@ -1,21 +1,32 @@
1
- import { InvalidMessageError, KEEP_ALIVE, TypedEventEmitter } from '@libp2p/interface'
2
- import { PeerSet } from '@libp2p/peer-collections'
1
+ import { TypedEventEmitter, setMaxListeners, start, stop } 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 { 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'
11
14
  import type { AdaptiveTimeoutInit } from '@libp2p/utils/adaptive-timeout'
12
15
 
13
- export const KAD_CLOSE_TAG_NAME = 'kad-close'
14
- export const KAD_CLOSE_TAG_VALUE = 50
15
16
  export const KBUCKET_SIZE = 20
16
- export const PREFIX_LENGTH = 32
17
- export const PING_TIMEOUT = 2000
18
- export const PING_CONCURRENCY = 20
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
19
30
 
20
31
  export interface RoutingTableInit {
21
32
  logPrefix: string
@@ -23,16 +34,28 @@ export interface RoutingTableInit {
23
34
  prefixLength?: number
24
35
  splitThreshold?: number
25
36
  kBucketSize?: number
26
- pingTimeout?: AdaptiveTimeoutInit
27
- pingConcurrency?: number
28
- tagName?: string
29
- 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
30
54
  }
31
55
 
32
56
  export interface RoutingTableComponents {
33
57
  peerId: PeerId
34
58
  peerStore: PeerStore
35
- connectionManager: ConnectionManager
36
59
  metrics?: Metrics
37
60
  logger: ComponentLogger
38
61
  }
@@ -40,33 +63,37 @@ export interface RoutingTableComponents {
40
63
  export interface RoutingTableEvents {
41
64
  'peer:add': CustomEvent<PeerId>
42
65
  'peer:remove': CustomEvent<PeerId>
66
+ 'peer:ping': CustomEvent<PeerId>
43
67
  }
44
68
 
45
69
  /**
46
- * A wrapper around `k-bucket`, to provide easy store and
47
- * retrieval for peers.
70
+ * A wrapper around `k-bucket`, to provide easy store and retrieval for peers.
48
71
  */
49
72
  export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implements Startable {
50
73
  public kBucketSize: number
51
- public kb?: KBucket
52
- public pingQueue: PeerQueue<boolean>
53
-
74
+ public kb: KBucket
75
+ public network: Network
76
+ private readonly closestPeerTagger: ClosestPeers
54
77
  private readonly log: Logger
55
78
  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
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
61
86
  private readonly protocol: string
62
- private readonly tagName: string
63
- private readonly tagValue: number
87
+ private readonly peerTagName: string
88
+ private readonly peerTagValue: number
64
89
  private readonly metrics?: {
65
90
  routingTableSize: Metric
66
91
  routingTableKadBucketTotal: Metric
67
92
  routingTableKadBucketAverageOccupancy: Metric
68
93
  routingTableKadBucketMaxDepth: Metric
69
- kadBucketEvents: CounterGroup<'ping' | 'ping_error' | 'peer_added' | 'peer_removed'>
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'>
70
97
  }
71
98
 
72
99
  constructor (components: RoutingTableComponents, init: RoutingTableInit) {
@@ -75,26 +102,61 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
75
102
  this.components = components
76
103
  this.log = components.logger.forComponent(`${init.logPrefix}:routing-table`)
77
104
  this.kBucketSize = init.kBucketSize ?? KBUCKET_SIZE
78
- this.pingConcurrency = init.pingConcurrency ?? PING_CONCURRENCY
79
105
  this.running = false
80
106
  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
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
90
122
  })
91
- this.pingQueue.addEventListener('error', evt => {
92
- this.log.error('error pinging peer', evt.detail)
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`
93
127
  })
94
- this.pingTimeout = new AdaptiveTimeout({
95
- ...(init.pingTimeout ?? {}),
128
+
129
+ this.pingNewContactQueue = new PeerQueue({
130
+ concurrency: init.pingNewContactConcurrency ?? PING_NEW_CONTACT_CONCURRENCY,
131
+ metricName: `${init.logPrefix.replaceAll(':', '_')}_ping_new_contact_queue`,
96
132
  metrics: this.components.metrics,
97
- metricName: `${init.logPrefix.replaceAll(':', '_')}_routing_table_ping_time_milliseconds`
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
151
+ })
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
98
160
  })
99
161
 
100
162
  if (this.components.metrics != null) {
@@ -102,6 +164,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
102
164
  routingTableSize: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_size`),
103
165
  routingTableKadBucketTotal: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_total`),
104
166
  routingTableKadBucketAverageOccupancy: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_average_occupancy`),
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`),
105
169
  routingTableKadBucketMaxDepth: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_max_depth`),
106
170
  kadBucketEvents: this.components.metrics.registerCounterGroup(`${init.logPrefix.replaceAll(':', '_')}_kad_bucket_events_total`)
107
171
  }
@@ -115,110 +179,87 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
115
179
  async start (): Promise<void> {
116
180
  this.running = true
117
181
 
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
- })
182
+ await start(this.closestPeerTagger)
183
+ await this.kb.addSelfPeer(this.components.peerId)
184
+ }
139
185
 
140
- 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
+ }
141
193
 
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)
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
+ }
146
207
 
147
- this.kb.add({ kadId: id, peerId: peer.id })
148
- 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
+ }
149
219
  }
150
- }
151
220
 
152
- this.log('added %d peer store peers to the routing table', peerStorePeers)
153
-
154
- // tag kad-close peers
155
- 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
+ })
156
226
  }
157
227
 
158
228
  async stop (): Promise<void> {
159
229
  this.running = false
160
- this.pingQueue.clear()
161
- this.kb = undefined
230
+ await stop(this.closestPeerTagger)
231
+ this.pingOldContactQueue.abort()
232
+ this.pingNewContactQueue.abort()
162
233
  }
163
234
 
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
- }
193
-
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
- })
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
201
241
  }
202
- })
203
- .catch(err => {
204
- this.log.error('Could not update peer tags', err)
205
- })
206
-
207
- kClosest = newClosest
208
- })
242
+ }
243
+ })
244
+ }
209
245
 
210
- kBuck.addEventListener('added', (evt) => {
211
- updatePeerTags()
246
+ this.updateMetrics()
247
+ this.metrics?.kadBucketEvents.increment({ peer_added: true })
248
+ this.safeDispatchEvent('peer:add', { detail: peer.peerId })
249
+ }
212
250
 
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()
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
+ }
218
259
 
219
- this.metrics?.kadBucketEvents.increment({ peer_removed: true })
220
- this.safeDispatchEvent('peer:remove', { detail: evt.detail.peerId })
221
- })
260
+ this.updateMetrics()
261
+ this.metrics?.kadBucketEvents.increment({ peer_removed: true })
262
+ this.safeDispatchEvent('peer:remove', { detail: peer.peerId })
222
263
  }
223
264
 
224
265
  /**
@@ -231,87 +272,132 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
231
272
  * `oldContacts` will not be empty and is the list of contacts that
232
273
  * have not been contacted for the longest.
233
274
  */
234
- async _onPing (evt: CustomEvent<PingEventDetails>): Promise<void> {
275
+ async * pingOldContacts (oldContacts: Peer[], options?: AbortOptions): AsyncGenerator<Peer> {
235
276
  if (!this.running) {
236
277
  return
237
278
  }
238
279
 
239
- const {
240
- oldContacts,
241
- newContact
242
- } = evt.detail
280
+ const jobs: Array<() => Promise<Peer | undefined>> = []
243
281
 
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
- }
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
+ }
252
287
 
253
- return this.pingQueue.add(async () => {
254
- let stream: Stream | undefined
255
- const signal = this.pingTimeout.getTimeoutSignal()
288
+ this.metrics?.kadBucketEvents.increment({ ping_old_contact: true })
256
289
 
257
- try {
258
- const options = {
259
- signal
260
- }
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)
261
293
 
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)
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)
265
297
 
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)
298
+ if (!result) {
299
+ return oldContact
300
+ }
273
301
 
274
- await pb.unwrap().unwrap().close(options)
302
+ return
303
+ }
275
304
 
276
- if (response.type !== MessageType.PING) {
277
- throw new InvalidMessageError(`Incorrect message type received, expected PING got ${response.type}`)
278
- }
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)
279
309
 
280
- this.log('old contact %p ping ok', oldContact.peerId)
310
+ try {
311
+ return await this.pingContact(oldContact, options)
312
+ } catch {
313
+ this.metrics?.kadBucketEvents.increment({ ping_old_contact_error: true })
281
314
  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
315
  } finally {
295
- this.pingTimeout.cleanUp(signal)
296
- this.updateMetrics()
316
+ this.pingOldContactTimeout.cleanUp(signal)
317
+ signals.clear()
297
318
  }
298
319
  }, {
299
- peerId: oldContact.peerId
320
+ peerId: oldContact.peerId,
321
+ signal: options?.signal
300
322
  })
323
+
324
+ if (!result) {
325
+ return oldContact
326
+ }
301
327
  })
302
- )
328
+ }
329
+
330
+ for await (const peer of parallel(jobs)) {
331
+ if (peer != null) {
332
+ yield peer
333
+ }
334
+ }
335
+ }
303
336
 
304
- const responded = results
305
- .filter(res => res)
306
- .length
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)
307
341
 
308
- if (this.running && responded < oldContacts.length && this.kb != null) {
309
- this.log('adding new contact %p', newContact.peerId)
310
- this.kb.add(newContact)
342
+ try {
343
+ const job = this.pingNewContactQueue.find(contact.peerId)
344
+
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()
311
369
  }
312
370
  }
313
371
 
314
- // -- 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
+ }
315
401
 
316
402
  /**
317
403
  * Amount of currently stored peers
@@ -328,8 +414,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
328
414
  * Find a specific peer by id
329
415
  */
330
416
  async find (peer: PeerId): Promise<PeerId | undefined> {
331
- const key = await utils.convertPeerId(peer)
332
- return this.kb?.get(key)?.peerId
417
+ const kadId = await utils.convertPeerId(peer)
418
+ return this.kb.get(kadId)?.peerId
333
419
  }
334
420
 
335
421
  /**
@@ -359,18 +445,12 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
359
445
  /**
360
446
  * Add or update the routing table with the given peer
361
447
  */
362
- async add (peerId: PeerId): Promise<void> {
448
+ async add (peerId: PeerId, options?: AbortOptions): Promise<void> {
363
449
  if (this.kb == null) {
364
450
  throw new Error('RoutingTable is not started')
365
451
  }
366
452
 
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()
453
+ await this.kb.add(peerId, options)
374
454
  }
375
455
 
376
456
  /**
@@ -381,11 +461,9 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
381
461
  throw new Error('RoutingTable is not started')
382
462
  }
383
463
 
384
- const id = await utils.convertPeerId(peer)
385
-
386
- this.kb.remove(id)
464
+ const kadId = await utils.convertPeerId(peer)
387
465
 
388
- this.updateMetrics()
466
+ await this.kb.remove(kadId)
389
467
  }
390
468
 
391
469
  private updateMetrics (): void {
@@ -396,6 +474,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
396
474
  let size = 0
397
475
  let buckets = 0
398
476
  let maxDepth = 0
477
+ let minOccupancy = 20
478
+ let maxOccupancy = 0
399
479
 
400
480
  function count (bucket: Bucket): void {
401
481
  if (isLeafBucket(bucket)) {
@@ -405,6 +485,15 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
405
485
 
406
486
  buckets++
407
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
+
408
497
  return
409
498
  }
410
499
 
@@ -417,6 +506,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
417
506
  this.metrics.routingTableSize.update(size)
418
507
  this.metrics.routingTableKadBucketTotal.update(buckets)
419
508
  this.metrics.routingTableKadBucketAverageOccupancy.update(Math.round(size / buckets))
509
+ this.metrics.routingTableKadBucketMinOccupancy.update(minOccupancy)
510
+ this.metrics.routingTableKadBucketMaxOccupancy.update(maxOccupancy)
420
511
  this.metrics.routingTableKadBucketMaxDepth.update(maxDepth)
421
512
  }
422
513
  }