Graceful handling of readonly DB error
This commit is contained in:
parent
1e7d658109
commit
bd40a7fb98
4 changed files with 98 additions and 27 deletions
20
app/main.ts
20
app/main.ts
|
@ -1573,6 +1573,20 @@ const runSQLCorruptionHandler = async () => {
|
||||||
await onDatabaseError(Errors.toLogFormat(error));
|
await onDatabaseError(Errors.toLogFormat(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runSQLReadonlyHandler = async () => {
|
||||||
|
// This is a glorified event handler. Normally, this promise never resolves,
|
||||||
|
// but if there is a corruption error triggered by any query that we run
|
||||||
|
// against the database - the promise will resolve and we will call
|
||||||
|
// `onDatabaseError`.
|
||||||
|
const error = await sql.whenReadonly();
|
||||||
|
|
||||||
|
getLogger().error(
|
||||||
|
`Detected readonly sql database in main process: ${error.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
|
||||||
async function initializeSQL(
|
async function initializeSQL(
|
||||||
userDataPath: string
|
userDataPath: string
|
||||||
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
|
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
|
||||||
|
@ -1619,6 +1633,7 @@ async function initializeSQL(
|
||||||
|
|
||||||
// Only if we've initialized things successfully do we set up the corruption handler
|
// Only if we've initialized things successfully do we set up the corruption handler
|
||||||
drop(runSQLCorruptionHandler());
|
drop(runSQLCorruptionHandler());
|
||||||
|
drop(runSQLReadonlyHandler());
|
||||||
|
|
||||||
return { ok: true, error: undefined };
|
return { ok: true, error: undefined };
|
||||||
}
|
}
|
||||||
|
@ -1669,6 +1684,11 @@ ipc.on('database-error', (_event: Electron.Event, error: string) => {
|
||||||
drop(onDatabaseError(error));
|
drop(onDatabaseError(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('database-readonly', (_event: Electron.Event, error: string) => {
|
||||||
|
// Just let global_errors.ts handle it
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
|
||||||
function loadPreferredSystemLocales(): Array<string> {
|
function loadPreferredSystemLocales(): Array<string> {
|
||||||
return getEnvironment() === Environment.Test
|
return getEnvironment() === Environment.Test
|
||||||
? ['en']
|
? ['en']
|
||||||
|
|
|
@ -65,7 +65,7 @@ import type {
|
||||||
StoredSignedPreKeyType,
|
StoredSignedPreKeyType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
import Server from './Server';
|
import Server from './Server';
|
||||||
import { isCorruptionError } from './errors';
|
import { parseSqliteError, SqliteErrorKind } from './errors';
|
||||||
import { MINUTE } from '../util/durations';
|
import { MINUTE } from '../util/durations';
|
||||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
|
|
||||||
|
@ -309,13 +309,18 @@ function makeChannel(fnName: string) {
|
||||||
// Ignoring this error TS2556: Expected 3 arguments, but got 0 or more.
|
// Ignoring this error TS2556: Expected 3 arguments, but got 0 or more.
|
||||||
return await serverFn(...args);
|
return await serverFn(...args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isCorruptionError(error)) {
|
const sqliteErrorKind = parseSqliteError(error);
|
||||||
|
if (sqliteErrorKind === SqliteErrorKind.Corrupted) {
|
||||||
log.error(
|
log.error(
|
||||||
'Detected sql corruption in renderer process. ' +
|
'Detected sql corruption in renderer process. ' +
|
||||||
`Restarting the application immediately. Error: ${error.message}`
|
`Restarting the application immediately. Error: ${error.message}`
|
||||||
);
|
);
|
||||||
ipc?.send('database-error', error.stack);
|
ipc?.send('database-error', error.stack);
|
||||||
|
} else if (sqliteErrorKind === SqliteErrorKind.Readonly) {
|
||||||
|
log.error(`Detected readonly sql database: ${error.message}`);
|
||||||
|
ipc?.send('database-readonly');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error(
|
log.error(
|
||||||
`Renderer SQL channel job (${fnName}) error ${error.message}`
|
`Renderer SQL channel job (${fnName}) error ${error.message}`
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,32 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
export function isCorruptionError(error?: Error): boolean {
|
export enum SqliteErrorKind {
|
||||||
return (
|
Corrupted,
|
||||||
error?.message?.includes('SQLITE_CORRUPT') ||
|
Readonly,
|
||||||
error?.message?.includes('database disk image is malformed') ||
|
Unknown,
|
||||||
error?.message?.includes('file is not a database') ||
|
}
|
||||||
false
|
|
||||||
);
|
export function parseSqliteError(error?: Error): SqliteErrorKind {
|
||||||
|
const message = error?.message;
|
||||||
|
if (!message) {
|
||||||
|
return SqliteErrorKind.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.includes('SQLITE_CORRUPT') ||
|
||||||
|
message.includes('database disk image is malformed') ||
|
||||||
|
message.includes('file is not a database')
|
||||||
|
) {
|
||||||
|
return SqliteErrorKind.Corrupted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.includes('SQLITE_READONLY') ||
|
||||||
|
message.includes('attempt to write a readonly database')
|
||||||
|
) {
|
||||||
|
return SqliteErrorKind.Readonly;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SqliteErrorKind.Unknown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { app } from 'electron';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { explodePromise } from '../util/explodePromise';
|
import { explodePromise } from '../util/explodePromise';
|
||||||
import type { LoggerType } from '../types/Logging';
|
import type { LoggerType } from '../types/Logging';
|
||||||
import { isCorruptionError } from './errors';
|
import { parseSqliteError, SqliteErrorKind } from './errors';
|
||||||
import type DB from './Server';
|
import type DB from './Server';
|
||||||
|
|
||||||
const MIN_TRACE_DURATION = 40;
|
const MIN_TRACE_DURATION = 40;
|
||||||
|
@ -64,6 +64,11 @@ type PromisePair<T> = {
|
||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type KnownErrorResolverType = Readonly<{
|
||||||
|
kind: SqliteErrorKind;
|
||||||
|
resolve: (err: Error) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
export class MainSQL {
|
export class MainSQL {
|
||||||
private readonly worker: Worker;
|
private readonly worker: Worker;
|
||||||
|
|
||||||
|
@ -73,9 +78,8 @@ export class MainSQL {
|
||||||
|
|
||||||
private readonly onExit: Promise<void>;
|
private readonly onExit: Promise<void>;
|
||||||
|
|
||||||
// This promise is resolved when any of the queries that we run against the
|
// Promise resolve callbacks for corruption and readonly errors.
|
||||||
// database reject with a corruption error (see `isCorruptionError`)
|
private errorResolvers = new Array<KnownErrorResolverType>();
|
||||||
private readonly onCorruption: Promise<Error>;
|
|
||||||
|
|
||||||
private seq = 0;
|
private seq = 0;
|
||||||
|
|
||||||
|
@ -88,10 +92,6 @@ export class MainSQL {
|
||||||
const scriptDir = join(app.getAppPath(), 'ts', 'sql', 'mainWorker.js');
|
const scriptDir = join(app.getAppPath(), 'ts', 'sql', 'mainWorker.js');
|
||||||
this.worker = new Worker(scriptDir);
|
this.worker = new Worker(scriptDir);
|
||||||
|
|
||||||
const { promise: onCorruption, resolve: resolveCorruption } =
|
|
||||||
explodePromise<Error>();
|
|
||||||
this.onCorruption = onCorruption;
|
|
||||||
|
|
||||||
this.worker.on('message', (wrappedResponse: WrappedWorkerResponse) => {
|
this.worker.on('message', (wrappedResponse: WrappedWorkerResponse) => {
|
||||||
if (wrappedResponse.type === 'log') {
|
if (wrappedResponse.type === 'log') {
|
||||||
const { level, args } = wrappedResponse;
|
const { level, args } = wrappedResponse;
|
||||||
|
@ -110,9 +110,7 @@ export class MainSQL {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorObj = new Error(error);
|
const errorObj = new Error(error);
|
||||||
if (isCorruptionError(errorObj)) {
|
this.onError(errorObj);
|
||||||
resolveCorruption(errorObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
pair.reject(errorObj);
|
pair.reject(errorObj);
|
||||||
} else {
|
} else {
|
||||||
|
@ -120,9 +118,9 @@ export class MainSQL {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onExit = new Promise<void>(resolve => {
|
const { promise: onExit, resolve: resolveOnExit } = explodePromise<void>();
|
||||||
this.worker.once('exit', resolve);
|
this.onExit = onExit;
|
||||||
});
|
this.worker.once('exit', resolveOnExit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initialize({
|
public async initialize({
|
||||||
|
@ -148,7 +146,15 @@ export class MainSQL {
|
||||||
}
|
}
|
||||||
|
|
||||||
public whenCorrupted(): Promise<Error> {
|
public whenCorrupted(): Promise<Error> {
|
||||||
return this.onCorruption;
|
const { promise, resolve } = explodePromise<Error>();
|
||||||
|
this.errorResolvers.push({ kind: SqliteErrorKind.Corrupted, resolve });
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public whenReadonly(): Promise<Error> {
|
||||||
|
const { promise, resolve } = explodePromise<Error>();
|
||||||
|
this.errorResolvers.push({ kind: SqliteErrorKind.Readonly, resolve });
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async close(): Promise<void> {
|
public async close(): Promise<void> {
|
||||||
|
@ -199,9 +205,8 @@ export class MainSQL {
|
||||||
const { seq } = this;
|
const { seq } = this;
|
||||||
this.seq += 1;
|
this.seq += 1;
|
||||||
|
|
||||||
const result = new Promise<Response>((resolve, reject) => {
|
const { promise: result, resolve, reject } = explodePromise<Response>();
|
||||||
this.onResponse.set(seq, { resolve, reject });
|
this.onResponse.set(seq, { resolve, reject });
|
||||||
});
|
|
||||||
|
|
||||||
const wrappedRequest: WrappedWorkerRequest = {
|
const wrappedRequest: WrappedWorkerRequest = {
|
||||||
seq,
|
seq,
|
||||||
|
@ -211,4 +216,24 @@ export class MainSQL {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onError(error: Error): void {
|
||||||
|
const errorKind = parseSqliteError(error);
|
||||||
|
if (errorKind === SqliteErrorKind.Unknown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvers = new Array<(error: Error) => void>();
|
||||||
|
this.errorResolvers = this.errorResolvers.filter(entry => {
|
||||||
|
if (entry.kind === errorKind) {
|
||||||
|
resolvers.push(entry.resolve);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const resolve of resolvers) {
|
||||||
|
resolve(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue