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