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

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/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
  }