blind-peer 3.4.0 → 3.5.0

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 (3) hide show
  1. package/index.js +87 -4
  2. package/lib/top-k.js +94 -0
  3. package/package.json +15 -7
package/index.js CHANGED
@@ -12,14 +12,21 @@ const safetyCatch = require('safety-catch')
12
12
  const Wakeup = require('protomux-wakeup')
13
13
  const ScopeLock = require('scope-lock')
14
14
  const IdEnc = require('hypercore-id-encoding')
15
+ const ProtomuxRpcClientPool = require('protomux-rpc-client-pool')
16
+ const ProtomuxRpcClient = require('protomux-rpc-client')
17
+ const {
18
+ AddCoreEncoding,
19
+ DeleteCoreEncoding,
20
+ RouterResolvePeersRequest,
21
+ RouterResolvePeersResponse
22
+ } = require('blind-peer-encodings')
15
23
 
16
24
  const BlindPeerDB = require('./lib/db.js')
25
+ const TopKWindow = require('./lib/top-k.js')
17
26
 
18
27
  // Enable Small wants in Hypercore. Must be before anywhere that uses Hypercore
19
28
  Hypercore.enable(Hypercore.SMALL_WANTS)
20
29
 
21
- const { AddCoreEncoding, DeleteCoreEncoding } = require('blind-peer-encodings')
22
-
23
30
  class CoreTracker {
24
31
  constructor(blindPeer, core) {
25
32
  this.blindPeer = blindPeer
@@ -197,10 +204,13 @@ class BlindPeer extends ReadyResource {
197
204
  maxBytes = 100_000_000_000,
198
205
  enableGc = true,
199
206
  trustedPubKeys,
207
+ routerKey,
208
+ routerPoolOpts,
200
209
  port,
201
210
  announcingInterval = 100,
202
211
  wakeupGcTickTime = null,
203
- replicationLagThreshold = 100
212
+ replicationLagThreshold = 100,
213
+ topK = {}
204
214
  } = {}
205
215
  ) {
206
216
  super()
@@ -227,6 +237,10 @@ class BlindPeer extends ReadyResource {
227
237
  this.announcedCores = new Map()
228
238
  this.replicationLagThreshold = replicationLagThreshold
229
239
 
240
+ this.routerKey = routerKey || null
241
+ this.routerPoolOpts = routerPoolOpts || {}
242
+ this.routerPool = null
243
+
230
244
  this.stats = {
231
245
  bytesGcd: 0,
232
246
  coresAdded: 0,
@@ -236,6 +250,17 @@ class BlindPeer extends ReadyResource {
236
250
  muxerPaired: 0,
237
251
  muxerErrors: 0
238
252
  }
253
+
254
+ const {
255
+ bucketCount = 6,
256
+ bucketTime = 10_000,
257
+ k = 5,
258
+ peerThreshold = 100,
259
+ referrerThreshold = 100
260
+ } = topK
261
+
262
+ this.topKByPeer = new TopKWindow(bucketCount, bucketTime, k, peerThreshold)
263
+ this.topKByReferrer = new TopKWindow(bucketCount, bucketTime, k, referrerThreshold)
239
264
  }
240
265
 
241
266
  get encryptionPublicKey() {
@@ -285,6 +310,14 @@ class BlindPeer extends ReadyResource {
285
310
  }
286
311
  this.swarm.on('connection', this._onconnection.bind(this))
287
312
 
313
+ if (this.routerKey) {
314
+ const rpcClient = new ProtomuxRpcClient(this.swarm.dht)
315
+ this.routerPool = new ProtomuxRpcClientPool([this.routerKey], rpcClient, this.routerPoolOpts)
316
+ }
317
+
318
+ await this.topKByPeer.ready()
319
+ await this.topKByReferrer.ready()
320
+
288
321
  this._announceCores().catch(safetyCatch) // announcing cores asynchronously
289
322
  this.flushInterval = setInterval(this.flush.bind(this), 10_000)
290
323
  }
@@ -473,6 +506,23 @@ class BlindPeer extends ReadyResource {
473
506
  stream.on('close', () => core.close().catch(safetyCatch))
474
507
  }
475
508
 
509
+ async _resolvePeers(key) {
510
+ if (!this.routerPool) return
511
+
512
+ const result = await this.routerPool.makeRequest(
513
+ 'resolve-peers',
514
+ { key },
515
+ {
516
+ requestEncoding: RouterResolvePeersRequest,
517
+ responseEncoding: RouterResolvePeersResponse
518
+ }
519
+ )
520
+
521
+ this.emit('resolve-peers', { key, result })
522
+
523
+ return result
524
+ }
525
+
476
526
  async _announceCores() {
477
527
  for await (const record of this.db.createAnnouncingCoresStream()) {
478
528
  if (this.closing) return
@@ -575,8 +625,14 @@ class BlindPeer extends ReadyResource {
575
625
 
576
626
  async _onaddcores(stream, request) {
577
627
  this.stats.addCoresRx++
578
- const priority = Math.min(request.priority, 1) // 2 is reserved for trusted peers
628
+
579
629
  const { cores, referrer } = request
630
+ if (referrer) {
631
+ this.topKByReferrer.hit(IdEnc.normalize(referrer))
632
+ }
633
+ this.topKByPeer.hit(IdEnc.normalize(stream.remotePublicKey))
634
+
635
+ const priority = Math.min(request.priority, 1) // 2 is reserved for trusted peers
580
636
 
581
637
  if (request.announce !== false && !this._isTrustedPeer(stream.remotePublicKey)) {
582
638
  // Note: we can't use the original downgrade-announce event because that assumes a 'record' object
@@ -650,6 +706,13 @@ class BlindPeer extends ReadyResource {
650
706
  const discoveryKey = core.discoveryKey
651
707
  await core.close()
652
708
  await this._onwakeup(discoveryKey, muxer)
709
+
710
+ // TODO: will process the result in V2
711
+ // TODO: handle no referrer
712
+ this._resolvePeers(referrer).catch((err) => {
713
+ safetyCatch(err)
714
+ this.emit('resolve-peers-error', { key: referrer, error: err })
715
+ })
653
716
  }
654
717
 
655
718
  for (const r of recordsToAdd) this.emit('add-new-core', r, true, stream)
@@ -713,7 +776,13 @@ class BlindPeer extends ReadyResource {
713
776
  }
714
777
 
715
778
  async _close() {
779
+ if (this.routerPool) {
780
+ await this.routerPool.destroy()
781
+ await this.routerPool.statelessRpc.close()
782
+ }
716
783
  clearInterval(this.flushInterval)
784
+ await this.topKByPeer.close()
785
+ await this.topKByReferrer.close()
717
786
  if (this.ownsWakeup) this.wakeup.destroy()
718
787
  if (this.ownsSwarm) await this.swarm.destroy()
719
788
  await this.flush()
@@ -822,6 +891,20 @@ class BlindPeer extends ReadyResource {
822
891
  this.set(self.stats.addCoresRx)
823
892
  }
824
893
  })
894
+ new promClient.Gauge({
895
+ name: 'blind_peer_add_cores_top5_by_remote_key',
896
+ help: 'The total number of requests from the top 5 peers in the last minute',
897
+ collect() {
898
+ this.set(self.topKByPeer.topKSum())
899
+ }
900
+ })
901
+ new promClient.Gauge({
902
+ name: 'blind_peer_add_cores_top5_by_referrer',
903
+ help: 'The total number of requests from the top 5 referrers in the last minute',
904
+ collect() {
905
+ this.set(self.topKByReferrer.topKSum())
906
+ }
907
+ })
825
908
  if (self.rocks.stats) {
826
909
  new promClient.Gauge({
827
910
  // eslint-disable-line no-new
package/lib/top-k.js ADDED
@@ -0,0 +1,94 @@
1
+ const ReadyResource = require('ready-resource')
2
+
3
+ /**
4
+ * Tracks the most frequent keys within a rolling time window.
5
+ */
6
+ class TopKWindow extends ReadyResource {
7
+ /**
8
+ * @param {number} [bucketCount=6]
9
+ * @param {number} [bucketTime=10_000]
10
+ * @param {number} [k=5]
11
+ * @param {number | null} [spikeThreshold=null]
12
+ */
13
+ constructor(bucketCount = 6, bucketTime = 10_000, k = 5, spikeThreshold = null) {
14
+ super()
15
+
16
+ this.bucketCount = bucketCount
17
+ this.bucketTime = bucketTime
18
+ this.k = k
19
+ this.spikeThreshold = spikeThreshold
20
+
21
+ /** @type {Map<string, number>[]} */
22
+ this._buckets = Array.from({ length: bucketCount }, () => new Map())
23
+ this._index = 0
24
+ this.topK = []
25
+ this._timer = null
26
+ }
27
+
28
+ /**
29
+ * @param {string} key
30
+ */
31
+ hit(key) {
32
+ const bucket = this._buckets[this._index]
33
+ bucket.set(key, (bucket.get(key) || 0) + 1)
34
+ }
35
+
36
+ /**
37
+ * @returns {number}
38
+ */
39
+ topKSum() {
40
+ let sum = 0
41
+ for (const { count } of this.topK) {
42
+ sum += count
43
+ }
44
+ return sum
45
+ }
46
+
47
+ async _open() {
48
+ this._timer = setInterval(this._rotate.bind(this), this.bucketTime)
49
+ }
50
+
51
+ async _close() {
52
+ if (this._timer !== null) {
53
+ clearInterval(this._timer)
54
+ this._timer = null
55
+ }
56
+
57
+ this.topK = []
58
+ for (const bucket of this._buckets) {
59
+ bucket.clear()
60
+ }
61
+ }
62
+
63
+ _rotate() {
64
+ // compute the top-k and cache
65
+ const totals = new Map()
66
+ for (const bucket of this._buckets) {
67
+ for (const [key, count] of bucket) {
68
+ totals.set(key, (totals.get(key) || 0) + count)
69
+ }
70
+ }
71
+
72
+ this.topK = [...totals.entries()]
73
+ .sort((a, b) => b[1] - a[1])
74
+ .slice(0, this.k)
75
+ .map(([key, count]) => ({ key, count }))
76
+
77
+ if (this.spikeThreshold !== null) {
78
+ // Deliberately emit only for cached top-k entries to keep spike volume bounded.
79
+ for (const { key, count } of this.topK) {
80
+ if (count >= this.spikeThreshold) {
81
+ this.emit('spike', key, count)
82
+ }
83
+ }
84
+ }
85
+
86
+ // rotate current bucket and clear the old bucket
87
+ this._index = (this._index + 1) % this.bucketCount
88
+ this._buckets[this._index].clear()
89
+
90
+ this.emit('rotated')
91
+ }
92
+ }
93
+
94
+ module.exports = TopKWindow
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "blind-peer",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Blind peers help keep hypercores available",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "autobase": "^7.0.18",
8
8
  "b4a": "^1.6.7",
9
- "blind-peer-encodings": "^3.1.1",
9
+ "blind-peer-encodings": "^3.2.0",
10
10
  "blind-peer-muxer": "^1.0.0",
11
11
  "compact-encoding": "^2.16.0",
12
12
  "corestore": "^7.4.4",
@@ -14,9 +14,12 @@
14
14
  "hypercore-crypto": "^3.5.0",
15
15
  "hypercore-id-encoding": "^1.3.0",
16
16
  "hyperdb": "^5.0.0",
17
+ "hyperdht": "^6.29.0",
17
18
  "hyperschema": "^1.10.3",
18
19
  "hyperswarm": "^4.13.1",
19
20
  "protomux-rpc": "^1.7.1",
21
+ "protomux-rpc-client": "^2.1.0",
22
+ "protomux-rpc-client-pool": "^2.1.0",
20
23
  "protomux-wakeup": "^2.9.0",
21
24
  "ready-resource": "^1.1.2",
22
25
  "repl-swarm": "^2.3.0",
@@ -28,14 +31,15 @@
28
31
  "bare-events": "^2.8.2",
29
32
  "bare-process": "^4.2.2",
30
33
  "bare-prom-client": "^15.1.3",
31
- "blind-peering": "^2.0.0",
34
+ "blind-peer-router": "^0.2.2",
35
+ "blind-peering": "^2.0.1",
32
36
  "brittle": "^3.7.0",
33
37
  "debounceify": "^1.1.0",
34
- "hyperdht": "^6.20.1",
38
+ "lunte": "^1.6.0",
35
39
  "prettier": "^3.6.2",
36
40
  "prettier-config-holepunch": "^2.0.0",
37
- "test-tmp": "^1.3.0",
38
- "which-runtime": "^1.3.2"
41
+ "protomux-rpc-router": "^1.2.0",
42
+ "test-tmp": "^1.3.0"
39
43
  },
40
44
  "files": [
41
45
  "index.js",
@@ -44,7 +48,7 @@
44
48
  "scripts": {
45
49
  "format": "prettier --write .",
46
50
  "format:check": "prettier --check .",
47
- "test": "npm run format:check && brittle test/*.js",
51
+ "test": "npm run format:check && lunte && brittle test/*.js",
48
52
  "test:bare": "bare test/test.js"
49
53
  },
50
54
  "repository": {
@@ -61,6 +65,10 @@
61
65
  "events": {
62
66
  "bare": "bare-events",
63
67
  "default": "events"
68
+ },
69
+ "prom-client": {
70
+ "bare": "bare-prom-client",
71
+ "default": "prom-client"
64
72
  }
65
73
  }
66
74
  }