Skip to content

Commit 753fdea

Browse files
authored
Merge pull request #15047 from dtanders/Fix-MultiItemCache-Stack-Exhaustion
Change the Implementation of get() in MultiItemCache to Prevent Stack Exhaustion
2 parents 149333f + b54f2ac commit 753fdea

2 files changed

Lines changed: 67 additions & 9 deletions

File tree

lib/CacheFacade.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"use strict";
77

8+
const { forEachBail } = require("enhanced-resolve");
89
const asyncLib = require("neo-async");
910
const getLazyHashedEtag = require("./cache/getLazyHashedEtag");
1011
const mergeEtags = require("./cache/mergeEtags");
@@ -46,15 +47,7 @@ class MultiItemCache {
4647
* @returns {void}
4748
*/
4849
get(callback) {
49-
const next = i => {
50-
this._items[i].get((err, result) => {
51-
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-
});
56-
};
57-
next(0);
50+
forEachBail(this._items, (item, callback) => item.get(callback), callback);
5851
}
5952

6053
/**

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).toBeNull();
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)