import { describe, expect, it, beforeEach } from "@effect/vitest"; import { acquireBuildSlot, releaseBuildSlot, resetBuildSlotsForTest, currentActiveBuildsForTest, currentQueueLengthForTest,} from "./session-build-semaphore"; describe("session-build-semaphore", () => { beforeEach(() => { resetBuildSlotsForTest(); }); it("grants up to the cap immediately, with no wait", async () => { const results = await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); expect(results).toEqual([ { acquired: true, waitMs: 0, timedOut: false }, { acquired: true, waitMs: 0, timedOut: false }, { acquired: true, waitMs: 0, timedOut: false }, { acquired: true, waitMs: 0, timedOut: false }, ]); expect(currentActiveBuildsForTest()).toBe(4); expect(currentQueueLengthForTest()).toBe(0); }); it("queues a build past the cap until a slot is released", async () => { await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); expect(currentActiveBuildsForTest()).toBe(4); let fifthResolved = false; const fifth = acquireBuildSlot().promise.then((result) => { fifthResolved = true; return result; }); expect(currentQueueLengthForTest()).toBe(1); // Nothing releases the pending fifth build without an explicit release — // it must not resolve on its own. await Promise.resolve(); await Promise.resolve(); expect(fifthResolved).toBe(false); releaseBuildSlot(); const result = await fifth; expect(fifthResolved).toBe(true); expect(result.acquired).toBe(true); expect(result.timedOut).toBe(false); expect(result.waitMs).toBeGreaterThanOrEqual(0); // The freed slot went straight to the waiter — total active stays at cap. expect(currentActiveBuildsForTest()).toBe(4); expect(currentQueueLengthForTest()).toBe(0); }); it("releases queued waiters in FIFO order", async () => { await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); const order: number[] = []; const second = acquireBuildSlot().promise.then(() => order.push(2)); const third = acquireBuildSlot().promise.then(() => order.push(3)); const fourth = acquireBuildSlot().promise.then(() => order.push(4)); expect(currentQueueLengthForTest()).toBe(3); releaseBuildSlot(); await second; releaseBuildSlot(); await third; releaseBuildSlot(); await fourth; expect(order).toEqual([2, 3, 4]); }); it("never deadlocks: releasing a slot always makes forward progress for the next waiter", async () => { await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); // 6 more builds arrive while all 4 slots are held — all 6 queue. const queued = Array.from({ length: 6 }, () => acquireBuildSlot().promise); expect(currentQueueLengthForTest()).toBe(6); // The 4 in-flight builds finish one at a time; each release must hand its // slot straight to the next queued waiter rather than sitting idle. for (let i = 0; i < 4; i++) releaseBuildSlot(); await Promise.all(queued.slice(0, 4)); expect(currentActiveBuildsForTest()).toBe(4); expect(currentQueueLengthForTest()).toBe(2); // Those 4 finish too, freeing the last 2 queued waiters. for (let i = 0; i < 4; i++) releaseBuildSlot(); await Promise.all(queued.slice(4)); expect(currentActiveBuildsForTest()).toBe(2); expect(currentQueueLengthForTest()).toBe(0); // And the last 2 finish, draining the semaphore completely. releaseBuildSlot(); releaseBuildSlot(); expect(currentActiveBuildsForTest()).toBe(0); expect(currentQueueLengthForTest()).toBe(0); }); it("does not go negative when released more times than acquired", () => { releaseBuildSlot(); releaseBuildSlot(); expect(currentActiveBuildsForTest()).toBe(0); }); it("cancelling a queued waiter dequeues it, and a subsequent release goes to the next waiter", async () => { await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); expect(currentActiveBuildsForTest()).toBe(4); const cancelled = acquireBuildSlot(); let cancelledSettled = false; void cancelled.promise.then(() => { cancelledSettled = true; }); const next = acquireBuildSlot(); let nextResult: Awaited<typeof next.promise> | undefined; void next.promise.then((r) => { nextResult = r; }); expect(currentQueueLengthForTest()).toBe(2); // Interrupted before it was ever granted a slot (e.g. client disconnect // while queued) — this must not hand it a slot later, and must not block // the waiter behind it. cancelled.cancel(); expect(currentQueueLengthForTest()).toBe(1); releaseBuildSlot(); await next.promise; // The cancelled waiter's promise never resolves and never gets a slot; // the freed slot went straight to the next waiter instead. await Promise.resolve(); await Promise.resolve(); expect(cancelledSettled).toBe(false); expect(nextResult).toEqual({ acquired: true, waitMs: expect.any(Number), timedOut: false }); expect(currentActiveBuildsForTest()).toBe(4); expect(currentQueueLengthForTest()).toBe(0); }); it("cancel-after-grant releases the slot instead of leaking it", async () => { await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); expect(currentActiveBuildsForTest()).toBe(4); // Queues, then gets granted a slot by the release below. const grantedThenAbandoned = acquireBuildSlot(); releaseBuildSlot(); const result = await grantedThenAbandoned.promise; expect(result.acquired).toBe(true); // Still at the cap: the freed slot was handed straight to this waiter. expect(currentActiveBuildsForTest()).toBe(4); // The caller never gets to consume the grant (e.g. its fiber was // interrupted in the same tick the grant happened) and cancels instead — // this must give the slot back rather than leaking it forever. grantedThenAbandoned.cancel(); expect(currentActiveBuildsForTest()).toBe(3); // Idempotent: a second cancel must not release it again. grantedThenAbandoned.cancel(); expect(currentActiveBuildsForTest()).toBe(3); // And the freed slot is usable by a fresh acquire. const fresh = await acquireBuildSlot().promise; expect(fresh).toEqual({ acquired: true, waitMs: 0, timedOut: false }); expect(currentActiveBuildsForTest()).toBe(4); }); it("cancelling an immediately-granted (never-queued) slot releases it exactly once", async () => { const handle = acquireBuildSlot(); await handle.promise; expect(currentActiveBuildsForTest()).toBe(1); handle.cancel(); expect(currentActiveBuildsForTest()).toBe(0); // Idempotent. handle.cancel(); expect(currentActiveBuildsForTest()).toBe(0); }); it("proceeds without a slot when the queue wait exceeds the timeout, and does not count it as active", async () => { await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); expect(currentActiveBuildsForTest()).toBe(4); const timedOutHandle = acquireBuildSlot(10); const result = await timedOutHandle.promise; expect(result).toEqual({ acquired: false, waitMs: expect.any(Number), timedOut: true }); expect(result.waitMs).toBeGreaterThanOrEqual(10); // A timed-out waiter never counted against the cap. expect(currentActiveBuildsForTest()).toBe(4); expect(currentQueueLengthForTest()).toBe(0); // A timed-out waiter never held a slot, so cancelling it must not // release one that was never granted. timedOutHandle.cancel(); expect(currentActiveBuildsForTest()).toBe(4); // The 4 originally-held slots are still releasable normally. releaseBuildSlot(); releaseBuildSlot(); releaseBuildSlot(); releaseBuildSlot(); expect(currentActiveBuildsForTest()).toBe(0); }); it("a waiter that times out is removed from the queue and does not block waiters behind it", async () => { await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, acquireBuildSlot().promise, ]); const timesOut = acquireBuildSlot(10); const staysQueued = acquireBuildSlot(); expect(currentQueueLengthForTest()).toBe(2); const timedOutResult = await timesOut.promise; expect(timedOutResult.timedOut).toBe(true); // Only the timed-out waiter left the queue; the other is still waiting. expect(currentQueueLengthForTest()).toBe(1); releaseBuildSlot(); const staysQueuedResult = await staysQueued.promise; expect(staysQueuedResult).toEqual({ acquired: true, waitMs: expect.any(Number), timedOut: false, }); expect(currentQueueLengthForTest()).toBe(0); });});
import { describe, expect, it, beforeEach } from "@effect/vitest";
import {
acquireBuildSlot,
releaseBuildSlot,
resetBuildSlotsForTest,
currentActiveBuildsForTest,
currentQueueLengthForTest,
} from "./session-build-semaphore";
describe("session-build-semaphore", () => {
beforeEach(() => {
resetBuildSlotsForTest();
});
it("grants up to the cap immediately, with no wait", async () => {
const results = await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
expect(results).toEqual([
{ acquired: true, waitMs: 0, timedOut: false },
{ acquired: true, waitMs: 0, timedOut: false },
{ acquired: true, waitMs: 0, timedOut: false },
{ acquired: true, waitMs: 0, timedOut: false },
]);
expect(currentActiveBuildsForTest()).toBe(4);
expect(currentQueueLengthForTest()).toBe(0);
});
it("queues a build past the cap until a slot is released", async () => {
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
expect(currentActiveBuildsForTest()).toBe(4);
let fifthResolved = false;
const fifth = acquireBuildSlot().promise.then((result) => {
fifthResolved = true;
return result;
});
expect(currentQueueLengthForTest()).toBe(1);
// Nothing releases the pending fifth build without an explicit release —
// it must not resolve on its own.
await Promise.resolve();
await Promise.resolve();
expect(fifthResolved).toBe(false);
releaseBuildSlot();
const result = await fifth;
expect(fifthResolved).toBe(true);
expect(result.acquired).toBe(true);
expect(result.timedOut).toBe(false);
expect(result.waitMs).toBeGreaterThanOrEqual(0);
// The freed slot went straight to the waiter — total active stays at cap.
expect(currentActiveBuildsForTest()).toBe(4);
expect(currentQueueLengthForTest()).toBe(0);
});
it("releases queued waiters in FIFO order", async () => {
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
const order: number[] = [];
const second = acquireBuildSlot().promise.then(() => order.push(2));
const third = acquireBuildSlot().promise.then(() => order.push(3));
const fourth = acquireBuildSlot().promise.then(() => order.push(4));
expect(currentQueueLengthForTest()).toBe(3);
releaseBuildSlot();
await second;
releaseBuildSlot();
await third;
releaseBuildSlot();
await fourth;
expect(order).toEqual([2, 3, 4]);
});
it("never deadlocks: releasing a slot always makes forward progress for the next waiter", async () => {
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
// 6 more builds arrive while all 4 slots are held — all 6 queue.
const queued = Array.from({ length: 6 }, () => acquireBuildSlot().promise);
expect(currentQueueLengthForTest()).toBe(6);
// The 4 in-flight builds finish one at a time; each release must hand its
// slot straight to the next queued waiter rather than sitting idle.
for (let i = 0; i < 4; i++) releaseBuildSlot();
await Promise.all(queued.slice(0, 4));
expect(currentActiveBuildsForTest()).toBe(4);
expect(currentQueueLengthForTest()).toBe(2);
// Those 4 finish too, freeing the last 2 queued waiters.
for (let i = 0; i < 4; i++) releaseBuildSlot();
await Promise.all(queued.slice(4));
expect(currentActiveBuildsForTest()).toBe(2);
expect(currentQueueLengthForTest()).toBe(0);
// And the last 2 finish, draining the semaphore completely.
releaseBuildSlot();
releaseBuildSlot();
expect(currentActiveBuildsForTest()).toBe(0);
expect(currentQueueLengthForTest()).toBe(0);
});
it("does not go negative when released more times than acquired", () => {
releaseBuildSlot();
releaseBuildSlot();
expect(currentActiveBuildsForTest()).toBe(0);
});
it("cancelling a queued waiter dequeues it, and a subsequent release goes to the next waiter", async () => {
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
expect(currentActiveBuildsForTest()).toBe(4);
const cancelled = acquireBuildSlot();
let cancelledSettled = false;
void cancelled.promise.then(() => {
cancelledSettled = true;
});
const next = acquireBuildSlot();
let nextResult: Awaited<typeof next.promise> | undefined;
void next.promise.then((r) => {
nextResult = r;
});
expect(currentQueueLengthForTest()).toBe(2);
// Interrupted before it was ever granted a slot (e.g. client disconnect
// while queued) — this must not hand it a slot later, and must not block
// the waiter behind it.
cancelled.cancel();
expect(currentQueueLengthForTest()).toBe(1);
releaseBuildSlot();
await next.promise;
// The cancelled waiter's promise never resolves and never gets a slot;
// the freed slot went straight to the next waiter instead.
await Promise.resolve();
await Promise.resolve();
expect(cancelledSettled).toBe(false);
expect(nextResult).toEqual({ acquired: true, waitMs: expect.any(Number), timedOut: false });
expect(currentActiveBuildsForTest()).toBe(4);
expect(currentQueueLengthForTest()).toBe(0);
});
it("cancel-after-grant releases the slot instead of leaking it", async () => {
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
expect(currentActiveBuildsForTest()).toBe(4);
// Queues, then gets granted a slot by the release below.
const grantedThenAbandoned = acquireBuildSlot();
releaseBuildSlot();
const result = await grantedThenAbandoned.promise;
expect(result.acquired).toBe(true);
// Still at the cap: the freed slot was handed straight to this waiter.
expect(currentActiveBuildsForTest()).toBe(4);
// The caller never gets to consume the grant (e.g. its fiber was
// interrupted in the same tick the grant happened) and cancels instead —
// this must give the slot back rather than leaking it forever.
grantedThenAbandoned.cancel();
expect(currentActiveBuildsForTest()).toBe(3);
// Idempotent: a second cancel must not release it again.
grantedThenAbandoned.cancel();
expect(currentActiveBuildsForTest()).toBe(3);
// And the freed slot is usable by a fresh acquire.
const fresh = await acquireBuildSlot().promise;
expect(fresh).toEqual({ acquired: true, waitMs: 0, timedOut: false });
expect(currentActiveBuildsForTest()).toBe(4);
});
it("cancelling an immediately-granted (never-queued) slot releases it exactly once", async () => {
const handle = acquireBuildSlot();
await handle.promise;
expect(currentActiveBuildsForTest()).toBe(1);
handle.cancel();
expect(currentActiveBuildsForTest()).toBe(0);
// Idempotent.
handle.cancel();
expect(currentActiveBuildsForTest()).toBe(0);
});
it("proceeds without a slot when the queue wait exceeds the timeout, and does not count it as active", async () => {
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
expect(currentActiveBuildsForTest()).toBe(4);
const timedOutHandle = acquireBuildSlot(10);
const result = await timedOutHandle.promise;
expect(result).toEqual({ acquired: false, waitMs: expect.any(Number), timedOut: true });
expect(result.waitMs).toBeGreaterThanOrEqual(10);
// A timed-out waiter never counted against the cap.
expect(currentActiveBuildsForTest()).toBe(4);
expect(currentQueueLengthForTest()).toBe(0);
// A timed-out waiter never held a slot, so cancelling it must not
// release one that was never granted.
timedOutHandle.cancel();
expect(currentActiveBuildsForTest()).toBe(4);
// The 4 originally-held slots are still releasable normally.
releaseBuildSlot();
releaseBuildSlot();
releaseBuildSlot();
releaseBuildSlot();
expect(currentActiveBuildsForTest()).toBe(0);
});
it("a waiter that times out is removed from the queue and does not block waiters behind it", async () => {
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
acquireBuildSlot().promise,
]);
const timesOut = acquireBuildSlot(10);
const staysQueued = acquireBuildSlot();
expect(currentQueueLengthForTest()).toBe(2);
const timedOutResult = await timesOut.promise;
expect(timedOutResult.timedOut).toBe(true);
// Only the timed-out waiter left the queue; the other is still waiting.
expect(currentQueueLengthForTest()).toBe(1);
releaseBuildSlot();
const staysQueuedResult = await staysQueued.promise;
expect(staysQueuedResult).toEqual({
acquired: true,
waitMs: expect.any(Number),
timedOut: false,
});
expect(currentQueueLengthForTest()).toBe(0);
});
});