fix: add retry-once logic to isSqliteBackend for startup race condition

This commit is contained in:
YeonGyu-Kim 2026-02-16 16:52:25 +09:00
parent 49ed32308b
commit ed84b431fc
2 changed files with 92 additions and 15 deletions

View File

@ -15,15 +15,27 @@ const SQLITE_VERSION = "1.1.53"
// Other files (e.g., opencode-message-dir.test.ts) mock ./opencode-storage-detection globally, // Other files (e.g., opencode-message-dir.test.ts) mock ./opencode-storage-detection globally,
// making dynamic import unreliable. By inlining, we test the actual logic with controlled deps. // making dynamic import unreliable. By inlining, we test the actual logic with controlled deps.
const NOT_CACHED = Symbol("NOT_CACHED") const NOT_CACHED = Symbol("NOT_CACHED")
let cachedResult: boolean | typeof NOT_CACHED = NOT_CACHED const FALSE_PENDING_RETRY = Symbol("FALSE_PENDING_RETRY")
let cachedResult: true | false | typeof NOT_CACHED | typeof FALSE_PENDING_RETRY = NOT_CACHED
function isSqliteBackend(): boolean { function isSqliteBackend(): boolean {
if (cachedResult !== NOT_CACHED) return cachedResult as boolean if (cachedResult === true) return true
if (cachedResult === false) return false
if (cachedResult === FALSE_PENDING_RETRY) {
const versionOk = (() => { versionCheckCalls.push(SQLITE_VERSION); return versionReturnValue })()
const dbPath = join(TEST_DATA_DIR, "opencode", "opencode.db")
const dbExists = existsSync(dbPath)
const result = versionOk && dbExists
cachedResult = result
return result
}
const versionOk = (() => { versionCheckCalls.push(SQLITE_VERSION); return versionReturnValue })() const versionOk = (() => { versionCheckCalls.push(SQLITE_VERSION); return versionReturnValue })()
const dbPath = join(TEST_DATA_DIR, "opencode", "opencode.db") const dbPath = join(TEST_DATA_DIR, "opencode", "opencode.db")
const dbExists = existsSync(dbPath) const dbExists = existsSync(dbPath)
cachedResult = versionOk && dbExists const result = versionOk && dbExists
return cachedResult if (result) { cachedResult = true }
else { cachedResult = FALSE_PENDING_RETRY }
return result
} }
function resetSqliteBackendCache(): void { function resetSqliteBackendCache(): void {
@ -77,7 +89,7 @@ describe("isSqliteBackend", () => {
expect(versionCheckCalls).toContain("1.1.53") expect(versionCheckCalls).toContain("1.1.53")
}) })
it("caches the result and does not re-check on subsequent calls", () => { it("caches true permanently and does not re-check", () => {
//#given //#given
versionReturnValue = true versionReturnValue = true
mkdirSync(join(TEST_DATA_DIR, "opencode"), { recursive: true }) mkdirSync(join(TEST_DATA_DIR, "opencode"), { recursive: true })
@ -91,4 +103,59 @@ describe("isSqliteBackend", () => {
//#then //#then
expect(versionCheckCalls.length).toBe(1) expect(versionCheckCalls.length).toBe(1)
}) })
it("retries once when first result is false, then caches permanently", () => {
//#given
versionReturnValue = true
//#when: first call — DB does not exist
const first = isSqliteBackend()
//#then
expect(first).toBe(false)
expect(versionCheckCalls.length).toBe(1)
//#when: second call — DB still does not exist (retry)
const second = isSqliteBackend()
//#then: retried once
expect(second).toBe(false)
expect(versionCheckCalls.length).toBe(2)
//#when: third call — no more retries
const third = isSqliteBackend()
//#then: no further checks
expect(third).toBe(false)
expect(versionCheckCalls.length).toBe(2)
})
it("recovers on retry when DB appears after first false", () => {
//#given
versionReturnValue = true
//#when: first call — DB does not exist
const first = isSqliteBackend()
//#then
expect(first).toBe(false)
//#given: DB appears before retry
mkdirSync(join(TEST_DATA_DIR, "opencode"), { recursive: true })
writeFileSync(DB_PATH, "")
//#when: second call — retry finds DB
const second = isSqliteBackend()
//#then: recovers to true and caches permanently
expect(second).toBe(true)
expect(versionCheckCalls.length).toBe(2)
//#when: third call — cached true
const third = isSqliteBackend()
//#then: no further checks
expect(third).toBe(true)
expect(versionCheckCalls.length).toBe(2)
})
}) })

View File

@ -4,19 +4,29 @@ import { getDataDir } from "./data-path"
import { isOpenCodeVersionAtLeast, OPENCODE_SQLITE_VERSION } from "./opencode-version" import { isOpenCodeVersionAtLeast, OPENCODE_SQLITE_VERSION } from "./opencode-version"
const NOT_CACHED = Symbol("NOT_CACHED") const NOT_CACHED = Symbol("NOT_CACHED")
let cachedResult: boolean | typeof NOT_CACHED = NOT_CACHED const FALSE_PENDING_RETRY = Symbol("FALSE_PENDING_RETRY")
let cachedResult: true | false | typeof NOT_CACHED | typeof FALSE_PENDING_RETRY = NOT_CACHED
export function isSqliteBackend(): boolean { export function isSqliteBackend(): boolean {
if (cachedResult !== NOT_CACHED) { if (cachedResult === true) return true
return cachedResult if (cachedResult === false) return false
const check = (): boolean => {
const versionOk = isOpenCodeVersionAtLeast(OPENCODE_SQLITE_VERSION)
const dbPath = join(getDataDir(), "opencode", "opencode.db")
return versionOk && existsSync(dbPath)
} }
const versionOk = isOpenCodeVersionAtLeast(OPENCODE_SQLITE_VERSION) if (cachedResult === FALSE_PENDING_RETRY) {
const dbPath = join(getDataDir(), "opencode", "opencode.db") const result = check()
const dbExists = existsSync(dbPath) cachedResult = result
return result
cachedResult = versionOk && dbExists }
return cachedResult
const result = check()
if (result) { cachedResult = true }
else { cachedResult = FALSE_PENDING_RETRY }
return result
} }
export function resetSqliteBackendCache(): void { export function resetSqliteBackendCache(): void {