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

Sign up to get free protection for your applications and to get access to all the features.
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
  }