blind-peer 3.2.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 +139 -3
  2. package/lib/top-k.js +94 -0
  3. package/package.json +16 -8
package/index.js CHANGED
@@ -12,10 +12,20 @@ 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
- const { AddCoreEncoding, DeleteCoreEncoding } = require('blind-peer-encodings')
27
+ // Enable Small wants in Hypercore. Must be before anywhere that uses Hypercore
28
+ Hypercore.enable(Hypercore.SMALL_WANTS)
19
29
 
20
30
  class CoreTracker {
21
31
  constructor(blindPeer, core) {
@@ -194,10 +204,13 @@ class BlindPeer extends ReadyResource {
194
204
  maxBytes = 100_000_000_000,
195
205
  enableGc = true,
196
206
  trustedPubKeys,
207
+ routerKey,
208
+ routerPoolOpts,
197
209
  port,
198
210
  announcingInterval = 100,
199
211
  wakeupGcTickTime = null,
200
- replicationLagThreshold = 100
212
+ replicationLagThreshold = 100,
213
+ topK = {}
201
214
  } = {}
202
215
  ) {
203
216
  super()
@@ -224,6 +237,10 @@ class BlindPeer extends ReadyResource {
224
237
  this.announcedCores = new Map()
225
238
  this.replicationLagThreshold = replicationLagThreshold
226
239
 
240
+ this.routerKey = routerKey || null
241
+ this.routerPoolOpts = routerPoolOpts || {}
242
+ this.routerPool = null
243
+
227
244
  this.stats = {
228
245
  bytesGcd: 0,
229
246
  coresAdded: 0,
@@ -233,6 +250,17 @@ class BlindPeer extends ReadyResource {
233
250
  muxerPaired: 0,
234
251
  muxerErrors: 0
235
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)
236
264
  }
237
265
 
238
266
  get encryptionPublicKey() {
@@ -282,6 +310,14 @@ class BlindPeer extends ReadyResource {
282
310
  }
283
311
  this.swarm.on('connection', this._onconnection.bind(this))
284
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
+
285
321
  this._announceCores().catch(safetyCatch) // announcing cores asynchronously
286
322
  this.flushInterval = setInterval(this.flush.bind(this), 10_000)
287
323
  }
@@ -470,6 +506,23 @@ class BlindPeer extends ReadyResource {
470
506
  stream.on('close', () => core.close().catch(safetyCatch))
471
507
  }
472
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
+
473
526
  async _announceCores() {
474
527
  for await (const record of this.db.createAnnouncingCoresStream()) {
475
528
  if (this.closing) return
@@ -572,8 +625,14 @@ class BlindPeer extends ReadyResource {
572
625
 
573
626
  async _onaddcores(stream, request) {
574
627
  this.stats.addCoresRx++
575
- const priority = Math.min(request.priority, 1) // 2 is reserved for trusted peers
628
+
576
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
577
636
 
578
637
  if (request.announce !== false && !this._isTrustedPeer(stream.remotePublicKey)) {
579
638
  // Note: we can't use the original downgrade-announce event because that assumes a 'record' object
@@ -647,6 +706,13 @@ class BlindPeer extends ReadyResource {
647
706
  const discoveryKey = core.discoveryKey
648
707
  await core.close()
649
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
+ })
650
716
  }
651
717
 
652
718
  for (const r of recordsToAdd) this.emit('add-new-core', r, true, stream)
@@ -710,7 +776,13 @@ class BlindPeer extends ReadyResource {
710
776
  }
711
777
 
712
778
  async _close() {
779
+ if (this.routerPool) {
780
+ await this.routerPool.destroy()
781
+ await this.routerPool.statelessRpc.close()
782
+ }
713
783
  clearInterval(this.flushInterval)
784
+ await this.topKByPeer.close()
785
+ await this.topKByReferrer.close()
714
786
  if (this.ownsWakeup) this.wakeup.destroy()
715
787
  if (this.ownsSwarm) await this.swarm.destroy()
716
788
  await this.flush()
@@ -819,6 +891,70 @@ class BlindPeer extends ReadyResource {
819
891
  this.set(self.stats.addCoresRx)
820
892
  }
821
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
+ })
908
+ if (self.rocks.stats) {
909
+ new promClient.Gauge({
910
+ // eslint-disable-line no-new
911
+ name: 'blind_peer_rocks_gets',
912
+ help: 'The amount of get ops from RocksDB',
913
+ collect() {
914
+ this.set(self.rocks.stats.gets)
915
+ }
916
+ })
917
+ new promClient.Gauge({
918
+ // eslint-disable-line no-new
919
+ name: 'blind_peer_rocks_puts',
920
+ help: 'The amount of put ops to RocksDB',
921
+ collect() {
922
+ this.set(self.rocks.stats.puts)
923
+ }
924
+ })
925
+ new promClient.Gauge({
926
+ // eslint-disable-line no-new
927
+ name: 'blind_peer_rocks_deletes',
928
+ help: 'The amount of delete ops from RocksDB',
929
+ collect() {
930
+ this.set(self.rocks.stats.deletes)
931
+ }
932
+ })
933
+ new promClient.Gauge({
934
+ // eslint-disable-line no-new
935
+ name: 'blind_peer_rocks_range_deletes',
936
+ help: 'The amount of range delete ops from RocksDB',
937
+ collect() {
938
+ this.set(self.rocks.stats.rangeDeletes)
939
+ }
940
+ })
941
+ new promClient.Gauge({
942
+ // eslint-disable-line no-new
943
+ name: 'blind_peer_rocks_read_batches',
944
+ help: 'The amount of read batches from RocksDB',
945
+ collect() {
946
+ this.set(self.rocks.stats.readBatches)
947
+ }
948
+ })
949
+ new promClient.Gauge({
950
+ // eslint-disable-line no-new
951
+ name: 'blind_peer_rocks_write_batches',
952
+ help: 'The amount of write batches to RocksDB',
953
+ collect() {
954
+ this.set(self.rocks.stats.writeBatches)
955
+ }
956
+ })
957
+ }
822
958
  }
823
959
  }
824
960
 
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,22 +1,25 @@
1
1
  {
2
2
  "name": "blind-peer",
3
- "version": "3.2.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",
13
- "hypercore": "^11.0.12",
13
+ "hypercore": "^11.26.0",
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
  }