Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,73 @@ server.listen(0, "127.0.0.1", () => {
10_000,
);
});

describe("OpenCode database lock startup", () => {
effectIt.live.each(["transient", "permanent", "configuration"] as const)(
"handles %s startup failures with bounded process ownership",
(mode) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-opencode-lock-" });
const platform = yield* HostProcess.Platform;
const executablePath = yield* HostProcess.ExecutablePath;
const environment = yield* HostProcess.Environment;
const scriptPath = `${root}/fake.mjs`;
const attemptsPath = `${root}/attempts`;
yield* fs.writeFileString(
scriptPath,
`
import { readFileSync, writeFileSync, writeSync } from "node:fs";
import { createServer } from "node:http";
const path = process.env.T3_TEST_ATTEMPTS;
let attempt = 0;
try { attempt = Number(readFileSync(path, "utf8")); } catch {}
writeFileSync(path, String(++attempt));
if (${JSON.stringify(mode)} !== "transient" || attempt < 3) {
writeSync(2, ${JSON.stringify(mode === "configuration" ? "Invalid configuration" : "SQLiteError: database is locked (SQLITE_BUSY)")} + "\\n");
process.exit(1);
}
const server = createServer((req, res) => res.end("ok"));
server.listen(0, "127.0.0.1", () => console.log("opencode server listening on http://127.0.0.1:" + server.address().port));
`,
);
const binaryPath = `${root}/opencode${platform === "win32" ? ".cmd" : ""}`;
yield* fs.writeFileString(
binaryPath,
platform === "win32"
? '@echo off\n"%T3_TEST_NODE_BINARY%" "%T3_TEST_OPENCODE_SCRIPT%" %*\n'
: '#!/bin/sh\nexec "$T3_TEST_NODE_BINARY" "$T3_TEST_OPENCODE_SCRIPT" "$@"\n',
);
if (platform !== "win32") yield* fs.chmod(binaryPath, 0o700);
const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
const result = yield* Effect.result(
runtime.startOpenCodeServerProcess({
binaryPath,
directory: root,
port: 0,
environment: {
...environment,
T3_TEST_NODE_BINARY: executablePath,
T3_TEST_OPENCODE_SCRIPT: scriptPath,
T3_TEST_ATTEMPTS: attemptsPath,
},
verify: () => Effect.succeed("1.14.19"),
}),
);
expect(result._tag).toBe(mode === "transient" ? "Success" : "Failure");
expect(Number(yield* fs.readFileString(attemptsPath))).toBe(
mode === "transient" ? 3 : mode === "permanent" ? 4 : 1,
);
}).pipe(
Effect.scoped,
Effect.provide([
OpenCodeRuntime.layer.pipe(
Layer.provide(OpenCodeServerLedger.layerTest),
Layer.provideMerge(NodeServices.layer),
),
FetchHttpClient.layer,
]),
),
15_000,
);
});
35 changes: 32 additions & 3 deletions packages/provider-opencode/src/server/OpenCodeRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as Ref from "effect/Ref";
import * as Result from "effect/Result";
import * as Scope from "effect/Scope";
import * as Schema from "effect/Schema";
import * as Schedule from "effect/Schedule";
import * as Stream from "effect/Stream";
import * as ChildProcess from "effect/process/ChildProcess";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
Expand Down Expand Up @@ -671,9 +672,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
throwOnError: true,
});

const startOpenCodeServerProcess: OpenCodeRuntime["Service"]["startOpenCodeServerProcess"] = (
input,
) =>
const startServerAttempt: OpenCodeRuntime["Service"]["startOpenCodeServerProcess"] = (input) =>
Effect.gen(function* () {
// Bind this server's lifetime to the caller's scope. When the caller's
// scope closes, the spawned child is killed and all associated fibers
Expand Down Expand Up @@ -809,6 +808,9 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
const exitFiber = yield* child.exitCode.pipe(
Effect.flatMap((code) =>
Effect.gen(function* () {
// Exit may arrive before the final stderr chunk containing SQLITE_BUSY.
yield* Fiber.join(stdoutFiber);
yield* Fiber.join(stderrFiber);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const stdout = (yield* Ref.get(stdoutRef)) ?? "";
const stderr = (yield* Ref.get(stderrRef)) ?? "";
const exitCode = Number(code);
Expand Down Expand Up @@ -887,6 +889,33 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
} satisfies OpenCodeServerProcess;
});

const startOpenCodeServerProcess: OpenCodeRuntime["Service"]["startOpenCodeServerProcess"] = (
input,
) =>
Effect.gen(function* () {
const ownerScope = yield* Scope.Scope;
const attempt = Effect.gen(function* () {
const attemptScope = yield* Scope.fork(ownerScope);
return yield* startServerAttempt(input).pipe(
Effect.provideService(Scope.Scope, attemptScope),
Effect.onExit((exit) =>
Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void,
),
);
});
// OpenCode instances share opencode.db. Migration/WAL locks are transient;
// configuration and executable errors are not helped by launching again.
return yield* attempt.pipe(
Effect.retry({
times: 3,
schedule: Schedule.spaced("250 millis"),
while: (error) =>
error.operation === "startOpenCodeServerProcess" &&
/SQLITE_BUSY|database is locked/i.test(error.detail),
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
}),
);
});

const connectToOpenCodeServer: OpenCodeRuntime["Service"]["connectToOpenCodeServer"] = (
input,
) => {
Expand Down
Loading