Skip to content

Commit 9c6557e

Browse files
committed
Change the way the MultiItemCache iterates over items to prevent stack exhaustion when it has a large number of items.
Added unit tests for MultiItemCache behavior, including one for covering the large item count case.
1 parent ccecc17 commit 9c6557e

2 files changed

Lines changed: 80 additions & 7 deletions

File tree

lib/CacheFacade.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,23 @@ class MultiItemCache {
4646
* @returns {void}
4747
*/
4848
get(callback) {
49-
const next = i => {
50-
this._items[i].get((err, result) => {
49+
let i = 0;
50+
let resultFound = false;
51+
const generateGetCallback = (/** @type {Boolean} */ lastItem) => {
52+
return (
53+
/** @type {import("./WebpackError")} */ err,
54+
/** @type {T} */ res
55+
) => {
56+
if (resultFound) return;
57+
resultFound = !!(err || res || false);
5158
if (err) return callback(err);
52-
if (result !== undefined) return callback(null, result);
53-
if (++i >= this._items.length) return callback();
54-
next(i);
55-
});
59+
if (res !== undefined) return callback(null, res);
60+
if (lastItem) return callback();
61+
};
5662
};
57-
next(0);
63+
do {
64+
this._items[i].get(generateGetCallback(i === this._items.length - 1));
65+
} while (++i < this._items.length);
5866
}
5967

6068
/**

test/MultiItemCache.unittest.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use strict";
2+
3+
const Cache = require("../lib/Cache");
4+
const { ItemCacheFacade, MultiItemCache } = require("../lib/CacheFacade");
5+
6+
describe("MultiItemCache", () => {
7+
it("Throws when getting items from an empty Cache", () => {
8+
const multiItemCache = new MultiItemCache(generateItemCaches(0));
9+
expect(() => multiItemCache.get(_ => _())).toThrowError();
10+
});
11+
12+
it("Returns the single ItemCacheFacade when passed an array of length 1", () => {
13+
const itemCaches = generateItemCaches(1);
14+
const multiItemCache = new MultiItemCache(itemCaches);
15+
expect(multiItemCache).toBe(itemCaches[0]);
16+
});
17+
18+
it("Retrieves items from the underlying Cache when get is called", () => {
19+
const itemCaches = generateItemCaches(10);
20+
const multiItemCache = new MultiItemCache(itemCaches);
21+
const callback = (err, res) => {
22+
expect(err).toBeNull();
23+
expect(res).toBeInstanceOf(Object);
24+
};
25+
for (let i = 0; i < 10; ++i) {
26+
multiItemCache.get(callback);
27+
}
28+
});
29+
30+
it("Can get() a large number of items without exhausting the stack", () => {
31+
const itemCaches = generateItemCaches(10000, () => undefined);
32+
const multiItemCache = new MultiItemCache(itemCaches);
33+
let callbacks = 0;
34+
const callback = (err, res) => {
35+
expect(err).toBeUndefined();
36+
expect(res).toBeUndefined();
37+
++callbacks;
38+
};
39+
multiItemCache.get(callback);
40+
expect(callbacks).toEqual(1);
41+
});
42+
43+
function generateItemCaches(howMany, dataGenerator) {
44+
const ret = [];
45+
for (let i = 0; i < howMany; ++i) {
46+
const name = `ItemCache${i}`;
47+
const tag = `ItemTag${i}`;
48+
const dataGen =
49+
dataGenerator ||
50+
(() => {
51+
return { name: tag };
52+
});
53+
const cache = new Cache();
54+
cache.hooks.get.tapAsync(
55+
"DataReturner",
56+
(_identifier, _etag, _gotHandlers, callback) => {
57+
callback(undefined, dataGen());
58+
}
59+
);
60+
const itemCache = new ItemCacheFacade(cache, name, tag);
61+
ret[i] = itemCache;
62+
}
63+
return ret;
64+
}
65+
});

0 commit comments

Comments
 (0)