Run integrity checks on database corruption
This commit is contained in:
parent
064659657f
commit
f5c18cfb51
5 changed files with 76 additions and 14 deletions
|
@ -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 = {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue