Skip to content

Commit a3eadb1

Browse files
committed
overlarge items should not be added to cache
Fixes a bug where a large item being added to the cache would cause everything to be evicted, including the item being added, but the `sizes` array was not updated properly.
1 parent 7ef678e commit a3eadb1

File tree

3 files changed

+111
-7
lines changed

3 files changed

+111
-7
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ the cache, and automatically evict items in order to stay below
114114
this size. Note that this may result in fewer than `max` items
115115
being stored.
116116

117+
Attempting to add an item to the cache whose calculated size is
118+
greater that this amount will be a no-op. The item will not be
119+
cached, and no other items will be evicted.
120+
117121
Optional, must be a positive integer if provided. Required if
118122
other size tracking features are used.
119123

@@ -134,7 +138,9 @@ argument, and the key is passed as the second argument.
134138
This may be overridden by passing an options object to
135139
`cache.set()`.
136140

137-
Requires `maxSize` to be set.
141+
Requires `maxSize` to be set. If the resulting calculated size
142+
is greater than `maxSize`, then the item will not be added to the
143+
cache.
138144

139145
Deprecated alias: `length`
140146

@@ -437,6 +443,9 @@ the `sizeCalculation` function and just use the specified number
437443
if it is a positive integer, and `noDisposeOnSet` which will
438444
prevent calling a `dispose` function in the case of overwrites.
439445

446+
If the `size` (or return value of `sizeCalculation`) is greater
447+
than `maxSize`, then the item will not be added to the cache.
448+
440449
Will update the recency of the entry.
441450

442451
Returns the cache object.

index.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,10 @@ class LRUCache {
364364
initializeSizeTracking() {
365365
this.calculatedSize = 0
366366
this.sizes = new ZeroArray(this.max)
367-
this.removeItemSize = index =>
368-
(this.calculatedSize -= this.sizes[index])
367+
this.removeItemSize = index => {
368+
this.calculatedSize -= this.sizes[index]
369+
this.sizes[index] = 0
370+
}
369371
this.requireSize = (k, v, size, sizeCalculation) => {
370372
if (!isPosInt(size)) {
371373
if (sizeCalculation) {
@@ -386,7 +388,7 @@ class LRUCache {
386388
}
387389
return size
388390
}
389-
this.addItemSize = (index, v, k, size) => {
391+
this.addItemSize = (index, size) => {
390392
this.sizes[index] = size
391393
const maxSize = this.maxSize - this.sizes[index]
392394
while (this.calculatedSize > maxSize) {
@@ -396,7 +398,7 @@ class LRUCache {
396398
}
397399
}
398400
removeItemSize(index) {}
399-
addItemSize(index, v, k, size) {}
401+
addItemSize(index, size) {}
400402
requireSize(k, v, size, sizeCalculation) {
401403
if (size || sizeCalculation) {
402404
throw new TypeError(
@@ -569,6 +571,10 @@ class LRUCache {
569571
} = {}
570572
) {
571573
size = this.requireSize(k, v, size, sizeCalculation)
574+
// if the item doesn't fit, don't do anything
575+
if (this.maxSize && size > this.maxSize) {
576+
return this
577+
}
572578
let index = this.size === 0 ? undefined : this.keyMap.get(k)
573579
if (index === undefined) {
574580
// addition
@@ -580,7 +586,7 @@ class LRUCache {
580586
this.prev[index] = this.tail
581587
this.tail = index
582588
this.size++
583-
this.addItemSize(index, v, k, size)
589+
this.addItemSize(index, size)
584590
noUpdateTTL = false
585591
} else {
586592
// update
@@ -598,7 +604,7 @@ class LRUCache {
598604
}
599605
this.removeItemSize(index)
600606
this.valList[index] = v
601-
this.addItemSize(index, v, k, size)
607+
this.addItemSize(index, size)
602608
}
603609
this.moveToTail(index)
604610
}

test/size-calculation.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,84 @@
11
import t from 'tap'
22
import LRU from '../'
33

4+
const checkSize = (c:LRU<any,any>) => {
5+
const sizes = (c as unknown as { sizes: number[] }).sizes
6+
const {calculatedSize, maxSize} = c
7+
const sum = [...sizes].reduce((a, b) => a + b, 0)
8+
if (sum !== calculatedSize) {
9+
console.error({sum, calculatedSize, sizes})
10+
throw new Error('calculatedSize does not equal sum of sizes')
11+
}
12+
if (calculatedSize > maxSize) {
13+
throw new Error('max size exceeded')
14+
}
15+
}
16+
417
t.test('store strings, size = length', t => {
518
const c = new LRU<any, string>({
619
max: 100,
720
maxSize: 100,
821
sizeCalculation: n => n.length,
922
})
1023

24+
checkSize(c)
1125
c.set(5, 'x'.repeat(5))
26+
checkSize(c)
1227
c.set(10, 'x'.repeat(10))
28+
checkSize(c)
1329
c.set(20, 'x'.repeat(20))
30+
checkSize(c)
1431
t.equal(c.calculatedSize, 35)
1532
c.delete(20)
33+
checkSize(c)
1634
t.equal(c.calculatedSize, 15)
1735
c.delete(5)
36+
checkSize(c)
1837
t.equal(c.calculatedSize, 10)
1938
c.clear()
39+
checkSize(c)
2040
t.equal(c.calculatedSize, 0)
2141

2242
const s = 'x'.repeat(10)
2343
for (let i = 0; i < 5; i++) {
2444
c.set(i, s)
45+
checkSize(c)
2546
}
2647
t.equal(c.calculatedSize, 50)
2748

2849
// the big item goes in, but triggers a prune
2950
// we don't preemptively prune until we *cross* the max
3051
c.set('big', 'x'.repeat(100))
52+
checkSize(c)
3153
t.equal(c.calculatedSize, 100)
3254
// override the size on set
3355
c.set('big', 'y'.repeat(100), { sizeCalculation: () => 10 })
56+
checkSize(c)
3457
t.equal(c.size, 1)
58+
checkSize(c)
3559
t.equal(c.calculatedSize, 10)
60+
checkSize(c)
3661
c.delete('big')
62+
checkSize(c)
3763
t.equal(c.size, 0)
3864
t.equal(c.calculatedSize, 0)
3965

4066
c.set('repeated', 'i'.repeat(10))
67+
checkSize(c)
4168
c.set('repeated', 'j'.repeat(10))
69+
checkSize(c)
4270
c.set('repeated', 'i'.repeat(10))
71+
checkSize(c)
4372
c.set('repeated', 'j'.repeat(10))
73+
checkSize(c)
4474
c.set('repeated', 'i'.repeat(10))
75+
checkSize(c)
4576
c.set('repeated', 'j'.repeat(10))
77+
checkSize(c)
4678
c.set('repeated', 'i'.repeat(10))
79+
checkSize(c)
4780
c.set('repeated', 'j'.repeat(10))
81+
checkSize(c)
4882
t.equal(c.size, 1)
4983
t.equal(c.calculatedSize, 10)
5084
t.equal(c.get('repeated'), 'j'.repeat(10))
@@ -81,29 +115,84 @@ t.test('bad size calculation fn throws on set()', t => {
81115

82116
t.test('delete while empty, or missing key, is no-op', t => {
83117
const c = new LRU({ max: 5, maxSize: 10, sizeCalculation: () => 2 })
118+
checkSize(c)
84119
c.set(1, 1)
120+
checkSize(c)
85121
t.equal(c.size, 1)
86122
t.equal(c.calculatedSize, 2)
87123
c.clear()
124+
checkSize(c)
88125
t.equal(c.size, 0)
89126
t.equal(c.calculatedSize, 0)
90127
c.delete(1)
128+
checkSize(c)
91129
t.equal(c.size, 0)
92130
t.equal(c.calculatedSize, 0)
93131

94132
c.set(1, 1)
133+
checkSize(c)
95134
c.set(1, 1)
135+
checkSize(c)
96136
c.set(1, 1)
137+
checkSize(c)
97138
t.equal(c.size, 1)
98139
t.equal(c.calculatedSize, 2)
99140
c.delete(99)
141+
checkSize(c)
100142
t.equal(c.size, 1)
101143
t.equal(c.calculatedSize, 2)
102144
c.delete(1)
145+
checkSize(c)
103146
t.equal(c.size, 0)
104147
t.equal(c.calculatedSize, 0)
105148
c.delete(1)
149+
checkSize(c)
106150
t.equal(c.size, 0)
107151
t.equal(c.calculatedSize, 0)
108152
t.end()
109153
})
154+
155+
t.test('large item falls out of cache, sizes are kept correct', t => {
156+
const c = new LRU<number, number>({
157+
maxSize: 10,
158+
sizeCalculation: () => 100,
159+
})
160+
const sizes:number[] = (c as unknown as { sizes: number[] }).sizes
161+
162+
checkSize(c)
163+
t.equal(c.size, 0)
164+
t.equal(c.calculatedSize, 0)
165+
t.same(sizes, [])
166+
167+
c.set(2, 2, { size: 2 })
168+
checkSize(c)
169+
t.equal(c.size, 1)
170+
t.equal(c.calculatedSize, 2)
171+
t.same(sizes, [2])
172+
173+
c.delete(2)
174+
checkSize(c)
175+
t.equal(c.size, 0)
176+
t.equal(c.calculatedSize, 0)
177+
t.same(sizes, [0])
178+
179+
c.set(1, 1)
180+
checkSize(c)
181+
t.equal(c.size, 0)
182+
t.equal(c.calculatedSize, 0)
183+
t.same(sizes, [0])
184+
185+
c.set(3, 3, { size: 3 })
186+
checkSize(c)
187+
t.equal(c.size, 1)
188+
t.equal(c.calculatedSize, 3)
189+
t.same(sizes, [3])
190+
191+
c.set(4, 4)
192+
checkSize(c)
193+
t.equal(c.size, 1)
194+
t.equal(c.calculatedSize, 3)
195+
t.same(sizes, [3])
196+
197+
t.end()
198+
})

0 commit comments

Comments
 (0)