atomics-sync
Version:
JavaScript multithreading synchronization library
115 lines (96 loc) • 3.7 kB
JavaScript
import assert from "node:assert";
import { describe, it } from "node:test";
import { Worker } from "worker_threads";
import { DeadlockError, INT32_MIN_VALUE, InvalidError, Mutex, PermissionError } from "../dist/index.js";
describe("Mutex", () => {
const OWNER_EMPTY = 0;
const STATE_UNLOCKED = 0;
const INDEX_STATE = 0;
const INDEX_OWNER = 1;
it("should initialize as Int32Array with size 2 in unlocked state and empty owner", () => {
const mutex = Mutex.init();
assert.ok(mutex instanceof Int32Array);
assert.equal(mutex.length, 2);
assert.equal(Atomics.load(mutex, INDEX_STATE), STATE_UNLOCKED);
assert.equal(Atomics.load(mutex, INDEX_OWNER), OWNER_EMPTY);
});
it("should lock and unlock correctly", () => {
const mutex = Mutex.init();
Mutex.lock(mutex, 123);
assert.equal(Atomics.load(mutex, 1), 123);
Mutex.unlock(mutex, 123);
assert.equal(Atomics.load(mutex, INDEX_STATE), STATE_UNLOCKED);
});
it("should throw DeadlockError when same thread tries to lock twice", () => {
const mutex = Mutex.init();
Mutex.lock(mutex, 123);
assert.throws(() => Mutex.lock(mutex, 123), DeadlockError);
});
it("should throw when unlocking already unlocked mutex", () => {
const mutex = Mutex.init();
assert.throws(() => Mutex.unlock(mutex, 123), PermissionError);
});
it("should throw PermissionError when non-owner tries to unlock", () => {
const mutex = Mutex.init();
Mutex.lock(mutex, 123);
assert.throws(() => Mutex.unlock(mutex, 456), PermissionError);
});
it("should throw InvalidError for non-integer threadId", () => {
const mutex = Mutex.init();
assert.throws(() => Mutex.lock(mutex, "invalid_threadId"), InvalidError);
});
it("should throw InvalidError for empty owner", () => {
const mutex = Mutex.init();
assert.throws(() => Mutex.lock(mutex, OWNER_EMPTY), InvalidError);
});
it("should throw RangeError for out of range threadId", () => {
const mutex = Mutex.init();
assert.throws(() => Mutex.lock(mutex, INT32_MIN_VALUE - 1), RangeError);
});
it("tryLock should return true when mutex is free", () => {
const mutex = Mutex.init();
assert.equal(Mutex.tryLock(mutex, 123), true);
});
it("tryLock should return false when mutex is locked", () => {
const mutex = Mutex.init();
Mutex.lock(mutex, 123);
assert.equal(Mutex.tryLock(mutex, 456), false);
});
it("lock/unlock should be fast", () => {
const mutex = Mutex.init();
const start = performance.now();
for (let i = 0; i < 10000; i++) {
Mutex.lock(mutex, 123);
Mutex.unlock(mutex, 123);
}
const duration = performance.now() - start;
assert(duration < 100, `Expected <100ms, got ${duration}ms`);
});
it("timedLock should return false after timeout", () => {
const mutex = Mutex.init();
Mutex.lock(mutex, 123);
const start = Date.now();
assert.equal(Mutex.timedLock(mutex, 456, start + 100), false);
assert(Date.now() - start >= 100);
});
it("should handle concurrent access (stress test)", async () => {
const THREADS = 10;
const promises = [];
const mtx = Mutex.init();
const shared = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
for (let i = 0; i < THREADS; i++) {
const worker = new Worker("./src/workers/mutex/incrementer.mjs", {
workerData: { threadId: i + 1, shared, mtx }
});
promises.push(
new Promise(resolve => {
worker.on("message", () => {
worker.terminate().then(resolve);
});
})
);
}
await Promise.all(promises);
assert.equal(shared[0], THREADS);
});
});