fix(gate): close probe connections before releasing the lane (#1229)

The gate-lane lock holds its lane by binding a local TCP port. Its server had no connection handler, so an idle local connection to that port (a port probe, `nc`, a monitor) kept `server.close()` waiting: the gate couldn't finish releasing its lane, and couldn't exit, until that connection dropped. The server now destroys each accepted socket at once. The bind is still the lock, so a live owner is never released early and a waiter still acquires the lane after release, now promptly. A test holds the lane, opens and keeps a client connection, and checks that release completes and the lane can be acquired again.
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-13 08:08:14 -07:00
1 parent 6d5f6c54ab
commit bf5d2b5f72
2 files changed
+38 -1

No files matched your search

+3 -1
View File
@@ -34,7 +34,9 @@ export async function acquireGateLane({ port = GATE_LANE_PORT, holderInfoPath })
return { ok: false, reason: "bind-error", message: "Gate lane port must be an integer from 1 to 65535." };
}
// P1 + P2: bind 127.0.0.1 explicitly; do NOT pass reusePort — exclusivity is the mutex.
const server = net.createServer();
// This port names a mutex, not a service. A local probe must not leave a
// connection open that keeps server.close() and gate release waiting.
const server = net.createServer((socket) => socket.destroy());
const bound = await new Promise((resolve) => {
server.once("error", (err) => resolve({ ok: false, err }));
server.listen(port, "127.0.0.1", () => resolve({ ok: true }));
+35
View File
@@ -124,3 +124,38 @@ test("configured gate port does not accept a partial numeric prefix", () => {
`], { env: { ...process.env, OPENRIG_GATE_LANE_PORT: "40404typo" }, encoding: "utf8" });
assert.deepEqual(JSON.parse(stdout), { finite: false, ok: false, reason: "bind-error" });
});
test("a local probe connection cannot delay gate release", async () => {
const root = mkdtempSync(join(tmpdir(), "openrig-lock-probe-"));
const reservation = net.createServer();
await new Promise((resolve) => reservation.listen(0, "127.0.0.1", resolve));
const port = reservation.address().port;
await new Promise((resolve) => reservation.close(resolve));
const lane = await acquireGateLane({ port, holderInfoPath: join(root, "holder.json") });
assert.equal(lane.ok, true);
const client = net.connect(port, "127.0.0.1");
client.on("error", () => {});
let release;
let timer;
try {
await new Promise((resolve, reject) => {
client.once("connect", resolve);
client.once("error", reject);
});
release = lane.release();
const completed = await Promise.race([
release.then(() => true),
new Promise((resolve) => { timer = setTimeout(() => resolve(false), 500); }),
]);
assert.equal(completed, true, "an idle local connection must not keep the mutex owner alive");
const next = await acquireGateLane({ port, holderInfoPath: join(root, "holder.json") });
assert.equal(next.ok, true);
if (next.ok) await next.release();
} finally {
clearTimeout(timer);
client.destroy();
await (release ?? lane.release());
rmSync(root, { recursive: true, force: true });
}
});