Skip to content

Commit 8f0ba37

Browse files
authored
fix: TimeCache handle key collision to prevent leak (#358)
* TimeCache handle key collision to prevent leak * Move unit tests into time-cache
1 parent 4c158cd commit 8f0ba37

File tree

4 files changed

+49
-3
lines changed

4 files changed

+49
-3
lines changed

src/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1140,7 +1140,12 @@ export class GossipSub extends EventEmitter<GossipsubEvents> implements PubSub<G
11401140
const messageId = { msgId, msgIdStr }
11411141

11421142
// Add the message to the duplicate caches
1143-
if (fastMsgIdStr !== undefined) this.fastMsgIdCache?.put(fastMsgIdStr, msgIdStr)
1143+
if (fastMsgIdStr !== undefined && this.fastMsgIdCache) {
1144+
const collision = this.fastMsgIdCache.put(fastMsgIdStr, msgIdStr)
1145+
if (collision) {
1146+
this.metrics?.fastMsgIdCacheCollision.inc()
1147+
}
1148+
}
11441149

11451150
if (this.seenCache.has(msgIdStr)) {
11461151
return { code: MessageStatus.duplicate, msgIdStr }

src/metrics.ts

+5
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,11 @@ export function getMetrics(
498498
help: 'Current mcache msg count not validated'
499499
}),
500500

501+
fastMsgIdCacheCollision: register.gauge({
502+
name: 'gossipsub_fastmsgid_cache_collision_total',
503+
help: 'Total count of key collisions on fastmsgid cache put'
504+
}),
505+
501506
topicStrToLabel: topicStrToLabel,
502507

503508
toTopic(topicStr: TopicStr): TopicLabel {

src/utils/time-cache.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,19 @@ export class SimpleTimeCache<T> {
2727
return this.entries.size
2828
}
2929

30-
put(key: string | number, value: T): void {
30+
/** Returns true if there was a key collision and the entry is dropped */
31+
put(key: string | number, value: T): boolean {
32+
if (this.entries.has(key)) {
33+
// Key collisions break insertion order in the entries cache, which break prune logic.
34+
// prune relies on each iterated entry to have strictly ascending validUntilMs, else it
35+
// won't prune expired entries and SimpleTimeCache will grow unexpectedly.
36+
// As of Oct 2022 NodeJS v16, inserting the same key twice with different value does not
37+
// change the key position in the iterator stream. A unit test asserts this behaviour.
38+
return true
39+
}
40+
3141
this.entries.set(key, { value, validUntilMs: Date.now() + this.validityMs })
42+
return false
3243
}
3344

3445
prune(): void {
@@ -38,7 +49,8 @@ export class SimpleTimeCache<T> {
3849
if (v.validUntilMs < now) {
3950
this.entries.delete(k)
4051
} else {
41-
// sort by insertion order
52+
// Entries are inserted with strictly ascending validUntilMs.
53+
// Stop early to save iterations
4254
break
4355
}
4456
}

test/time-cache.spec.ts

+24
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,28 @@ describe('SimpleTimeCache', () => {
4040
expect(timeCache.has('bFirst')).to.be.false()
4141
expect(timeCache.has('cFirst')).to.be.false()
4242
})
43+
44+
it('Map insertion order', () => {
45+
const key1 = 'key1'
46+
const key2 = 'key2'
47+
const key3 = 'key3'
48+
49+
const map = new Map<string, number>()
50+
map.set(key1, Date.now())
51+
map.set(key2, Date.now())
52+
map.set(key3, Date.now())
53+
54+
expect(Array.from(map.keys())).deep.equals([key1, key2, key3], 'Map iterator order')
55+
56+
// Does not change key position
57+
map.set(key2, Date.now())
58+
59+
expect(Array.from(map.keys())).deep.equals([key1, key2, key3], 'Map iterator order after re-set')
60+
61+
// Changes key position
62+
map.delete(key2)
63+
map.set(key2, Date.now())
64+
65+
expect(Array.from(map.keys())).deep.equals([key1, key3, key2], 'Map iterator order after delete set')
66+
})
4367
})

0 commit comments

Comments
 (0)