1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5
6// API Gateway REST has a very low account-wide control-plane mutation quota,
7// especially for deleting REST APIs. The authoritative AWS sweep runs a
8// distributed lane and a Lambda-quota lane in separate Bun processes; an
9// in-memory semaphore cannot coordinate a RestApiEventSource test in one lane
10// with ordinary ApiGateway tests in the other. Use one service-local,
11// cross-process lock directory instead. Unrelated AWS services remain fully
12// parallel.
13const profile = (process.env.ALCHEMY_PROFILE ?? "testing").replace(
14 /[^a-zA-Z0-9_.-]/g,
15 "-",
16);
17const uid = process.getuid?.() ?? 0;
18const lockDirectory = join(
19 tmpdir(),
20 `alchemy-test-apigateway-${uid}-${profile}.lock`,
21);
22const ownerFile = join(lockDirectory, "owner");
23
24const processIsAlive = (pid: number) => {
25 try {
26 process.kill(pid, 0);
27 return true;
28 } catch (error) {
29 return (error as NodeJS.ErrnoException).code === "EPERM";
30 }
31};
32
33const pollInterval = "250 millis";
34// A freshly-acquired lock writes its owner file immediately after the atomic
35// mkdir, so an unreadable owner is normally a sub-second race. If it stays
36// unreadable for this many consecutive polls (~10s) the lock predates a crash
37// or reboot that corrupted the directory (reads can then fail with
38// EFAULT/ENOENT/EPERM in bun) and it is treated as stale.
39const unreadableStalePolls = 40;
40
41/**
42 * Cross-process test lease over a tmpdir lock directory.
43 *
44 * Robustness contract: NO filesystem failure may escape into the test run.
45 * A machine reboot can leave the lock directory in a state where even
46 * `rm`/`read` fail with EFAULT (observed bun edge case on stale tmp entries);
47 * every fs primitive below is therefore best-effort, and a stale lock whose
48 * removal keeps failing is stolen in place by overwriting the owner file.
49 */
50export const makeApiGatewayTestLease = () => {
51 let held = false;
52
53 const acquire = Effect.gen(function* () {
54 const fs = yield* FileSystem.FileSystem;
55
56 const tryMakeDir = fs.makeDirectory(lockDirectory).pipe(
57 Effect.as(true),
58 Effect.catchCause(() => Effect.succeed(false)),
59 );
60 const tryWriteOwner = fs
61 .writeFileString(ownerFile, String(process.pid))
62 .pipe(
63 Effect.as(true),
64 Effect.catchCause(() => Effect.succeed(false)),
65 );
66 const tryReadOwnerPid = fs.readFileString(ownerFile).pipe(
67 Effect.map((content) => Number.parseInt(content, 10)),
68 Effect.catchCause(() => Effect.succeed<number | undefined>(undefined)),
69 );
70 const tryRemoveLock = fs.remove(lockDirectory, { recursive: true }).pipe(
71 Effect.as(true),
72 Effect.catchCause(() => Effect.succeed(false)),
73 );
74
75 let unreadablePolls = 0;
76
77 while (true) {
78 if (yield* tryMakeDir) {
79 if (yield* tryWriteOwner) {
80 held = true;
81 return;
82 }
83 // We created the directory but could not record ownership. Back out
84 // (best-effort) so other processes are not stuck waiting on a
85 // headless lock, then retry.
86 yield* tryRemoveLock;
87 yield* Effect.sleep(pollInterval);
88 continue;
89 }
90
91 // The directory already exists — or mkdir itself failed abnormally
92 // (e.g. EFAULT on a reboot-stale path). Inspect the recorded owner.
93 const ownerPid = yield* tryReadOwnerPid;
94
95 if (
96 ownerPid !== undefined &&
97 Number.isFinite(ownerPid) &&
98 processIsAlive(ownerPid)
99 ) {
100 unreadablePolls = 0;
101 yield* Effect.sleep(pollInterval);
102 continue;
103 }
104
105 if (ownerPid === undefined) {
106 // Missing or unreadable owner file: give the current owner a grace
107 // window to finish writing it before declaring the lock stale.
108 unreadablePolls += 1;
109 if (unreadablePolls < unreadableStalePolls) {
110 yield* Effect.sleep(pollInterval);
111 continue;
112 }
113 }
114
115 // Stale lock: the owner is dead, invalid, or unreadable past the grace
116 // window. Prefer removing the directory so the atomic-mkdir path can
117 // re-acquire; if removal itself keeps failing (EFAULT on the stale
118 // directory), steal in place by overwriting the owner file and
119 // verifying we won any concurrent write race.
120 unreadablePolls = 0;
121 if (yield* tryRemoveLock) {
122 continue;
123 }
124 if (yield* tryWriteOwner) {
125 yield* Effect.sleep("100 millis");
126 if ((yield* tryReadOwnerPid) === process.pid) {
127 held = true;
128 return;
129 }
130 }
131 yield* Effect.sleep(pollInterval);
132 }
133 });
134
135 const release = Effect.gen(function* () {
136 if (!held) return;
137 held = false;
138 const fs = yield* FileSystem.FileSystem;
139 const removed = yield* fs.remove(lockDirectory, { recursive: true }).pipe(
140 Effect.as(true),
141 Effect.catchCause(() => Effect.succeed(false)),
142 );
143 if (!removed) {
144 // Could not remove the directory (stale-tmp EFAULT). Best-effort drop
145 // of the owner file so a successor's staleness detection steals the
146 // lock instead of waiting on this (soon-dead) pid. Never fail release.
147 yield* fs.remove(ownerFile).pipe(Effect.catchCause(() => Effect.void));
148 }
149 });
150
151 return { acquire, release };
152};