fix(cli): harden election edge cases

This commit is contained in:
Kit Langton 2026-07-07 22:11:14 -04:00
commit c61e93a1f3
2 changed files with 12 additions and 7 deletions

View file

@ -29,7 +29,7 @@ type ManagedServiceOptions = Service.Options & { readonly file: string }
export const run = Effect.fn("cli.server-process.run")((options: Options) => export const run = Effect.fn("cli.server-process.run")((options: Options) =>
processEffect(options).pipe( 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(Updater.layer),
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, EffectFlock.node]))), Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, EffectFlock.node]))),
Effect.provide(NodeServices.layer), Effect.provide(NodeServices.layer),
@ -55,7 +55,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
(acquired) => acquired, (acquired) => acquired,
() => ({ _tag: "ServiceAlreadyOwned" }) as const, () => ({ _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 = const password =

View file

@ -164,11 +164,12 @@ export namespace EffectFlock {
process.kill(owner.pid, 0) process.kill(owner.pid, 0)
return true return true
}, },
catch: (cause) => { catch: (cause) => (cause && typeof cause === "object" && "code" in cause ? cause.code : undefined),
const code = cause && typeof cause === "object" && "code" in cause ? cause.code : undefined }).pipe(
return code !== "ESRCH" // 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.
}).pipe(Effect.orElseSucceed(() => false)) Effect.catch((code) => Effect.succeed(code !== "ESRCH")),
)
if (!alive) return true if (!alive) return true
} }