@libp2p/kad-dht 13.1.2 → 14.0.0-0d326d102

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/constants.d.ts +1 -0
  3. package/dist/src/constants.d.ts.map +1 -1
  4. package/dist/src/constants.js +3 -0
  5. package/dist/src/constants.js.map +1 -1
  6. package/dist/src/index.d.ts +28 -6
  7. package/dist/src/index.d.ts.map +1 -1
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/kad-dht.d.ts.map +1 -1
  10. package/dist/src/kad-dht.js +25 -11
  11. package/dist/src/kad-dht.js.map +1 -1
  12. package/dist/src/network.d.ts +1 -1
  13. package/dist/src/network.d.ts.map +1 -1
  14. package/dist/src/network.js +1 -3
  15. package/dist/src/network.js.map +1 -1
  16. package/dist/src/peer-distance-list.d.ts.map +1 -0
  17. package/dist/src/{peer-list/peer-distance-list.js → peer-distance-list.js} +1 -1
  18. package/dist/src/peer-distance-list.js.map +1 -0
  19. package/dist/src/peer-routing/index.js +1 -1
  20. package/dist/src/peer-routing/index.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 +259 -161
  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/constants.ts +5 -0
  44. package/src/index.ts +32 -6
  45. package/src/kad-dht.ts +29 -13
  46. package/src/network.ts +1 -4
  47. package/src/{peer-list/peer-distance-list.ts → peer-distance-list.ts} +1 -1
  48. package/src/peer-routing/index.ts +1 -1
  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 +300 -194
  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, CounterGroup, 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,33 +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
67
- 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'>
68
97
  }
69
98
 
70
99
  constructor (components: RoutingTableComponents, init: RoutingTableInit) {
@@ -73,22 +102,61 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
73
102
  this.components = components
74
103
  this.log = components.logger.forComponent(`${init.logPrefix}:routing-table`)
75
104
  this.kBucketSize = init.kBucketSize ?? KBUCKET_SIZE
76
- this.pingTimeout = init.pingTimeout ?? PING_TIMEOUT
77
- this.pingConcurrency = init.pingConcurrency ?? PING_CONCURRENCY
78
105
  this.running = false
79
106
  this.protocol = init.protocol
80
- this.tagName = init.tagName ?? KAD_CLOSE_TAG_NAME
81
- this.tagValue = init.tagValue ?? KAD_CLOSE_TAG_VALUE
82
- this.prefixLength = init.prefixLength ?? PREFIX_LENGTH
83
- this.splitThreshold = init.splitThreshold ?? KBUCKET_SIZE
84
-
85
- this.pingQueue = new PeerQueue({
86
- concurrency: this.pingConcurrency,
87
- metricName: `${init.logPrefix.replaceAll(':', '_')}_ping_queue`,
88
- 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
89
151
  })
90
- this.pingQueue.addEventListener('error', evt => {
91
- 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
92
160
  })
93
161
 
94
162
  if (this.components.metrics != null) {
@@ -96,6 +164,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
96
164
  routingTableSize: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_size`),
97
165
  routingTableKadBucketTotal: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_total`),
98
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`),
99
169
  routingTableKadBucketMaxDepth: this.components.metrics.registerMetric(`${init.logPrefix.replaceAll(':', '_')}_routing_table_kad_bucket_max_depth`),
100
170
  kadBucketEvents: this.components.metrics.registerCounterGroup(`${init.logPrefix.replaceAll(':', '_')}_kad_bucket_events_total`)
101
171
  }
@@ -109,106 +179,87 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
109
179
  async start (): Promise<void> {
110
180
  this.running = true
111
181
 
112
- const kBuck = new KBucket({
113
- localPeer: {
114
- kadId: await utils.convertPeerId(this.components.peerId),
115
- peerId: this.components.peerId
116
- },
117
- kBucketSize: this.kBucketSize,
118
- prefixLength: this.prefixLength,
119
- splitThreshold: this.splitThreshold,
120
- numberOfNodesToPing: 1
121
- })
122
- this.kb = kBuck
123
-
124
- // test whether to evict peers
125
- kBuck.addEventListener('ping', (evt) => {
126
- this.metrics?.kadBucketEvents.increment({ ping: true })
127
-
128
- this._onPing(evt).catch(err => {
129
- this.metrics?.kadBucketEvents.increment({ ping_error: true })
130
- this.log.error('could not process k-bucket ping event', err)
131
- })
132
- })
182
+ await start(this.closestPeerTagger)
183
+ await this.kb.addSelfPeer(this.components.peerId)
184
+ }
133
185
 
134
- 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
+ }
135
193
 
136
- // add existing peers from the peer store to routing table
137
- for (const peer of await this.components.peerStore.all()) {
138
- if (peer.protocols.includes(this.protocol)) {
139
- 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
+ }
140
207
 
141
- this.kb.add({ kadId: id, peerId: peer.id })
142
- 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
+ }
143
219
  }
144
- }
145
-
146
- this.log('added %d peer store peers to the routing table', peerStorePeers)
147
220
 
148
- // tag kad-close peers
149
- 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
+ })
150
226
  }
151
227
 
152
228
  async stop (): Promise<void> {
153
229
  this.running = false
154
- this.pingQueue.clear()
155
- this.kb = undefined
230
+ await stop(this.closestPeerTagger)
231
+ this.pingOldContactQueue.abort()
232
+ this.pingNewContactQueue.abort()
156
233
  }
157
234
 
158
- /**
159
- * Keep track of our k-closest peers and tag them in the peer store as such
160
- * - this will lower the chances that connections to them get closed when
161
- * we reach connection limits
162
- */
163
- _tagPeers (kBuck: KBucket): void {
164
- let kClosest = new PeerSet()
165
-
166
- const updatePeerTags = utils.debounce(() => {
167
- const newClosest = new PeerSet(
168
- kBuck.closest(kBuck.localPeer.kadId, KBUCKET_SIZE)
169
- )
170
- const addedPeers = newClosest.difference(kClosest)
171
- const removedPeers = kClosest.difference(newClosest)
172
-
173
- Promise.resolve()
174
- .then(async () => {
175
- for (const peer of addedPeers) {
176
- await this.components.peerStore.merge(peer, {
177
- tags: {
178
- [this.tagName]: {
179
- value: this.tagValue
180
- }
181
- }
182
- })
183
- }
184
-
185
- for (const peer of removedPeers) {
186
- await this.components.peerStore.merge(peer, {
187
- tags: {
188
- [this.tagName]: undefined
189
- }
190
- })
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
191
241
  }
192
- })
193
- .catch(err => {
194
- this.log.error('Could not update peer tags', err)
195
- })
196
-
197
- kClosest = newClosest
198
- })
242
+ }
243
+ })
244
+ }
199
245
 
200
- kBuck.addEventListener('added', (evt) => {
201
- updatePeerTags()
246
+ this.updateMetrics()
247
+ this.metrics?.kadBucketEvents.increment({ peer_added: true })
248
+ this.safeDispatchEvent('peer:add', { detail: peer.peerId })
249
+ }
202
250
 
203
- this.metrics?.kadBucketEvents.increment({ peer_added: true })
204
- this.safeDispatchEvent('peer:add', { detail: evt.detail.peerId })
205
- })
206
- kBuck.addEventListener('removed', (evt) => {
207
- 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
+ }
208
259
 
209
- this.metrics?.kadBucketEvents.increment({ peer_removed: true })
210
- this.safeDispatchEvent('peer:remove', { detail: evt.detail.peerId })
211
- })
260
+ this.updateMetrics()
261
+ this.metrics?.kadBucketEvents.increment({ peer_removed: true })
262
+ this.safeDispatchEvent('peer:remove', { detail: peer.peerId })
212
263
  }
213
264
 
214
265
  /**
@@ -221,82 +272,132 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
221
272
  * `oldContacts` will not be empty and is the list of contacts that
222
273
  * have not been contacted for the longest.
223
274
  */
224
- async _onPing (evt: CustomEvent<PingEventDetails>): Promise<void> {
275
+ async * pingOldContacts (oldContacts: Peer[], options?: AbortOptions): AsyncGenerator<Peer> {
225
276
  if (!this.running) {
226
277
  return
227
278
  }
228
279
 
229
- const {
230
- oldContacts,
231
- newContact
232
- } = evt.detail
233
-
234
- const results = await Promise.all(
235
- oldContacts.map(async oldContact => {
236
- // if a previous ping wants us to ping this contact, re-use the result
237
- const pingJob = this.pingQueue.find(oldContact.peerId)
280
+ const jobs: Array<() => Promise<Peer | undefined>> = []
238
281
 
239
- if (pingJob != null) {
240
- return pingJob.join()
241
- }
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
+ }
242
287
 
243
- return this.pingQueue.add(async () => {
244
- let stream: Stream | undefined
288
+ this.metrics?.kadBucketEvents.increment({ ping_old_contact: true })
245
289
 
246
- try {
247
- const options = {
248
- signal: AbortSignal.timeout(this.pingTimeout)
249
- }
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)
250
293
 
251
- this.log('pinging old contact %p', oldContact.peerId)
252
- const connection = await this.components.connectionManager.openConnection(oldContact.peerId, options)
253
- 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)
254
297
 
255
- const pb = pbStream(stream)
256
- await pb.write({
257
- type: MessageType.PING
258
- }, Message, options)
259
- const response = await pb.read(Message, options)
298
+ if (!result) {
299
+ return oldContact
300
+ }
260
301
 
261
- await pb.unwrap().close()
302
+ return
303
+ }
262
304
 
263
- if (response.type !== MessageType.PING) {
264
- throw new InvalidMessageError(`Incorrect message type received, expected PING got ${response.type}`)
265
- }
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)
266
309
 
310
+ try {
311
+ return await this.pingContact(oldContact, options)
312
+ } catch {
313
+ this.metrics?.kadBucketEvents.increment({ ping_old_contact_error: true })
267
314
  return true
268
- } catch (err: any) {
269
- if (this.running && this.kb != null) {
270
- // only evict peers if we are still running, otherwise we evict
271
- // when dialing is cancelled due to shutdown in progress
272
- this.log.error('could not ping peer %p', oldContact.peerId, err)
273
- this.log('evicting old contact after ping failed %p', oldContact.peerId)
274
- this.kb.remove(oldContact.kadId)
275
- }
276
-
277
- stream?.abort(err)
278
-
279
- return false
280
315
  } finally {
281
- this.updateMetrics()
316
+ this.pingOldContactTimeout.cleanUp(signal)
317
+ signals.clear()
282
318
  }
283
319
  }, {
284
- peerId: oldContact.peerId
320
+ peerId: oldContact.peerId,
321
+ signal: options?.signal
285
322
  })
323
+
324
+ if (!result) {
325
+ return oldContact
326
+ }
286
327
  })
287
- )
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)
288
341
 
289
- const responded = results
290
- .filter(res => res)
291
- .length
342
+ try {
343
+ const job = this.pingNewContactQueue.find(contact.peerId)
292
344
 
293
- if (this.running && responded < oldContacts.length && this.kb != null) {
294
- this.log('adding new contact %p', newContact.peerId)
295
- 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()
296
369
  }
297
370
  }
298
371
 
299
- // -- 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
+ }
300
401
 
301
402
  /**
302
403
  * Amount of currently stored peers
@@ -313,8 +414,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
313
414
  * Find a specific peer by id
314
415
  */
315
416
  async find (peer: PeerId): Promise<PeerId | undefined> {
316
- const key = await utils.convertPeerId(peer)
317
- return this.kb?.get(key)?.peerId
417
+ const kadId = await utils.convertPeerId(peer)
418
+ return this.kb.get(kadId)?.peerId
318
419
  }
319
420
 
320
421
  /**
@@ -344,18 +445,12 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
344
445
  /**
345
446
  * Add or update the routing table with the given peer
346
447
  */
347
- async add (peerId: PeerId): Promise<void> {
448
+ async add (peerId: PeerId, options?: AbortOptions): Promise<void> {
348
449
  if (this.kb == null) {
349
450
  throw new Error('RoutingTable is not started')
350
451
  }
351
452
 
352
- const kadId = await utils.convertPeerId(peerId)
353
-
354
- this.kb.add({ kadId, peerId })
355
-
356
- this.log.trace('added %p with kad id %b', peerId, kadId)
357
-
358
- this.updateMetrics()
453
+ await this.kb.add(peerId, options)
359
454
  }
360
455
 
361
456
  /**
@@ -366,11 +461,9 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
366
461
  throw new Error('RoutingTable is not started')
367
462
  }
368
463
 
369
- const id = await utils.convertPeerId(peer)
370
-
371
- this.kb.remove(id)
464
+ const kadId = await utils.convertPeerId(peer)
372
465
 
373
- this.updateMetrics()
466
+ await this.kb.remove(kadId)
374
467
  }
375
468
 
376
469
  private updateMetrics (): void {
@@ -381,6 +474,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
381
474
  let size = 0
382
475
  let buckets = 0
383
476
  let maxDepth = 0
477
+ let minOccupancy = 20
478
+ let maxOccupancy = 0
384
479
 
385
480
  function count (bucket: Bucket): void {
386
481
  if (isLeafBucket(bucket)) {
@@ -390,6 +485,15 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
390
485
 
391
486
  buckets++
392
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
+
393
497
  return
394
498
  }
395
499
 
@@ -402,6 +506,8 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
402
506
  this.metrics.routingTableSize.update(size)
403
507
  this.metrics.routingTableKadBucketTotal.update(buckets)
404
508
  this.metrics.routingTableKadBucketAverageOccupancy.update(Math.round(size / buckets))
509
+ this.metrics.routingTableKadBucketMinOccupancy.update(minOccupancy)
510
+ this.metrics.routingTableKadBucketMaxOccupancy.update(maxOccupancy)
405
511
  this.metrics.routingTableKadBucketMaxDepth.update(maxDepth)
406
512
  }
407
513
  }