fix(profile-state): keep the SQLite failure behind a startup open error across the writer worker (#29280)

This commit is contained in:
Neil
2026-10-15 18:40:21 -07:00
committed by GitHub
parent 7c23f79622
commit 69fcd672d8
3 changed files with 102 additions and 3 deletions
@@ -56,8 +56,11 @@ export function encodeProfileStateWriterError(
if (error instanceof ProfileStateDocumentCorruptionError) {
return { code: error.code, message: error.message, outcome, domain: error.domain }
}
if (error instanceof ProfileStateDatabaseOpenError) {
const cause = encodeOpenErrorCause(error.cause)
return { code: error.code, message: error.message, outcome, ...(cause ? { cause } : {}) }
}
if (
error instanceof ProfileStateDatabaseOpenError ||
error instanceof ProfileStateWriterError ||
error instanceof ProfileStateIndeterminateWriteError ||
error instanceof ProfileStateReadRollbackError
@@ -88,8 +91,29 @@ export function decodeProfileStateWriterError(data: ProfileStateWriterErrorData)
data.code === 'identity-mismatch' ||
data.code === 'invalid-profile-id'
) {
return new ProfileStateDatabaseOpenError(data.code, data.message)
return new ProfileStateDatabaseOpenError(
data.code,
data.message,
data.cause ? Object.assign(new Error(data.cause.message), data.cause) : undefined
)
}
}
return new ProfileStateWriterError(data.code, data.message, data.outcome)
}
// Why: open failures are rare and fatal to startup; without the SQLite code the log cannot tell
// a lock race from real damage (#27333). Messages are SQLite's or our validators', not user data.
function encodeOpenErrorCause(cause: unknown): ProfileStateWriterErrorData['cause'] {
if (!(cause instanceof Error)) {
return undefined
}
const code = 'code' in cause && typeof cause.code === 'string' ? cause.code : undefined
// node:sqlite's extended result code (e.g. 261 = SQLITE_BUSY_RECOVERY) is the precise verdict.
const errcode =
'errcode' in cause && typeof cause.errcode === 'number' ? cause.errcode : undefined
return {
message: cause.message.slice(0, 500),
...(code ? { code } : {}),
...(errcode !== undefined ? { errcode } : {})
}
}
@@ -0,0 +1,63 @@
import { expect, it } from 'vitest'
import Database from '../../sqlite/sync-database'
import {
decodeProfileStateWriterError,
encodeProfileStateWriterError
} from './profile-state-writer-errors'
import { ProfileStateDatabaseOpenError } from './profile-state-database-errors'
function acrossWorker(error: unknown): Error {
return decodeProfileStateWriterError(
JSON.parse(JSON.stringify(encodeProfileStateWriterError(error)))
)
}
// #27333: the startup failure logged `cause: undefined`, hiding which SQLite call failed.
it('keeps the SQLite failure behind a database open error across the writer boundary', () => {
const sqliteFailure = Object.assign(new Error('database is locked'), {
code: 'ERR_SQLITE_ERROR',
errcode: 261
})
const decoded = acrossWorker(
new ProfileStateDatabaseOpenError(
'unreadable',
'Unable to initialize profile state database',
sqliteFailure
)
)
expect(decoded).toBeInstanceOf(ProfileStateDatabaseOpenError)
expect(decoded.cause).toBeInstanceOf(Error)
expect(decoded.cause).toMatchObject({
code: 'ERR_SQLITE_ERROR',
errcode: 261,
message: 'database is locked'
})
})
it('carries a real SQLite error code from a failed open', () => {
const db = new Database(':memory:')
let failure: unknown
try {
db.exec('SELECT * FROM missing_table')
} catch (error) {
failure = error
} finally {
db.close()
}
const decoded = acrossWorker(
new ProfileStateDatabaseOpenError('unreadable', 'open failed', failure)
)
expect(decoded.cause).toMatchObject({
code: 'ERR_SQLITE_ERROR',
errcode: 1,
message: expect.stringContaining('missing_table')
})
})
it('keeps a cause-less open error cause-less', () => {
const decoded = acrossWorker(new ProfileStateDatabaseOpenError('unreadable', 'gone'))
expect(decoded).toBeInstanceOf(ProfileStateDatabaseOpenError)
expect(decoded.cause).toBeUndefined()
})
@@ -30,6 +30,8 @@ export type ProfileStateWriterErrorData = {
expectedRevision?: number
actualRevision?: number
domain?: string | null
/** The SQLite failure behind a database open error, which the error class alone cannot name. */
cause?: { code?: string; errcode?: number; message: string }
}
export type ProfileStateWriterResponse =
| { id: number; ok: true; revision: number; exportedRevision?: number | null }
@@ -97,7 +99,17 @@ function isErrorData(value: unknown): value is ProfileStateWriterErrorData {
(value.outcome === 'known-failure' || value.outcome === 'indeterminate') &&
(value.expectedRevision === undefined || isProfileStateRevision(value.expectedRevision)) &&
(value.actualRevision === undefined || isProfileStateRevision(value.actualRevision)) &&
(value.domain === undefined || value.domain === null || typeof value.domain === 'string')
(value.domain === undefined || value.domain === null || typeof value.domain === 'string') &&
(value.cause === undefined || isErrorCauseData(value.cause))
)
}
function isErrorCauseData(value: unknown): boolean {
return (
isRecord(value) &&
typeof value.message === 'string' &&
(value.code === undefined || typeof value.code === 'string') &&
(value.errcode === undefined || typeof value.errcode === 'number')
)
}