Periodically optimize FTS table
This commit is contained in:
parent
e8ed8b3400
commit
5dfdde998b
7 changed files with 131 additions and 0 deletions
|
@ -55,6 +55,7 @@ import { senderCertificateService } from './services/senderCertificate';
|
||||||
import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher';
|
import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher';
|
||||||
import * as KeyboardLayout from './services/keyboardLayout';
|
import * as KeyboardLayout from './services/keyboardLayout';
|
||||||
import * as StorageService from './services/storage';
|
import * as StorageService from './services/storage';
|
||||||
|
import { optimizeFTS } from './services/ftsOptimizer';
|
||||||
import { RoutineProfileRefresher } from './routineProfileRefresh';
|
import { RoutineProfileRefresher } from './routineProfileRefresh';
|
||||||
import { isOlderThan, toDayMillis } from './util/timestamp';
|
import { isOlderThan, toDayMillis } from './util/timestamp';
|
||||||
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
|
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
|
||||||
|
@ -862,6 +863,8 @@ export async function startApp(): Promise<void> {
|
||||||
if (newVersion) {
|
if (newVersion) {
|
||||||
await window.Signal.Data.cleanupOrphanedAttachments();
|
await window.Signal.Data.cleanupOrphanedAttachments();
|
||||||
|
|
||||||
|
optimizeFTS();
|
||||||
|
|
||||||
// Don't block on the following operation
|
// Don't block on the following operation
|
||||||
void window.Signal.Data.ensureFilePermissions();
|
void window.Signal.Data.ensureFilePermissions();
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@ import { isNotNil } from '../util/isNotNil';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
import { notificationService } from '../services/notifications';
|
import { notificationService } from '../services/notifications';
|
||||||
import { storageServiceUploadJob } from '../services/storage';
|
import { storageServiceUploadJob } from '../services/storage';
|
||||||
|
import { scheduleOptimizeFTS } from '../services/ftsOptimizer';
|
||||||
import { getSendOptions } from '../util/getSendOptions';
|
import { getSendOptions } from '../util/getSendOptions';
|
||||||
import { isConversationAccepted } from '../util/isConversationAccepted';
|
import { isConversationAccepted } from '../util/isConversationAccepted';
|
||||||
import {
|
import {
|
||||||
|
@ -5039,6 +5040,8 @@ export class ConversationModel extends window.Backbone
|
||||||
await window.Signal.Data.removeAllMessagesInConversation(this.id, {
|
await window.Signal.Data.removeAllMessagesInConversation(this.id, {
|
||||||
logId: this.idForLogging(),
|
logId: this.idForLogging(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scheduleOptimizeFTS();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle(options?: { isShort?: boolean }): string {
|
getTitle(options?: { isShort?: boolean }): string {
|
||||||
|
|
|
@ -79,6 +79,7 @@ import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
|
||||||
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
||||||
import { getOwn } from '../util/getOwn';
|
import { getOwn } from '../util/getOwn';
|
||||||
import { markRead, markViewed } from '../services/MessageUpdater';
|
import { markRead, markViewed } from '../services/MessageUpdater';
|
||||||
|
import { scheduleOptimizeFTS } from '../services/ftsOptimizer';
|
||||||
import { isMessageUnread } from '../util/isMessageUnread';
|
import { isMessageUnread } from '../util/isMessageUnread';
|
||||||
import {
|
import {
|
||||||
isDirectConversation,
|
isDirectConversation,
|
||||||
|
@ -1184,6 +1185,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.Signal.Data.deleteSentProtoByMessageId(this.id);
|
await window.Signal.Data.deleteSentProtoByMessageId(this.id);
|
||||||
|
|
||||||
|
scheduleOptimizeFTS();
|
||||||
}
|
}
|
||||||
|
|
||||||
override isEmpty(): boolean {
|
override isEmpty(): boolean {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
import { sleep } from '../util/sleep';
|
import { sleep } from '../util/sleep';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
|
import { scheduleOptimizeFTS } from './ftsOptimizer';
|
||||||
|
|
||||||
class ExpiringMessagesDeletionService {
|
class ExpiringMessagesDeletionService {
|
||||||
public update: typeof this.checkExpiringMessages;
|
public update: typeof this.checkExpiringMessages;
|
||||||
|
@ -64,6 +65,10 @@ class ExpiringMessagesDeletionService {
|
||||||
conversation.decrementMessageCount();
|
conversation.decrementMessageCount();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (messages.length > 0) {
|
||||||
|
scheduleOptimizeFTS();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.SignalContext.log.error(
|
window.SignalContext.log.error(
|
||||||
'destroyExpiredMessages: Error deleting expired messages',
|
'destroyExpiredMessages: Error deleting expired messages',
|
||||||
|
|
63
ts/services/ftsOptimizer.ts
Normal file
63
ts/services/ftsOptimizer.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { SECOND } from '../util/durations';
|
||||||
|
import { sleep } from '../util/sleep';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
import dataInterface from '../sql/Client';
|
||||||
|
import type { FTSOptimizationStateType } from '../sql/Interface';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
|
const INTERACTIVITY_DELAY_MS = 50;
|
||||||
|
|
||||||
|
class FTSOptimizer {
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
log.info('ftsOptimizer: starting');
|
||||||
|
|
||||||
|
let state: FTSOptimizationStateType | undefined;
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
if (state !== undefined) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep(INTERACTIVITY_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
state = await dataInterface.optimizeFTS(state);
|
||||||
|
} while (!state?.done);
|
||||||
|
} finally {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
log.warn('ftsOptimizer: no final state');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`ftsOptimizer: took ${duration}ms and ${state.steps} steps`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const optimizer = new FTSOptimizer();
|
||||||
|
|
||||||
|
export const optimizeFTS = (): void => {
|
||||||
|
drop(optimizer.run());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scheduleOptimizeFTS = debounce(optimizeFTS, SECOND, {
|
||||||
|
maxWait: 5 * SECOND,
|
||||||
|
});
|
|
@ -360,6 +360,12 @@ export type GetAllStoriesResultType = ReadonlyArray<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type FTSOptimizationStateType = Readonly<{
|
||||||
|
changes: number;
|
||||||
|
steps: number;
|
||||||
|
done?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type DataInterface = {
|
export type DataInterface = {
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
removeDB: () => Promise<void>;
|
removeDB: () => Promise<void>;
|
||||||
|
@ -704,6 +710,10 @@ export type DataInterface = {
|
||||||
getMaxMessageCounter(): Promise<number | undefined>;
|
getMaxMessageCounter(): Promise<number | undefined>;
|
||||||
|
|
||||||
getStatisticsForLogging(): Promise<Record<string, string>>;
|
getStatisticsForLogging(): Promise<Record<string, string>>;
|
||||||
|
|
||||||
|
optimizeFTS: (
|
||||||
|
state?: FTSOptimizationStateType
|
||||||
|
) => Promise<FTSOptimizationStateType | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerInterface = DataInterface & {
|
export type ServerInterface = DataInterface & {
|
||||||
|
|
|
@ -78,6 +78,7 @@ import type {
|
||||||
DeleteSentProtoRecipientOptionsType,
|
DeleteSentProtoRecipientOptionsType,
|
||||||
DeleteSentProtoRecipientResultType,
|
DeleteSentProtoRecipientResultType,
|
||||||
EmojiType,
|
EmojiType,
|
||||||
|
FTSOptimizationStateType,
|
||||||
GetAllStoriesResultType,
|
GetAllStoriesResultType,
|
||||||
GetConversationRangeCenteredOnMessageResultType,
|
GetConversationRangeCenteredOnMessageResultType,
|
||||||
GetKnownMessageAttachmentsResultType,
|
GetKnownMessageAttachmentsResultType,
|
||||||
|
@ -341,6 +342,8 @@ const dataInterface: ServerInterface = {
|
||||||
|
|
||||||
getStatisticsForLogging,
|
getStatisticsForLogging,
|
||||||
|
|
||||||
|
optimizeFTS,
|
||||||
|
|
||||||
// Server-only
|
// Server-only
|
||||||
|
|
||||||
initialize,
|
initialize,
|
||||||
|
@ -5405,6 +5408,47 @@ async function removeKnownDraftAttachments(
|
||||||
return Object.keys(lookup);
|
return Object.keys(lookup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default value of 'automerge'.
|
||||||
|
// See: https://www.sqlite.org/fts5.html#the_automerge_configuration_option
|
||||||
|
const OPTIMIZE_FTS_PAGE_COUNT = 4;
|
||||||
|
|
||||||
|
// This query is incremental. It gets the `state` from the return value of
|
||||||
|
// previous `optimizeFTS` call. When `state.done` is `true` - optimization is
|
||||||
|
// complete.
|
||||||
|
async function optimizeFTS(
|
||||||
|
state?: FTSOptimizationStateType
|
||||||
|
): Promise<FTSOptimizationStateType | undefined> {
|
||||||
|
// See https://www.sqlite.org/fts5.html#the_merge_command
|
||||||
|
let pageCount = OPTIMIZE_FTS_PAGE_COUNT;
|
||||||
|
if (state === undefined) {
|
||||||
|
pageCount = -pageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getInstance();
|
||||||
|
const { changes } = prepare(
|
||||||
|
db,
|
||||||
|
`
|
||||||
|
INSERT INTO messages_fts(messages_fts, rank) VALUES ('merge', $pageCount);
|
||||||
|
`
|
||||||
|
).run({ pageCount });
|
||||||
|
|
||||||
|
if (state === undefined) {
|
||||||
|
return {
|
||||||
|
changes,
|
||||||
|
steps: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { changes: prevChanges, steps } = state;
|
||||||
|
|
||||||
|
if (Math.abs(changes - prevChanges) < 2) {
|
||||||
|
return { changes, steps, done: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// More work is needed.
|
||||||
|
return { changes, steps: steps + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
async function getJobsInQueue(queueType: string): Promise<Array<StoredJob>> {
|
async function getJobsInQueue(queueType: string): Promise<Array<StoredJob>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
return getJobsInQueueSync(db, queueType);
|
return getJobsInQueueSync(db, queueType);
|
||||||
|
|
Loading…
Add table
Reference in a new issue