From c61e93a1f3f6aa5d0f09904c685cd51db0762f4b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 7 Jul 2026 22:11:14 -0400 Subject: [PATCH] fix(cli): harden election edge cases --- packages/cli/src/server-process.ts | 8 ++++++-- packages/core/src/util/effect-flock.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/server-process.ts b/packages/cli/src/server-process.ts index c0ab18d2d2..6ed14cba6e 100644 --- a/packages/cli/src/server-process.ts +++ b/packages/cli/src/server-process.ts @@ -29,7 +29,7 @@ type ManagedServiceOptions = Service.Options & { readonly file: string } export const run = Effect.fn("cli.server-process.run")((options: Options) => processEffect(options).pipe( - Effect.catchTag("ServiceAlreadyOwned", () => Effect.void), + Effect.catchTag("ServiceAlreadyOwned", () => Effect.logInfo("another process owns the managed service, exiting")), Effect.provide(Updater.layer), Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, EffectFlock.node]))), Effect.provide(NodeServices.layer), @@ -55,7 +55,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) { (acquired) => acquired, () => ({ _tag: "ServiceAlreadyOwned" }) as const, ), - Effect.retry(Schedule.spaced("100 millis").pipe(Schedule.both(Schedule.recurs(20)))), + // Retry only lost elections: a displaced owner may take a moment to release. + Effect.retry({ + while: (error) => error._tag === "ServiceAlreadyOwned", + schedule: Schedule.spaced("100 millis").pipe(Schedule.both(Schedule.recurs(20))), + }), ) } const password = diff --git a/packages/core/src/util/effect-flock.ts b/packages/core/src/util/effect-flock.ts index a3f70719b9..bd9e1c5e2a 100644 --- a/packages/core/src/util/effect-flock.ts +++ b/packages/core/src/util/effect-flock.ts @@ -164,11 +164,12 @@ export namespace EffectFlock { process.kill(owner.pid, 0) return true }, - catch: (cause) => { - const code = cause && typeof cause === "object" && "code" in cause ? cause.code : undefined - return code !== "ESRCH" - }, - }).pipe(Effect.orElseSucceed(() => false)) + catch: (cause) => (cause && typeof cause === "object" && "code" in cause ? cause.code : undefined), + }).pipe( + // Only ESRCH proves the pid is gone; other errors (e.g. EPERM) mean a + // process exists, so never break a possibly live owner's lock. + Effect.catch((code) => Effect.succeed(code !== "ESRCH")), + ) if (!alive) return true }