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