Compare commits

...

1 Commits

Author SHA1 Message Date
Dax 96902d1a10 fix(core): disable WAL on network filesystems 2026-08-18 00:25:11 +00:00
7 changed files with 83 additions and 7 deletions
+1
View File
@@ -89,6 +89,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
? "opencode.db"
: `opencode-${OPENCODE_CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`),
wal: process.env.OPENCODE_DB_WAL === undefined ? undefined : truthy(process.env.OPENCODE_DB_WAL),
},
models: {
url: process.env.OPENCODE_MODELS_URL,
+3 -3
View File
@@ -18,6 +18,7 @@ export interface Interface {
export const Options = Schema.Struct({
path: Schema.optional(Schema.String),
wal: Schema.optional(Schema.Boolean),
})
export type Options = typeof Options.Type
@@ -29,11 +30,9 @@ const databaseLayer = Layer.effect(
const db = yield* makeDatabase
if (supportsTuningPragmas) {
yield* db.run("PRAGMA journal_mode = WAL")
yield* db.run("PRAGMA synchronous = NORMAL")
yield* db.run("PRAGMA busy_timeout = 5000")
yield* db.run("PRAGMA cache_size = -64000")
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
}
// Durable Object SQLite always enforces foreign keys and rejects the pragma.
if (supportsForeignKeyToggle) yield* db.run("PRAGMA foreign_keys = ON")
@@ -46,7 +45,8 @@ const databaseLayer = Layer.effect(
export function layer(options: Options = { path: ":memory:" }) {
return Layer.unwrap(
Effect.gen(function* () {
const provide = (filename: string) => layerFromClient.pipe(Layer.provide(sqliteLayer({ filename })))
const provide = (filename: string) =>
layerFromClient.pipe(Layer.provide(sqliteLayer({ filename, wal: options.wal })))
const filename = options.path ?? ":memory:"
if (filename === ":memory:" || isAbsolute(filename)) return provide(filename)
const global = yield* Global.Service
@@ -0,0 +1,17 @@
import { statfsSync } from "node:fs"
const NETWORK_FILESYSTEM_TYPES = new Set([
0x0000517b, // SMB
0x01021997, // 9P
0x65735546, // FUSE (including VirtioFS and SSHFS)
0x00006969, // NFS
0xff534d42, // CIFS
])
export function isNetworkFilesystemType(type: number) {
return NETWORK_FILESYSTEM_TYPES.has(type >>> 0)
}
export function isNetworkFilesystem(filename: string) {
return isNetworkFilesystemType(statfsSync(filename).type)
}
+8 -2
View File
@@ -3,6 +3,7 @@ import { Context, Effect, Layer } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient } from "effect/unstable/sql"
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
import { isNetworkFilesystem } from "./network-filesystem.js"
import { Sqlite } from "./sqlite.js"
const TypeId = "~@opencode-ai/core/database/SqliteBun" as const
@@ -17,7 +18,7 @@ interface Config extends Sqlite.ClientConfig {
readonly readonly?: boolean
readonly create?: boolean
readonly readwrite?: boolean
readonly disableWAL?: boolean
readonly wal?: boolean
}
const make = (options: Config) =>
@@ -90,7 +91,12 @@ const nativeLayer = (config: Config) =>
create: config.create ?? true,
})
yield* Effect.addFinalizer(() => Effect.sync(() => native.close()))
if (config.disableWAL !== true) native.run("PRAGMA journal_mode = WAL;")
const wal = config.filename !== ":memory:" && (config.wal ?? !isNetworkFilesystem(config.filename))
if (wal) {
native.run("PRAGMA journal_mode = WAL;")
native.run("PRAGMA wal_checkpoint(PASSIVE);")
}
if (!wal && config.filename !== ":memory:") native.run("PRAGMA journal_mode = DELETE;")
return native
}),
)
+9 -2
View File
@@ -3,6 +3,7 @@ import { Context, Effect, Layer } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient } from "effect/unstable/sql"
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
import { isNetworkFilesystem } from "./network-filesystem.js"
import { Sqlite } from "./sqlite.js"
const TypeId = "~@opencode-ai/core/database/SqliteNode" as const
@@ -17,7 +18,7 @@ interface Config extends Sqlite.ClientConfig {
readonly readonly?: boolean
readonly create?: boolean
readonly readwrite?: boolean
readonly disableWAL?: boolean
readonly wal?: boolean
readonly timeout?: number
readonly allowExtension?: boolean
}
@@ -87,7 +88,13 @@ const nativeLayer = (config: Config) =>
open: true,
})
yield* Effect.addFinalizer(() => Effect.sync(() => native.close()))
if (config.disableWAL !== true && config.readonly !== true) native.exec("PRAGMA journal_mode = WAL;")
const wal = config.filename !== ":memory:" && (config.wal ?? !isNetworkFilesystem(config.filename))
if (wal && config.readonly !== true) {
native.exec("PRAGMA journal_mode = WAL;")
native.exec("PRAGMA wal_checkpoint(PASSIVE);")
}
if (!wal && config.filename !== ":memory:" && config.readonly !== true)
native.exec("PRAGMA journal_mode = DELETE;")
return native
}),
)
@@ -0,0 +1,42 @@
import { expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { Database } from "@opencode-ai/core/database/database"
import { isNetworkFilesystemType } from "@opencode-ai/core/database/network-filesystem"
import { Global } from "@opencode-ai/util/global"
import { sql } from "drizzle-orm"
import { Effect } from "effect"
test.each([
["SMB", 0x0000517b],
["9P", 0x01021997],
["FUSE", 0x65735546],
["NFS", 0x00006969],
["CIFS", 0xff534d42],
])("disables WAL on %s", (_name, type) => {
expect(isNetworkFilesystemType(type)).toBe(true)
})
test("keeps WAL on local filesystems", () => {
expect(isNetworkFilesystemType(0xef53)).toBe(false)
})
test("allows WAL to be disabled explicitly", async () => {
const directory = await mkdtemp(join(tmpdir(), "opencode-database-"))
try {
const mode = await Effect.runPromise(
Effect.gen(function* () {
const database = yield* Database.Service
return yield* database.db.all<{ journal_mode: string }>(sql`PRAGMA journal_mode`)
}).pipe(
Effect.provide(Database.layer({ path: join(directory, "opencode.db"), wal: false })),
Effect.provideService(Global.Service, Global.make({ data: directory })),
Effect.scoped,
),
)
expect(mode).toEqual([{ journal_mode: "delete" }])
} finally {
await rm(directory, { recursive: true, force: true })
}
})
@@ -99,6 +99,9 @@ The database normally lives at:
`OPENCODE_DB` can override the database location.
OpenCode disables SQLite WAL mode when it detects NFS or another shared network filesystem. Set
`OPENCODE_DB_WAL=true` to force WAL mode or `OPENCODE_DB_WAL=false` to disable it explicitly.
<Callout type="warning">
Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the
daemon, and make a backup before inspecting persistent data with external tools.