mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
Short throttle windows can expire between synchronous calls when the process is descheduled. Delayed timer dispatch can also release two queued calls together, invalidating the strict test's spacing assertion. Move timing checks to a separate test with mocked timers and a matching libuv clock. Cover both punctual and delayed dispatch, and keep real async_hooks timer-allocation checks with windows canceled before expiry. Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66034 Refs: https://github.com/nodejs/node/pull/65899 Refs: https://ci.nodejs.org/job/node-test-commit-linux-containered/nodes=ubuntu2404_sharedlibs_openssl111_x64/59189/ Refs: https://ci.nodejs.org/job/node-test-commit-linuxone/nodes=rhel8-s390x/58602/ Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stefan Stojanovic <stefan.stojanovic@janeasystems.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
171 lines
5.7 KiB
JavaScript
171 lines
5.7 KiB
JavaScript
// Flags: --expose-internals
|
|
'use strict';
|
|
|
|
const common = require('../common');
|
|
const assert = require('node:assert');
|
|
const { mock } = require('node:test');
|
|
const { setImmediate } = require('node:timers/promises');
|
|
const { internalBinding } = require('internal/test/binding');
|
|
|
|
// Throttle uses the libuv clock as well as timers. Control both before loading
|
|
// the implementation, which captures setTimeout and clearTimeout.
|
|
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
|
|
mock.method(internalBinding('timers'), 'getLibuvNow', () => Date.now());
|
|
const { throttle } = require('node:util');
|
|
|
|
process.on('unhandledRejection', common.mustNotCall());
|
|
|
|
(async () => {
|
|
{
|
|
const values = [];
|
|
const times = [];
|
|
const start = Date.now();
|
|
const throttled = throttle(common.mustCall(function(value) {
|
|
assert.strictEqual(this, throttled);
|
|
values.push(value);
|
|
times.push(Date.now() - start);
|
|
return value;
|
|
}, 5), 2, 40);
|
|
|
|
assert.strictEqual(typeof throttled.cancel, 'function');
|
|
assert.strictEqual(typeof throttled.hasImmediateCapacity, 'function');
|
|
assert.strictEqual(typeof throttled.ref, 'function');
|
|
assert.strictEqual(typeof throttled.unref, 'function');
|
|
assert.strictEqual(throttled.pending, null);
|
|
assert.strictEqual(throttled.pendingCount, 0);
|
|
assert.strictEqual(throttled.activeCount, 0);
|
|
assert.strictEqual(throttled.hasImmediateCapacity(), true);
|
|
|
|
const first = throttled(1);
|
|
const second = throttled(2);
|
|
const third = throttled(3);
|
|
const fourth = throttled(4);
|
|
const fifth = throttled(5);
|
|
|
|
assert(first instanceof Promise);
|
|
assert(second instanceof Promise);
|
|
assert.notStrictEqual(first, second);
|
|
assert.deepStrictEqual(values, [1, 2]);
|
|
assert.strictEqual(throttled.hasImmediateCapacity(), false);
|
|
assert.strictEqual(throttled.pending, fifth);
|
|
assert.strictEqual(throttled.pendingCount, 3);
|
|
assert.strictEqual(throttled.unref(), throttled);
|
|
assert.strictEqual(throttled.ref(), throttled);
|
|
|
|
mock.timers.tick(39);
|
|
assert.deepStrictEqual(values, [1, 2]);
|
|
mock.timers.tick(1);
|
|
assert.deepStrictEqual(values, [1, 2, 3, 4]);
|
|
assert.strictEqual(throttled.pendingCount, 1);
|
|
mock.timers.tick(39);
|
|
assert.deepStrictEqual(values, [1, 2, 3, 4]);
|
|
mock.timers.tick(1);
|
|
|
|
assert.deepStrictEqual(
|
|
await Promise.all([first, second, third, fourth, fifth]),
|
|
[1, 2, 3, 4, 5],
|
|
);
|
|
assert.deepStrictEqual(values, [1, 2, 3, 4, 5]);
|
|
assert.strictEqual(throttled.pending, null);
|
|
assert.strictEqual(throttled.pendingCount, 0);
|
|
assert.strictEqual(throttled.activeCount, 0);
|
|
assert.deepStrictEqual(times, [0, 0, 40, 40, 80]);
|
|
}
|
|
|
|
{
|
|
const values = [];
|
|
const throttled = throttle(common.mustCall((value) => {
|
|
values.push(value);
|
|
return value;
|
|
}, 2), 1, 20, { maxPending: 1 });
|
|
const first = throttled(1);
|
|
const second = throttled(2);
|
|
const dropped = throttled(3);
|
|
|
|
assert.deepStrictEqual(values, [1]);
|
|
assert.strictEqual(throttled.pendingCount, 1);
|
|
await setImmediate();
|
|
await assert.rejects(dropped, { code: 'ERR_THROTTLED' });
|
|
mock.timers.tick(19);
|
|
assert.deepStrictEqual(values, [1]);
|
|
mock.timers.tick(1);
|
|
assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]);
|
|
assert.deepStrictEqual(values, [1, 2]);
|
|
}
|
|
|
|
{
|
|
const values = [];
|
|
const throttled = throttle(common.mustCall((value) => {
|
|
values.push(value);
|
|
return value;
|
|
}, 3), 2, 30, { overflow: 'drop' });
|
|
const first = throttled(1);
|
|
const second = throttled(2);
|
|
await assert.rejects(throttled(3), { code: 'ERR_THROTTLED' });
|
|
assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]);
|
|
|
|
mock.timers.tick(29);
|
|
assert.strictEqual(throttled.hasImmediateCapacity(), false);
|
|
await assert.rejects(throttled(4), { code: 'ERR_THROTTLED' });
|
|
mock.timers.tick(1);
|
|
assert.strictEqual(throttled.hasImmediateCapacity(), true);
|
|
assert.strictEqual(await throttled(5), 5);
|
|
assert.deepStrictEqual(values, [1, 2, 5]);
|
|
}
|
|
|
|
// A rolling window releases one slot at a time when timers run on schedule.
|
|
// If dispatch is delayed until both slots expire, both calls may run together.
|
|
for (const delayed of [false, true]) {
|
|
const times = [];
|
|
const start = Date.now();
|
|
const throttled = throttle(common.mustCall((value) => {
|
|
times.push(Date.now() - start);
|
|
return value;
|
|
}, 4), 2, 80, { strict: true });
|
|
|
|
const first = throttled(1);
|
|
mock.timers.tick(40);
|
|
const second = throttled(2);
|
|
const third = throttled(3);
|
|
const fourth = throttled(4);
|
|
|
|
mock.timers.tick(39);
|
|
assert.deepStrictEqual(times, [0, 40]);
|
|
if (delayed) {
|
|
mock.timers.tick(41);
|
|
} else {
|
|
mock.timers.tick(1);
|
|
assert.deepStrictEqual(times, [0, 40, 80]);
|
|
mock.timers.tick(39);
|
|
assert.deepStrictEqual(times, [0, 40, 80]);
|
|
mock.timers.tick(1);
|
|
}
|
|
|
|
assert.deepStrictEqual(
|
|
await Promise.all([first, second, third, fourth]),
|
|
[1, 2, 3, 4],
|
|
);
|
|
assert.deepStrictEqual(times, delayed ? [0, 40, 120, 120] : [0, 40, 80, 120]);
|
|
}
|
|
|
|
{
|
|
let recursive;
|
|
const values = [];
|
|
const throttled = throttle(common.mustCall((value) => {
|
|
values.push(value);
|
|
if (value === 1) recursive = throttled(2);
|
|
return value;
|
|
}, 2), 1, 20);
|
|
|
|
const first = throttled(1);
|
|
assert.deepStrictEqual(values, [1]);
|
|
assert.strictEqual(throttled.pending, recursive);
|
|
assert.strictEqual(throttled.pendingCount, 1);
|
|
mock.timers.tick(19);
|
|
assert.deepStrictEqual(values, [1]);
|
|
mock.timers.tick(1);
|
|
assert.deepStrictEqual(await Promise.all([first, recursive]), [1, 2]);
|
|
assert.deepStrictEqual(values, [1, 2]);
|
|
}
|
|
})().then(common.mustCall()).finally(() => mock.reset());
|