Batch deleteSentProtoRecipient queries

This commit is contained in:
Fedor Indutny 2021-08-31 14:35:01 -07:00 committed by GitHub
parent b71e4875e6
commit 6f3191117f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 81 deletions

View file

@ -13,11 +13,13 @@ import { isOutgoing } from '../state/selectors/message';
import { isDirectConversation } from '../util/whatTypeOfConversation'; import { isDirectConversation } from '../util/whatTypeOfConversation';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { createWaitBatcher } from '../util/waitBatcher';
import { import {
SendActionType, SendActionType,
SendStatus, SendStatus,
sendStateReducer, sendStateReducer,
} from '../messages/MessageSendState'; } from '../messages/MessageSendState';
import type { DeleteSentProtoRecipientOptionsType } from '../sql/Interface';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
const { deleteSentProtoRecipient } = dataInterface; const { deleteSentProtoRecipient } = dataInterface;
@ -40,6 +42,18 @@ class MessageReceiptModel extends Model<MessageReceiptAttributesType> {}
let singleton: MessageReceipts | undefined; let singleton: MessageReceipts | undefined;
const deleteSentProtoBatcher = createWaitBatcher({
name: 'deleteSentProtoBatcher',
wait: 250,
maxSize: 30,
async processBatch(items: Array<DeleteSentProtoRecipientOptionsType>) {
window.log.info(
`MessageReceipts: Batching ${items.length} sent proto recipients deletes`
);
await deleteSentProtoRecipient(items);
},
});
async function getTargetMessage( async function getTargetMessage(
sourceId: string, sourceId: string,
messages: MessageModelCollectionType messages: MessageModelCollectionType
@ -202,7 +216,7 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
const deviceId = receipt.get('sourceDevice'); const deviceId = receipt.get('sourceDevice');
if (recipientUuid && deviceId) { if (recipientUuid && deviceId) {
await deleteSentProtoRecipient({ await deleteSentProtoBatcher.add({
timestamp: messageSentAt, timestamp: messageSentAt,
recipientUuid, recipientUuid,
deviceId, deviceId,

View file

@ -48,6 +48,7 @@ import {
ClientJobType, ClientJobType,
ClientSearchResultMessageType, ClientSearchResultMessageType,
ConversationType, ConversationType,
DeleteSentProtoRecipientOptionsType,
IdentityKeyType, IdentityKeyType,
ItemKeyType, ItemKeyType,
ItemType, ItemType,
@ -825,11 +826,11 @@ async function insertProtoRecipients(options: {
}): Promise<void> { }): Promise<void> {
await channels.insertProtoRecipients(options); await channels.insertProtoRecipients(options);
} }
async function deleteSentProtoRecipient(options: { async function deleteSentProtoRecipient(
timestamp: number; options:
recipientUuid: string; | DeleteSentProtoRecipientOptionsType
deviceId: number; | ReadonlyArray<DeleteSentProtoRecipientOptionsType>
}): Promise<void> { ): Promise<void> {
await channels.deleteSentProtoRecipient(options); await channels.deleteSentProtoRecipient(options);
} }

View file

@ -215,6 +215,12 @@ export type LastConversationMessagesType = {
hasUserInitiatedMessages: boolean; hasUserInitiatedMessages: boolean;
}; };
export type DeleteSentProtoRecipientOptionsType = Readonly<{
timestamp: number;
recipientUuid: string;
deviceId: number;
}>;
export type DataInterface = { export type DataInterface = {
close: () => Promise<void>; close: () => Promise<void>;
removeDB: () => Promise<void>; removeDB: () => Promise<void>;
@ -267,11 +273,11 @@ export type DataInterface = {
recipientUuid: string; recipientUuid: string;
deviceIds: Array<number>; deviceIds: Array<number>;
}) => Promise<void>; }) => Promise<void>;
deleteSentProtoRecipient: (options: { deleteSentProtoRecipient: (
timestamp: number; options:
recipientUuid: string; | DeleteSentProtoRecipientOptionsType
deviceId: number; | ReadonlyArray<DeleteSentProtoRecipientOptionsType>
}) => Promise<void>; ) => Promise<void>;
getSentProtoByRecipient: (options: { getSentProtoByRecipient: (options: {
now: number; now: number;
recipientUuid: string; recipientUuid: string;

View file

@ -52,6 +52,7 @@ import {
AttachmentDownloadJobType, AttachmentDownloadJobType,
ConversationMetricsType, ConversationMetricsType,
ConversationType, ConversationType,
DeleteSentProtoRecipientOptionsType,
EmojiType, EmojiType,
IdentityKeyType, IdentityKeyType,
ItemKeyType, ItemKeyType,
@ -2708,82 +2709,87 @@ async function insertProtoRecipients({
})(); })();
} }
async function deleteSentProtoRecipient({ async function deleteSentProtoRecipient(
timestamp, options:
recipientUuid, | DeleteSentProtoRecipientOptionsType
deviceId, | ReadonlyArray<DeleteSentProtoRecipientOptionsType>
}: { ): Promise<void> {
timestamp: number;
recipientUuid: string;
deviceId: number;
}): Promise<void> {
const db = getInstance(); const db = getInstance();
// Note: we use `pluck` in this function to fetch only the first column of returned row. const items = Array.isArray(options) ? options : [options];
// Note: we use `pluck` in this function to fetch only the first column of
// returned row.
db.transaction(() => { db.transaction(() => {
// 1. Figure out what payload we're talking about. for (const item of items) {
const rows = prepare( const { timestamp, recipientUuid, deviceId } = item;
db,
` // 1. Figure out what payload we're talking about.
SELECT sendLogPayloads.id FROM sendLogPayloads const rows = prepare(
INNER JOIN sendLogRecipients db,
ON sendLogRecipients.payloadId = sendLogPayloads.id `
WHERE SELECT sendLogPayloads.id FROM sendLogPayloads
sendLogPayloads.timestamp = $timestamp AND INNER JOIN sendLogRecipients
sendLogRecipients.recipientUuid = $recipientUuid AND ON sendLogRecipients.payloadId = sendLogPayloads.id
sendLogRecipients.deviceId = $deviceId; WHERE
` sendLogPayloads.timestamp = $timestamp AND
).all({ timestamp, recipientUuid, deviceId }); sendLogRecipients.recipientUuid = $recipientUuid AND
if (!rows.length) { sendLogRecipients.deviceId = $deviceId;
return; `
} ).all({ timestamp, recipientUuid, deviceId });
if (rows.length > 1) { if (!rows.length) {
console.warn( continue;
`deleteSentProtoRecipient: More than one payload matches recipient and timestamp ${timestamp}. Using the first.` }
if (rows.length > 1) {
console.warn(
'deleteSentProtoRecipient: More than one payload matches ' +
`recipient and timestamp ${timestamp}. Using the first.`
);
continue;
}
const { id } = rows[0];
// 2. Delete the recipient/device combination in question.
prepare(
db,
`
DELETE FROM sendLogRecipients
WHERE
payloadId = $id AND
recipientUuid = $recipientUuid AND
deviceId = $deviceId;
`
).run({ id, recipientUuid, deviceId });
// 3. See how many more recipient devices there were for this payload.
const remaining = prepare(
db,
'SELECT count(*) FROM sendLogRecipients WHERE payloadId = $id;'
)
.pluck(true)
.get({ id });
if (!isNumber(remaining)) {
throw new Error(
'deleteSentProtoRecipient: select count() returned non-number!'
);
}
if (remaining > 0) {
continue;
}
// 4. Delete the entire payload if there are no more recipients left.
console.info(
'deleteSentProtoRecipient: ' +
`Deleting proto payload for timestamp ${timestamp}`
); );
return; prepare(db, 'DELETE FROM sendLogPayloads WHERE id = $id;').run({
id,
});
} }
const { id } = rows[0];
// 2. Delete the recipient/device combination in question.
prepare(
db,
`
DELETE FROM sendLogRecipients
WHERE
payloadId = $id AND
recipientUuid = $recipientUuid AND
deviceId = $deviceId;
`
).run({ id, recipientUuid, deviceId });
// 3. See how many more recipient devices there were for this payload.
const remaining = prepare(
db,
'SELECT count(*) FROM sendLogRecipients WHERE payloadId = $id;'
)
.pluck(true)
.get({ id });
if (!isNumber(remaining)) {
throw new Error(
'deleteSentProtoRecipient: select count() returned non-number!'
);
}
if (remaining > 0) {
return;
}
// 4. Delete the entire payload if there are no more recipients left.
console.info(
`deleteSentProtoRecipient: Deleting proto payload for timestamp ${timestamp}`
);
prepare(db, 'DELETE FROM sendLogPayloads WHERE id = $id;').run({
id,
});
})(); })();
} }

View file

@ -416,6 +416,49 @@ describe('sendLog', () => {
assert.lengthOf(await getAllSentProtos(), 0); assert.lengthOf(await getAllSentProtos(), 0);
assert.lengthOf(await _getAllSentProtoRecipients(), 0); assert.lengthOf(await _getAllSentProtoRecipients(), 0);
}); });
it('deletes multiple recipients in a single transaction', async () => {
const timestamp = Date.now();
const recipientUuid1 = getGuid();
const recipientUuid2 = getGuid();
const proto = {
contentHint: 1,
proto: Buffer.from(getRandomBytes(128)),
timestamp,
};
await insertSentProto(proto, {
messageIds: [getGuid()],
recipients: {
[recipientUuid1]: [1, 2],
[recipientUuid2]: [1],
},
});
assert.lengthOf(await getAllSentProtos(), 1);
assert.lengthOf(await _getAllSentProtoRecipients(), 3);
await deleteSentProtoRecipient([
{
timestamp,
recipientUuid: recipientUuid1,
deviceId: 1,
},
{
timestamp,
recipientUuid: recipientUuid1,
deviceId: 2,
},
{
timestamp,
recipientUuid: recipientUuid2,
deviceId: 1,
},
]);
assert.lengthOf(await getAllSentProtos(), 0);
assert.lengthOf(await _getAllSentProtoRecipients(), 0);
});
}); });
describe('#getSentProtoByRecipient', () => { describe('#getSentProtoByRecipient', () => {