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.
- package/index.js +139 -3
- package/lib/top-k.js +94 -0
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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-
|
|
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
|
-
"
|
|
38
|
+
"lunte": "^1.6.0",
|
|
35
39
|
"prettier": "^3.6.2",
|
|
36
40
|
"prettier-config-holepunch": "^2.0.0",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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
|
}
|