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>
 | 
			
		||||
  ) => Promise<Array<string>>;
 | 
			
		||||
  getAllBadgeImageFileLocalPaths: () => Promise<Set<string>>;
 | 
			
		||||
 | 
			
		||||
  runCorruptionChecks: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type GetRecentStoryRepliesOptionsType = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -413,6 +413,8 @@ const dataInterface: ServerInterface = {
 | 
			
		|||
  removeKnownStickers,
 | 
			
		||||
  removeKnownDraftAttachments,
 | 
			
		||||
  getAllBadgeImageFileLocalPaths,
 | 
			
		||||
 | 
			
		||||
  runCorruptionChecks,
 | 
			
		||||
};
 | 
			
		||||
export default dataInterface;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -698,6 +700,27 @@ async function getWritableInstance(): Promise<Database> {
 | 
			
		|||
  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';
 | 
			
		||||
async function createOrUpdateIdentityKey(
 | 
			
		||||
  data: StoredIdentityKeyType
 | 
			
		||||
| 
						 | 
				
			
			@ -1722,9 +1745,7 @@ async function searchMessages({
 | 
			
		|||
}): Promise<Array<ServerSearchResultMessageType>> {
 | 
			
		||||
  const { limit = conversationId ? 100 : 500 } = options ?? {};
 | 
			
		||||
 | 
			
		||||
  // We don't actually write to the database, but temporary tables below
 | 
			
		||||
  // require write access.
 | 
			
		||||
  const db = await getWritableInstance();
 | 
			
		||||
  const db = getUnsafeWritableInstance('only temp table use');
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
| 
						 | 
				
			
			@ -3637,7 +3658,7 @@ async function getCallHistoryGroupsCount(
 | 
			
		|||
): Promise<number> {
 | 
			
		||||
  // getCallHistoryGroupDataSync creates a temporary table and thus requires
 | 
			
		||||
  // write access.
 | 
			
		||||
  const db = await getWritableInstance();
 | 
			
		||||
  const db = getUnsafeWritableInstance('only temp table use');
 | 
			
		||||
  const result = getCallHistoryGroupDataSync(db, true, filter, {
 | 
			
		||||
    limit: 0,
 | 
			
		||||
    offset: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -3665,7 +3686,7 @@ async function getCallHistoryGroups(
 | 
			
		|||
): Promise<Array<CallHistoryGroup>> {
 | 
			
		||||
  // getCallHistoryGroupDataSync creates a temporary table and thus requires
 | 
			
		||||
  // write access.
 | 
			
		||||
  const db = await getWritableInstance();
 | 
			
		||||
  const db = getUnsafeWritableInstance('only temp table use');
 | 
			
		||||
  const groupsData = groupsDataSchema.parse(
 | 
			
		||||
    getCallHistoryGroupDataSync(db, false, filter, pagination)
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -5191,6 +5212,32 @@ async function getAllBadgeImageFileLocalPaths(): Promise<Set<string>> {
 | 
			
		|||
  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<
 | 
			
		||||
  {
 | 
			
		||||
    allowsReplies: 0 | 1;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,9 +2,9 @@
 | 
			
		|||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
export enum SqliteErrorKind {
 | 
			
		||||
  Corrupted,
 | 
			
		||||
  Readonly,
 | 
			
		||||
  Unknown,
 | 
			
		||||
  Corrupted = 'Corrupted',
 | 
			
		||||
  Readonly = 'Readonly',
 | 
			
		||||
  Unknown = 'Unknown',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function parseSqliteError(error?: Error): SqliteErrorKind {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import { app } from 'electron';
 | 
			
		|||
import { strictAssert } from '../util/assert';
 | 
			
		||||
import { explodePromise } from '../util/explodePromise';
 | 
			
		||||
import type { LoggerType } from '../types/Logging';
 | 
			
		||||
import { parseSqliteError, SqliteErrorKind } from './errors';
 | 
			
		||||
import { SqliteErrorKind } from './errors';
 | 
			
		||||
import type DB from './Server';
 | 
			
		||||
 | 
			
		||||
const MIN_TRACE_DURATION = 40;
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +54,7 @@ export type WrappedWorkerResponse =
 | 
			
		|||
      type: 'response';
 | 
			
		||||
      seq: number;
 | 
			
		||||
      error: string | undefined;
 | 
			
		||||
      errorKind: SqliteErrorKind | undefined;
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
      response: any;
 | 
			
		||||
    }>
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +103,7 @@ export class MainSQL {
 | 
			
		|||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { seq, error, response } = wrappedResponse;
 | 
			
		||||
      const { seq, error, errorKind, response } = wrappedResponse;
 | 
			
		||||
 | 
			
		||||
      const pair = this.onResponse.get(seq);
 | 
			
		||||
      this.onResponse.delete(seq);
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +113,7 @@ export class MainSQL {
 | 
			
		|||
 | 
			
		||||
      if (error) {
 | 
			
		||||
        const errorObj = new Error(error);
 | 
			
		||||
        this.onError(errorObj);
 | 
			
		||||
        this.onError(errorKind ?? SqliteErrorKind.Unknown, errorObj);
 | 
			
		||||
 | 
			
		||||
        pair.reject(errorObj);
 | 
			
		||||
      } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -227,8 +228,7 @@ export class MainSQL {
 | 
			
		|||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private onError(error: Error): void {
 | 
			
		||||
    const errorKind = parseSqliteError(error);
 | 
			
		||||
  private onError(errorKind: SqliteErrorKind, error: Error): void {
 | 
			
		||||
    if (errorKind === SqliteErrorKind.Unknown) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import type {
 | 
			
		|||
  WrappedWorkerLogEntry,
 | 
			
		||||
} from './main';
 | 
			
		||||
import db from './Server';
 | 
			
		||||
import { SqliteErrorKind, parseSqliteError } from './errors';
 | 
			
		||||
 | 
			
		||||
if (!parentPort) {
 | 
			
		||||
  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
 | 
			
		||||
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 = {
 | 
			
		||||
    type: 'response',
 | 
			
		||||
    seq,
 | 
			
		||||
    error: error === undefined ? undefined : Errors.toLogFormat(error),
 | 
			
		||||
    error: errorString,
 | 
			
		||||
    errorKind,
 | 
			
		||||
    response,
 | 
			
		||||
  };
 | 
			
		||||
  port.postMessage(wrappedResponse);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue