mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 05:38:47 +02:00

PR-URL: https://github.com/nodejs/node/pull/58666 Fixes: https://github.com/nodejs/node/pull/36502 Refs: https://w3c.github.io/web-locks Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
222 lines
7.4 KiB
JavaScript
222 lines
7.4 KiB
JavaScript
'use strict';
|
|
// Flags: --expose-gc
|
|
|
|
const common = require('../common');
|
|
const { describe, it } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const { Worker } = require('node:worker_threads');
|
|
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
|
|
describe('Web Locks with worker threads', () => {
|
|
it('should handle exclusive locks', async () => {
|
|
const worker = new Worker(`
|
|
const { parentPort } = require('worker_threads');
|
|
const assert = require('node:assert');
|
|
|
|
navigator.locks.request('exclusive-test', async (lock) => {
|
|
assert.strictEqual(lock.mode, 'exclusive');
|
|
parentPort.postMessage({ success: true });
|
|
}).catch(err => parentPort.postMessage({ error: err.message }));
|
|
`, { eval: true });
|
|
|
|
const result = await new Promise((resolve) => {
|
|
worker.once('message', resolve);
|
|
});
|
|
|
|
assert.strictEqual(result.success, true);
|
|
await worker.terminate();
|
|
|
|
await navigator.locks.request('exclusive-test', async (lock) => {
|
|
assert.strictEqual(lock.mode, 'exclusive');
|
|
assert.strictEqual(lock.name, 'exclusive-test');
|
|
});
|
|
});
|
|
|
|
it('should handle shared locks', async () => {
|
|
const worker = new Worker(`
|
|
const { parentPort } = require('worker_threads');
|
|
const assert = require('node:assert');
|
|
|
|
navigator.locks.request('shared-test', { mode: 'shared' }, async (lock) => {
|
|
assert.strictEqual(lock.mode, 'shared');
|
|
parentPort.postMessage({ success: true });
|
|
}).catch(err => parentPort.postMessage({ error: err.message }));
|
|
`, { eval: true });
|
|
|
|
const result = await new Promise((resolve) => {
|
|
worker.once('message', resolve);
|
|
});
|
|
assert.strictEqual(result.success, true);
|
|
|
|
await navigator.locks.request('shared-test', { mode: 'shared' }, async (lock1) => {
|
|
await navigator.locks.request('shared-test', { mode: 'shared' }, async (lock2) => {
|
|
assert.strictEqual(lock1.mode, 'shared');
|
|
assert.strictEqual(lock2.mode, 'shared');
|
|
});
|
|
});
|
|
|
|
await worker.terminate();
|
|
});
|
|
|
|
it('should handle steal option - no existing lock', async () => {
|
|
await navigator.locks.request('steal-simple', { steal: true }, async (lock) => {
|
|
assert.strictEqual(lock.name, 'steal-simple');
|
|
assert.strictEqual(lock.mode, 'exclusive');
|
|
});
|
|
});
|
|
|
|
it('should handle steal option - existing lock', async () => {
|
|
let originalLockRejected = false;
|
|
|
|
const originalLockPromise = navigator.locks.request('steal-target', async (lock) => {
|
|
assert.strictEqual(lock.name, 'steal-target');
|
|
return 'original-completed';
|
|
}).catch((err) => {
|
|
originalLockRejected = true;
|
|
assert.strictEqual(err.name, 'AbortError');
|
|
assert.strictEqual(err.message, 'The operation was aborted');
|
|
return 'original-rejected';
|
|
});
|
|
|
|
const stealResult = await navigator.locks.request('steal-target', { steal: true }, async (stolenLock) => {
|
|
assert.strictEqual(stolenLock.name, 'steal-target');
|
|
assert.strictEqual(stolenLock.mode, 'exclusive');
|
|
return 'steal-completed';
|
|
});
|
|
|
|
assert.strictEqual(stealResult, 'steal-completed');
|
|
|
|
const originalResult = await originalLockPromise;
|
|
assert.strictEqual(originalLockRejected, true);
|
|
assert.strictEqual(originalResult, 'original-rejected');
|
|
});
|
|
|
|
it('should handle ifAvailable option', async () => {
|
|
await navigator.locks.request('ifavailable-test', async () => {
|
|
const result = await navigator.locks.request('ifavailable-test', { ifAvailable: true }, (lock) => {
|
|
return lock; // should be null
|
|
});
|
|
|
|
assert.strictEqual(result, null);
|
|
|
|
const availableResult = await navigator.locks.request('ifavailable-different-resource',
|
|
{ ifAvailable: true }, (lock) => {
|
|
return lock !== null;
|
|
});
|
|
|
|
assert.strictEqual(availableResult, true);
|
|
});
|
|
});
|
|
|
|
it('should handle AbortSignal', async () => {
|
|
const worker = new Worker(`
|
|
const { parentPort } = require('worker_threads');
|
|
const assert = require('node:assert');
|
|
|
|
const controller = new AbortController();
|
|
|
|
navigator.locks.request('signal-after-grant', { signal: controller.signal }, async (lock) => {
|
|
parentPort.postMessage({ acquired: true });
|
|
|
|
setTimeout(() => controller.abort(), 50);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
return 'completed successfully';
|
|
}).then(result => {
|
|
parentPort.postMessage({ resolved: result });
|
|
}).catch(err => {
|
|
parentPort.postMessage({ rejected: err.name });
|
|
});
|
|
`, { eval: true });
|
|
|
|
const acquired = await new Promise((resolve) => {
|
|
worker.once('message', resolve);
|
|
});
|
|
assert.strictEqual(acquired.acquired, true);
|
|
|
|
const result = await new Promise((resolve) => {
|
|
worker.once('message', resolve);
|
|
});
|
|
assert.strictEqual(result.resolved, 'completed successfully');
|
|
|
|
await worker.terminate();
|
|
});
|
|
|
|
it('should handle many concurrent locks without hanging', async () => {
|
|
if (global.gc) global.gc();
|
|
const before = process.memoryUsage().rss;
|
|
|
|
let callbackCount = 0;
|
|
let resolveCount = 0;
|
|
|
|
const promises = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
const promise = navigator.locks.request(`test-${i}`, async (lock) => {
|
|
callbackCount++;
|
|
const innerPromise = navigator.locks.request(`inner-${i}`, async () => {
|
|
resolveCount++;
|
|
return 'done';
|
|
});
|
|
await innerPromise;
|
|
return `completed-${lock.name}`;
|
|
});
|
|
|
|
promises.push(promise);
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
if (global.gc) global.gc();
|
|
|
|
const after = process.memoryUsage().rss;
|
|
|
|
assert.strictEqual(callbackCount, 100);
|
|
assert.strictEqual(resolveCount, 100);
|
|
assert(after < before * 3);
|
|
});
|
|
|
|
it('should preserve AsyncLocalStorage context across lock callback', async () => {
|
|
const als = new AsyncLocalStorage();
|
|
const store = { id: 'lock' };
|
|
|
|
als.run(store, () => {
|
|
navigator.locks
|
|
.request('als-context-test', async () => {
|
|
assert.strictEqual(als.getStore(), store);
|
|
})
|
|
.then(common.mustCall());
|
|
});
|
|
});
|
|
|
|
it('should clean up when worker is terminated with a pending lock', async () => {
|
|
// Acquire the lock in the main thread so that the worker's request will be pending
|
|
await navigator.locks.request('cleanup-test', async () => {
|
|
// Launch a worker that requests the same lock
|
|
const worker = new Worker(`
|
|
const { parentPort } = require('worker_threads');
|
|
|
|
parentPort.postMessage({ requesting: true });
|
|
|
|
navigator.locks.request('cleanup-test', async () => {
|
|
return 'should-not-complete';
|
|
}).catch(err => {
|
|
parentPort.postMessage({ error: err.name });
|
|
});
|
|
`, { eval: true });
|
|
|
|
const requestSignal = await new Promise((resolve) => {
|
|
worker.once('message', resolve);
|
|
});
|
|
|
|
assert.strictEqual(requestSignal.requesting, true);
|
|
|
|
await worker.terminate();
|
|
|
|
});
|
|
|
|
// Request the lock again to make sure cleanup succeeded
|
|
await navigator.locks.request('cleanup-test', async (lock) => {
|
|
assert.strictEqual(lock.name, 'cleanup-test');
|
|
});
|
|
});
|
|
});
|