refactor(client): simplify local service lifecycle
This commit is contained in:
parent
4f1298063b
commit
75bc611ef1
51 changed files with 635 additions and 534 deletions
|
|
@ -10,7 +10,6 @@ export const HealthHandler = HttpApiBuilder.group(Api, "server.health", (handler
|
|||
healthy: true as const,
|
||||
version: InstallationVersion,
|
||||
pid: process.pid,
|
||||
status: { type: "ready" },
|
||||
}),
|
||||
)
|
||||
.handle("health.stop", () => Effect.succeed({ accepted: false })),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as ServerProcess from "./process"
|
||||
|
||||
import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
|
||||
import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty"
|
||||
|
|
@ -45,7 +46,7 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options:
|
|||
const applicationScope = yield* Scope.fork(parentScope)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
status
|
||||
.beginStopping()
|
||||
.beginStopping
|
||||
.pipe(
|
||||
Effect.andThen(Ref.set(application, Option.none())),
|
||||
Effect.andThen(Effect.sync(() => bound.server.closeAllConnections())),
|
||||
|
|
@ -73,22 +74,17 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options:
|
|||
}).pipe(
|
||||
Effect.catchCause((cause) => {
|
||||
if (!options.service || Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
|
||||
return status
|
||||
.fail({
|
||||
message: "The background service could not start.",
|
||||
action: "Run `opencode service restart` after checking the service logs.",
|
||||
})
|
||||
.pipe(
|
||||
Effect.andThen(
|
||||
Scope.close(applicationScope, Exit.failCause(cause)).pipe(
|
||||
Effect.catchCause((cleanupCause) =>
|
||||
Effect.logError("failed to clean up background service boot", { cause: cleanupCause }),
|
||||
),
|
||||
return status.fail.pipe(
|
||||
Effect.andThen(
|
||||
Scope.close(applicationScope, Exit.failCause(cause)).pipe(
|
||||
Effect.catchCause((cleanupCause) =>
|
||||
Effect.logError("failed to clean up background service boot", { cause: cleanupCause }),
|
||||
),
|
||||
),
|
||||
Effect.andThen(Effect.logError("background service boot failed", { cause })),
|
||||
Effect.andThen(Effect.never),
|
||||
)
|
||||
),
|
||||
Effect.andThen(Effect.logError("background service boot failed", { cause })),
|
||||
Effect.andThen(Effect.never),
|
||||
)
|
||||
}),
|
||||
)
|
||||
if (!options.service) return yield* boot
|
||||
|
|
@ -189,18 +185,21 @@ const control = Effect.fnUntraced(function* (
|
|||
})
|
||||
|
||||
const healthResponse = Effect.fnUntraced(function* (status: Status.Interface) {
|
||||
const health = yield* status.health
|
||||
return HttpServerResponse.jsonUnsafe(health, {
|
||||
status: health.status.type === "ready" ? 200 : 503,
|
||||
headers:
|
||||
health.status.type === "starting" || health.status.type === "stopping" ? { "retry-after": "1" } : undefined,
|
||||
const state = yield* status.current
|
||||
return HttpServerResponse.jsonUnsafe({ healthy: true, version: InstallationVersion, pid: process.pid }, {
|
||||
status: state.type === "ready" ? 200 : state.type === "failed" ? 500 : 503,
|
||||
headers: state.type === "starting" || state.type === "stopping" ? { "retry-after": "1" } : undefined,
|
||||
})
|
||||
})
|
||||
|
||||
function unavailable(status: ServiceStatus.State) {
|
||||
function unavailable(status: Status.State) {
|
||||
if (status.type === "failed")
|
||||
return HttpServerResponse.jsonUnsafe(
|
||||
{ code: "service_failed", message: status.message, action: status.action },
|
||||
{
|
||||
code: "service_failed",
|
||||
message: "The background service could not start.",
|
||||
action: "Run `opencode service restart` after checking the service logs.",
|
||||
},
|
||||
{ status: 503 },
|
||||
)
|
||||
return HttpServerResponse.jsonUnsafe(
|
||||
|
|
|
|||
|
|
@ -1,57 +1,45 @@
|
|||
export * as Status from "./service-status"
|
||||
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
|
||||
import { Effect, Ref } from "effect"
|
||||
|
||||
export type State =
|
||||
| { readonly type: "starting" }
|
||||
| { readonly type: "ready" }
|
||||
| { readonly type: "stopping" }
|
||||
| { readonly type: "failed" }
|
||||
|
||||
export interface Interface {
|
||||
readonly health: Effect.Effect<ServiceStatus.Health>
|
||||
readonly current: Effect.Effect<ServiceStatus.State>
|
||||
readonly current: Effect.Effect<State>
|
||||
readonly ready: Effect.Effect<void>
|
||||
readonly fail: (failure: { readonly message: string; readonly action: string }) => Effect.Effect<void>
|
||||
readonly beginStopping: (targetVersion?: string) => Effect.Effect<void>
|
||||
readonly fail: Effect.Effect<void>
|
||||
readonly beginStopping: Effect.Effect<void>
|
||||
readonly requestStop: (request: ServiceStatus.StopRequest) => Effect.Effect<boolean>
|
||||
}
|
||||
|
||||
export const make = Effect.fnUntraced(function* (options: {
|
||||
readonly instanceID: string
|
||||
readonly managed: boolean
|
||||
readonly initial?: ServiceStatus.State
|
||||
readonly initial?: State
|
||||
}) {
|
||||
const current = yield* Ref.make(options.initial ?? ({ type: "starting" } satisfies ServiceStatus.State))
|
||||
const transitionToStopping = (targetVersion?: string) =>
|
||||
Ref.update(current, (status) => {
|
||||
if (status.type === "stopping") return status
|
||||
return (
|
||||
targetVersion === undefined || targetVersion === InstallationVersion
|
||||
? { type: "stopping" }
|
||||
: { type: "stopping", targetVersion }
|
||||
) satisfies ServiceStatus.State
|
||||
})
|
||||
const current = yield* Ref.make(options.initial ?? ({ type: "starting" } satisfies State))
|
||||
const beginStopping = Ref.update(current, (status) =>
|
||||
status.type === "stopping" ? status : ({ type: "stopping" } satisfies State),
|
||||
)
|
||||
|
||||
return {
|
||||
current: Ref.get(current),
|
||||
health: Effect.gen(function* () {
|
||||
return {
|
||||
healthy: true as const,
|
||||
version: InstallationVersion,
|
||||
pid: process.pid,
|
||||
instanceID: options.instanceID,
|
||||
status: yield* Ref.get(current),
|
||||
}
|
||||
}),
|
||||
ready: Ref.update(current, (status) =>
|
||||
status.type === "starting" ? ({ type: "ready" } satisfies ServiceStatus.State) : status,
|
||||
status.type === "starting" ? ({ type: "ready" } satisfies State) : status,
|
||||
),
|
||||
fail: (failure) =>
|
||||
Ref.update(current, (status) =>
|
||||
status.type === "starting" ? ({ type: "failed", ...failure } satisfies ServiceStatus.State) : status,
|
||||
),
|
||||
beginStopping: transitionToStopping,
|
||||
fail: Ref.update(current, (status) =>
|
||||
status.type === "starting" ? ({ type: "failed" } satisfies State) : status,
|
||||
),
|
||||
beginStopping,
|
||||
requestStop: (request) => {
|
||||
if (!options.managed || request.instanceID !== options.instanceID)
|
||||
return Effect.succeed(false)
|
||||
return transitionToStopping(request.targetVersion).pipe(Effect.as(true))
|
||||
return beginStopping.pipe(Effect.as(true))
|
||||
},
|
||||
} satisfies Interface
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue