Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions tests/codex-integration/codex-write-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,15 @@ describe("two real processes contend for one lock", () => {

function spawnChild(payload: Record<string, unknown>) {
return Bun.spawn(["bun", childPath], {
env: { ...process.env, CODEX_HOME: codexHome, OCX_LOCK_CHILD_PAYLOAD: JSON.stringify(payload) },
env: {
...process.env,
CODEX_HOME: codexHome,
// N is the lock under test. Give the child processes in this case their
// own C database so unrelated files in the same Bun batch cannot make a
// holder retry after it has published its held marker.
OPENCODEX_HOME: join(root, ".opencodex"),
OCX_LOCK_CHILD_PAYLOAD: JSON.stringify(payload),
},
stdout: "pipe",
stderr: "pipe",
});
Expand All @@ -298,6 +306,7 @@ describe("two real processes contend for one lock", () => {
env: {
...process.env,
CODEX_HOME: codexHome,
OPENCODEX_HOME: join(root, ".opencodex"),
...env,
OCX_LOCK_CHILD_PAYLOAD: JSON.stringify(payload),
},
Expand All @@ -309,7 +318,13 @@ describe("two real processes contend for one lock", () => {
async function childResult(child: ReturnType<typeof Bun.spawn>) {
const [stdout] = await Promise.all([new Response(child.stdout).text(), child.exited]);
const line = stdout.trim().split("\n").filter(Boolean).at(-1) ?? "{}";
return JSON.parse(line) as { status: string; reason?: string; value?: string; lockId?: string };
return JSON.parse(line) as {
status: string;
reason?: string;
value?: string;
waitedMs?: number;
lockId?: string;
};
}

// A spawned holder child boots in 8-19 s on a loaded windows-latest shard; the 10 s
Expand Down Expand Up @@ -362,14 +377,18 @@ describe("two real processes contend for one lock", () => {
test("a contender with a deadline waits for the holder instead of failing immediately", async () => {
const holdMarker = join(root, "held-2");
const releaseMarker = join(root, "release-2");
const waitMarker = join(root, "waiting-2");
const holder = spawnChild({ holdMarker, releaseMarker, timeoutMs: 0, holdMs: 20_000 });
await waitFor(holdMarker);

const waiter = withCodexWriteLock(options({ timeoutMs: 5_000 }), publishing("waited"));
await Bun.sleep(150);
const waiter = spawnChild({ timeoutMs: 5_000, waitMarker });
// The waiter writes this only after withCodexWriteLock has returned its
// pending promise. Because the holder is still held, that means the waiter
// has attempted N and reached the retry wait rather than failing fast.
await waitFor(waitMarker);
Comment on lines +384 to +388

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Give the waiter child a spawn-sized readiness window

On loaded Windows runners, this file already records that this helper can take 8–19 seconds to boot, but the new waitFor(waitMarker) still uses the 15-second INTERNAL_DEADLINE_MS. The newly spawned waiter can therefore be healthy yet fail before publishing the marker; if startup approaches the holder's 20-second ceiling, the holder may also release first, causing the waiter to settle without ever writing the marker. This reintroduces the timing-dependent CI failure the change is intended to remove; use a readiness deadline sized for a spawned process and make the holder ceiling safely exceed it.

Useful? React with 👍 / 👎.

writeFileSync(releaseMarker, "go");

const [waited, holderResult] = await Promise.all([waiter, childResult(holder)]);
const [waited, holderResult] = await Promise.all([childResult(waiter), childResult(holder)]);
expect(holderResult.status).toBe("acquired");
expect(waited.status).toBe("acquired");
expect(waited.status === "acquired" && waited.waitedMs).toBeGreaterThan(0);
Expand Down
26 changes: 23 additions & 3 deletions tests/helpers/codex-write-lock-child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ const payload = JSON.parse(process.env.OCX_LOCK_CHILD_PAYLOAD ?? "{}") as {
timeoutMs?: number;
holdMarker?: string;
releaseMarker?: string;
waitMarker?: string;
holdMs?: number;
};

const admitted = { authoritySnapshotId: "authority-child" } as AdmissionSnapshot;

const result = await withCodexWriteLock(
const pending = withCodexWriteLock(
{
timeoutMs: payload.timeoutMs ?? 0,
admitted,
Expand Down Expand Up @@ -75,9 +76,28 @@ const result = await withCodexWriteLock(
},
);

if (payload.waitMarker) {
let settled = false;
void pending.then(
() => { settled = true; },
() => { settled = true; },
);
// Flush reactions for a promise that completed synchronously. When a holder
// already owns N, an unsettled promise here means withCodexWriteLock tried to
// acquire it and suspended in its retry wait.
await Promise.resolve();
if (!settled) writeFileSync(payload.waitMarker, "waiting");
}

const result = await pending;

console.log(JSON.stringify({
status: result.status,
...(result.status === "acquired" ? { value: result.value, lockId: result.lockId } : {}),
...(result.status === "busy" ? { reason: result.reason, lockId: result.lockId } : {}),
...(result.status === "acquired"
? { value: result.value, waitedMs: result.waitedMs, lockId: result.lockId }
: {}),
...(result.status === "busy"
? { reason: result.reason, waitedMs: result.waitedMs, lockId: result.lockId }
: {}),
...(result.status === "refused" ? { reason: result.reason, message: result.message } : {}),
}));
Loading