fix(cli): release service startup lock
This commit is contained in:
parent
987242b3e8
commit
fb566141d3
2 changed files with 26 additions and 11 deletions
|
|
@ -10,7 +10,7 @@ import { AppProcess } from "@opencode-ai/core/process"
|
|||
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
|
||||
import { randomBytes, randomUUID } from "node:crypto"
|
||||
import path from "node:path"
|
||||
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
||||
import { Effect, Exit, FileSystem, Logger, Option, Redacted, Schedule, Schema, Scope } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { Env } from "./env"
|
||||
import { ServiceConfig } from "./services/service-config"
|
||||
|
|
@ -38,8 +38,12 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
|||
return yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined
|
||||
if (serviceOptions !== undefined) {
|
||||
const lockScope = serviceOptions === undefined ? undefined : yield* Scope.make()
|
||||
if (lockScope !== undefined) yield* Effect.addFinalizer((exit) => Scope.close(lockScope, exit))
|
||||
if (serviceOptions !== undefined && lockScope !== undefined) {
|
||||
if ((yield* Service.discover(serviceOptions)) !== undefined) return yield* Effect.void
|
||||
const acquired = yield* ProcessLock.acquire(serviceOptions.file + ".lock").pipe(
|
||||
Effect.provideService(Scope.Scope, lockScope),
|
||||
Effect.as(true),
|
||||
Effect.catchTag("ProcessLockHeldError", () => Effect.succeed(false)),
|
||||
)
|
||||
|
|
@ -72,6 +76,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
|||
? undefined
|
||||
: { onListen: (address) => register(address, password, instanceID, serviceOptions.file) },
|
||||
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
||||
if (lockScope !== undefined) yield* Scope.close(lockScope, Exit.void)
|
||||
const url = HttpServer.formatAddress(server.address)
|
||||
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
|
||||
import { expect, test } from "bun:test"
|
||||
import { Effect, Schedule, Schema } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
|
|
@ -142,15 +143,23 @@ test("concurrent service processes elect one server", async () => {
|
|||
|
||||
expect(exited).toEqual(losers.map(() => true))
|
||||
expect(winner?.exitCode).toBe(null)
|
||||
expect(
|
||||
await fetch(new URL("/api/health", info.url), {
|
||||
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
|
||||
}).then((response) => response.json()),
|
||||
).toEqual({
|
||||
healthy: true,
|
||||
version: info.version,
|
||||
pid: info.pid,
|
||||
})
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
yield* ProcessLock.acquire(registration + ".lock")
|
||||
expect(winner?.exitCode).toBe(null)
|
||||
expect(
|
||||
yield* Effect.promise(() =>
|
||||
fetch(new URL("/api/health", info.url), {
|
||||
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
|
||||
}).then((response) => response.json()),
|
||||
),
|
||||
).toEqual({
|
||||
healthy: true,
|
||||
version: info.version,
|
||||
pid: info.pid,
|
||||
})
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
const blockedTemp = registration + "." + info.id + ".tmp"
|
||||
await fs.mkdir(blockedTemp)
|
||||
await fs.rm(registration)
|
||||
|
|
@ -172,6 +181,7 @@ test("concurrent service processes elect one server", async () => {
|
|||
Bun.sleep(10_000).then(() => false),
|
||||
])
|
||||
expect(contenderExited).toBe(true)
|
||||
expect(contender.exitCode).toBe(0)
|
||||
expect((await waitForInfo(registration)).id).toBe(info.id)
|
||||
} finally {
|
||||
contender.kill("SIGTERM")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue