Run integrity checks on database corruption

This commit is contained in:
Fedor Indutny 2023-10-11 01:19:11 +02:00 committed by GitHub
parent 064659657f
commit f5c18cfb51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 76 additions and 14 deletions

View file

@ -911,6 +911,8 @@ export type ServerInterface = DataInterface & {
allStickers: ReadonlyArray<string> allStickers: ReadonlyArray<string>
) => Promise<Array<string>>; ) => Promise<Array<string>>;
getAllBadgeImageFileLocalPaths: () => Promise<Set<string>>; getAllBadgeImageFileLocalPaths: () => Promise<Set<string>>;
runCorruptionChecks: () => void;
}; };
export type GetRecentStoryRepliesOptionsType = { export type GetRecentStoryRepliesOptionsType = {

View file

@ -413,6 +413,8 @@ const dataInterface: ServerInterface = {
removeKnownStickers, removeKnownStickers,
removeKnownDraftAttachments, removeKnownDraftAttachments,
getAllBadgeImageFileLocalPaths, getAllBadgeImageFileLocalPaths,
runCorruptionChecks,
}; };
export default dataInterface; export default dataInterface;
@ -698,6 +700,27 @@ async function getWritableInstance(): Promise<Database> {
return globalWritableInstance; return globalWritableInstance;
} }
// This is okay to use for queries that:
//
// - Don't modify persistent tables, but create and do work in temporary
// tables
// - Integrity checks
//
function getUnsafeWritableInstance(
reason: 'only temp table use' | 'integrity check'
): Database {
// Not actually used
void reason;
if (!globalWritableInstance) {
throw new Error(
'getUnsafeWritableInstance: globalWritableInstance not set!'
);
}
return globalWritableInstance;
}
const IDENTITY_KEYS_TABLE = 'identityKeys'; const IDENTITY_KEYS_TABLE = 'identityKeys';
async function createOrUpdateIdentityKey( async function createOrUpdateIdentityKey(
data: StoredIdentityKeyType data: StoredIdentityKeyType
@ -1722,9 +1745,7 @@ async function searchMessages({
}): Promise<Array<ServerSearchResultMessageType>> { }): Promise<Array<ServerSearchResultMessageType>> {
const { limit = conversationId ? 100 : 500 } = options ?? {}; const { limit = conversationId ? 100 : 500 } = options ?? {};
// We don't actually write to the database, but temporary tables below const db = getUnsafeWritableInstance('only temp table use');
// require write access.
const db = await getWritableInstance();
// sqlite queries with a join on a virtual table (like FTS5) are de-optimized // sqlite queries with a join on a virtual table (like FTS5) are de-optimized
// and can't use indices for ordering results. Instead an in-memory index of // and can't use indices for ordering results. Instead an in-memory index of
@ -3637,7 +3658,7 @@ async function getCallHistoryGroupsCount(
): Promise<number> { ): Promise<number> {
// getCallHistoryGroupDataSync creates a temporary table and thus requires // getCallHistoryGroupDataSync creates a temporary table and thus requires
// write access. // write access.
const db = await getWritableInstance(); const db = getUnsafeWritableInstance('only temp table use');
const result = getCallHistoryGroupDataSync(db, true, filter, { const result = getCallHistoryGroupDataSync(db, true, filter, {
limit: 0, limit: 0,
offset: 0, offset: 0,
@ -3665,7 +3686,7 @@ async function getCallHistoryGroups(
): Promise<Array<CallHistoryGroup>> { ): Promise<Array<CallHistoryGroup>> {
// getCallHistoryGroupDataSync creates a temporary table and thus requires // getCallHistoryGroupDataSync creates a temporary table and thus requires
// write access. // write access.
const db = await getWritableInstance(); const db = getUnsafeWritableInstance('only temp table use');
const groupsData = groupsDataSchema.parse( const groupsData = groupsDataSchema.parse(
getCallHistoryGroupDataSync(db, false, filter, pagination) getCallHistoryGroupDataSync(db, false, filter, pagination)
); );
@ -5191,6 +5212,32 @@ async function getAllBadgeImageFileLocalPaths(): Promise<Set<string>> {
return new Set(localPaths); return new Set(localPaths);
} }
function runCorruptionChecks(): void {
const db = getUnsafeWritableInstance('integrity check');
try {
const result = db.pragma('integrity_check');
if (result.length === 1 && result.at(0)?.integrity_check === 'ok') {
logger.info('runCorruptionChecks: general integrity is ok');
} else {
logger.error('runCorruptionChecks: general integrity is not ok', result);
}
} catch (error) {
logger.error(
'runCorruptionChecks: general integrity check error',
Errors.toLogFormat(error)
);
}
try {
db.exec("INSERT INTO messages_fts(messages_fts) VALUES('integrity-check')");
logger.info('runCorruptionChecks: FTS5 integrity ok');
} catch (error) {
logger.error(
'runCorruptionChecks: FTS5 integrity check error.',
Errors.toLogFormat(error)
);
}
}
type StoryDistributionForDatabase = Readonly< type StoryDistributionForDatabase = Readonly<
{ {
allowsReplies: 0 | 1; allowsReplies: 0 | 1;

View file

@ -2,9 +2,9 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
export enum SqliteErrorKind { export enum SqliteErrorKind {
Corrupted, Corrupted = 'Corrupted',
Readonly, Readonly = 'Readonly',
Unknown, Unknown = 'Unknown',
} }
export function parseSqliteError(error?: Error): SqliteErrorKind { export function parseSqliteError(error?: Error): SqliteErrorKind {

View file

@ -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 { parseSqliteError, SqliteErrorKind } from './errors'; import { SqliteErrorKind } from './errors';
import type DB from './Server'; import type DB from './Server';
const MIN_TRACE_DURATION = 40; const MIN_TRACE_DURATION = 40;
@ -54,6 +54,7 @@ export type WrappedWorkerResponse =
type: 'response'; type: 'response';
seq: number; seq: number;
error: string | undefined; error: string | undefined;
errorKind: SqliteErrorKind | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
response: any; response: any;
}> }>
@ -102,7 +103,7 @@ export class MainSQL {
return; return;
} }
const { seq, error, response } = wrappedResponse; const { seq, error, errorKind, response } = wrappedResponse;
const pair = this.onResponse.get(seq); const pair = this.onResponse.get(seq);
this.onResponse.delete(seq); this.onResponse.delete(seq);
@ -112,7 +113,7 @@ export class MainSQL {
if (error) { if (error) {
const errorObj = new Error(error); const errorObj = new Error(error);
this.onError(errorObj); this.onError(errorKind ?? SqliteErrorKind.Unknown, errorObj);
pair.reject(errorObj); pair.reject(errorObj);
} else { } else {
@ -227,8 +228,7 @@ export class MainSQL {
return result; return result;
} }
private onError(error: Error): void { private onError(errorKind: SqliteErrorKind, error: Error): void {
const errorKind = parseSqliteError(error);
if (errorKind === SqliteErrorKind.Unknown) { if (errorKind === SqliteErrorKind.Unknown) {
return; return;
} }

View file

@ -11,6 +11,7 @@ import type {
WrappedWorkerLogEntry, WrappedWorkerLogEntry,
} from './main'; } from './main';
import db from './Server'; import db from './Server';
import { SqliteErrorKind, parseSqliteError } from './errors';
if (!parentPort) { if (!parentPort) {
throw new Error('Must run as a worker thread'); throw new Error('Must run as a worker thread');
@ -20,10 +21,22 @@ const port = parentPort;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function respond(seq: number, error: Error | undefined, response?: any) { function respond(seq: number, error: Error | undefined, response?: any) {
let errorKind: SqliteErrorKind | undefined;
let errorString: string | undefined;
if (error !== undefined) {
errorKind = parseSqliteError(error);
errorString = Errors.toLogFormat(error);
if (errorKind === SqliteErrorKind.Corrupted) {
db.runCorruptionChecks();
}
}
const wrappedResponse: WrappedWorkerResponse = { const wrappedResponse: WrappedWorkerResponse = {
type: 'response', type: 'response',
seq, seq,
error: error === undefined ? undefined : Errors.toLogFormat(error), error: errorString,
errorKind,
response, response,
}; };
port.postMessage(wrappedResponse); port.postMessage(wrappedResponse);