crypto: add buffering to randomInt · nodejs/node@960c6be
@@ -2,6 +2,10 @@
2233const {
44 Array,
5+ ArrayPrototypeForEach,
6+ ArrayPrototypePush,
7+ ArrayPrototypeShift,
8+ ArrayPrototypeSplice,
59 BigInt,
610 FunctionPrototypeBind,
711 FunctionPrototypeCall,
@@ -186,6 +190,13 @@ function randomFill(buf, offset, size, callback) {
186190// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
187191const RAND_MAX = 0xFFFF_FFFF_FFFF;
188192193+// Cache random data to use in randomInt. The cache size must be evenly
194+// divisible by 6 because each attempt to obtain a random int uses 6 bytes.
195+const randomCache = new FastBuffer(6 * 1024);
196+let randomCacheOffset = randomCache.length;
197+let asyncCacheFillInProgress = false;
198+const asyncCachePendingTasks = [];
199+189200// Generates an integer in [min, max) range where min is inclusive and max is
190201// exclusive.
191202function randomInt(min, max, callback) {
@@ -230,33 +241,59 @@ function randomInt(min, max, callback) {
230241// than or equal to 0 and less than randLimit.
231242const randLimit = RAND_MAX - (RAND_MAX % range);
232243233-if (isSync) {
234-// Sync API
235-while (true) {
236-const x = randomBytes(6).readUIntBE(0, 6);
237-if (x >= randLimit) {
238-// Try again.
239-continue;
240-}
241-return (x % range) + min;
244+// If we don't have a callback, or if there is still data in the cache, we can
245+// do this synchronously, which is super fast.
246+while (isSync || (randomCacheOffset < randomCache.length)) {
247+if (randomCacheOffset === randomCache.length) {
248+// This might block the thread for a bit, but we are in sync mode.
249+randomFillSync(randomCache);
250+randomCacheOffset = 0;
251+}
252+253+const x = randomCache.readUIntBE(randomCacheOffset, 6);
254+randomCacheOffset += 6;
255+256+if (x < randLimit) {
257+const n = (x % range) + min;
258+if (isSync) return n;
259+process.nextTick(callback, undefined, n);
260+return;
242261}
243-} else {
244-// Async API
245-const pickAttempt = () => {
246-randomBytes(6, (err, bytes) => {
247-if (err) return callback(err);
248-const x = bytes.readUIntBE(0, 6);
249-if (x >= randLimit) {
250-// Try again.
251-return pickAttempt();
252-}
253-const n = (x % range) + min;
254-callback(null, n);
255-});
256-};
257-258-pickAttempt();
259262}
263+264+// At this point, we are in async mode with no data in the cache. We cannot
265+// simply refill the cache, because another async call to randomInt might
266+// already be doing that. Instead, queue this call for when the cache has
267+// been refilled.
268+ArrayPrototypePush(asyncCachePendingTasks, { min, max, callback });
269+asyncRefillRandomIntCache();
270+}
271+272+function asyncRefillRandomIntCache() {
273+if (asyncCacheFillInProgress)
274+return;
275+276+asyncCacheFillInProgress = true;
277+randomFill(randomCache, (err) => {
278+asyncCacheFillInProgress = false;
279+280+const tasks = asyncCachePendingTasks;
281+const errorReceiver = err && ArrayPrototypeShift(tasks);
282+if (!err)
283+randomCacheOffset = 0;
284+285+// Restart all pending tasks. If an error occurred, we only notify a single
286+// callback (errorReceiver) about it. This way, every async call to
287+// randomInt has a chance of being successful, and it avoids complex
288+// exception handling here.
289+ArrayPrototypeForEach(ArrayPrototypeSplice(tasks, 0), (task) => {
290+randomInt(task.min, task.max, task.callback);
291+});
292+293+// This is the only call that might throw, and is therefore done at the end.
294+if (errorReceiver)
295+errorReceiver.callback(err);
296+});
260297}
261298262299