Use job queue when restoring call links from storage

This commit is contained in:
ayumi-signal 2024-09-16 22:54:59 -07:00 committed by GitHub
parent 84c562d0b2
commit f23f86d0e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 138 additions and 10 deletions

View file

@ -0,0 +1,126 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as z from 'zod';
import PQueue from 'p-queue';
import { CallLinkRootKey } from '@signalapp/ringrtc';
import type { LoggerType } from '../types/Logging';
import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff';
import type { ParsedJob } from './types';
import type { JOB_STATUS } from './JobQueue';
import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { DAY, SECOND } from '../util/durations';
import { commonShouldJobContinue } from './helpers/commonShouldJobContinue';
import { DataReader, DataWriter } from '../sql/Client';
import { storageServiceUploadJob } from '../services/storage';
import type { CallLinkType } from '../types/CallLink';
import { calling } from '../services/calling';
import { sleeper } from '../util/sleeper';
const MAX_RETRY_TIME = DAY;
const MAX_PARALLEL_JOBS = 5;
const MAX_ATTEMPTS = exponentialBackoffMaxAttempts(MAX_RETRY_TIME);
const DEFAULT_SLEEP_TIME = 20 * SECOND;
const callLinkRefreshJobDataSchema = z.object({
roomId: z.string(),
});
export type CallLinkRefreshJobData = z.infer<
typeof callLinkRefreshJobDataSchema
>;
export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
private parallelQueue = new PQueue({ concurrency: MAX_PARALLEL_JOBS });
protected override getQueues(): ReadonlySet<PQueue> {
return new Set([this.parallelQueue]);
}
protected override getInMemoryQueue(
_parsedJob: ParsedJob<CallLinkRefreshJobData>
): PQueue {
return this.parallelQueue;
}
protected parseData(data: unknown): CallLinkRefreshJobData {
return callLinkRefreshJobDataSchema.parse(data);
}
protected async run(
{
data,
timestamp,
}: Readonly<{ data: CallLinkRefreshJobData; timestamp: number }>,
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
const { roomId } = data;
const logId = `callLinkRefreshJobQueue(${roomId}).run`;
log.info(`${logId}: Starting`);
const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now();
const shouldContinue = await commonShouldJobContinue({
attempt,
log,
timeRemaining,
skipWait: false,
});
if (!shouldContinue) {
return undefined;
}
const existingCallLink = await DataReader.getCallLinkByRoomId(roomId);
if (!existingCallLink) {
log.warn(`${logId}: Call link missing locally, can't refresh`);
return undefined;
}
let error: Error | undefined;
const callLinkRootKey = CallLinkRootKey.parse(existingCallLink.rootKey);
try {
// This will either return the fresh call link state,
// null (link deleted from server), or err (connection error)
const freshCallLinkState = await calling.readCallLink(callLinkRootKey);
if (freshCallLinkState != null) {
log.info(`${logId}: Refreshed call link`);
const callLink: CallLinkType = {
...existingCallLink,
...freshCallLinkState,
};
await DataWriter.updateCallLinkState(roomId, freshCallLinkState);
window.reduxActions.calling.handleCallLinkUpdateLocal(callLink);
} else {
log.info(
`${logId}: Call link not found on server, deleting local call link`
);
await DataWriter.beginDeleteCallLink(roomId, {
storageNeedsSync: true,
});
storageServiceUploadJob();
window.reduxActions.calling.handleCallLinkDelete({ roomId });
}
} catch (err) {
error = err;
}
// Always throttle API calls to the calling server, but if shutting down and job
// was successful then resolve and dequeue it on app shutdown, otherwise reject
await sleeper.sleep(DEFAULT_SLEEP_TIME, `${logId}: Default sleep`, {
resolveOnShutdown: error === undefined,
});
// Repropagate error so JobQueue handles it
if (error) {
throw error;
}
return undefined;
}
}
export const callLinkRefreshJobQueue = new CallLinkRefreshJobQueue({
maxAttempts: MAX_ATTEMPTS,
queueType: 'call link refresh',
store: jobQueueDatabaseStore,
});

View file

@ -44,11 +44,13 @@ export async function commonShouldJobContinue({
} }
const sleepTime = exponentialBackoffSleepTime(attempt); const sleepTime = exponentialBackoffSleepTime(attempt);
log.info(`sleeping for ${sleepTime}`); if (sleepTime > 0) {
await sleeper.sleep( log.info(`sleeping for ${sleepTime}`);
sleepTime, await sleeper.sleep(
`commonShouldJobContinue: attempt ${attempt}, skipWait ${skipWait}` sleepTime,
); `commonShouldJobContinue: attempt ${attempt}, skipWait ${skipWait}`
);
}
return true; return true;
} }

View file

@ -5,6 +5,7 @@ import type { WebAPIType } from '../textsecure/WebAPI';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import { CallLinkDeleteManager } from './CallLinkDeleteManager'; import { CallLinkDeleteManager } from './CallLinkDeleteManager';
import { callLinkRefreshJobQueue } from './callLinkRefreshJobQueue';
import { conversationJobQueue } from './conversationJobQueue'; import { conversationJobQueue } from './conversationJobQueue';
import { groupAvatarJobQueue } from './groupAvatarJobQueue'; import { groupAvatarJobQueue } from './groupAvatarJobQueue';
import { readSyncJobQueue } from './readSyncJobQueue'; import { readSyncJobQueue } from './readSyncJobQueue';
@ -41,11 +42,13 @@ export function initializeAllJobQueues({
// Other queues // Other queues
drop(removeStorageKeyJobQueue.streamJobs()); drop(removeStorageKeyJobQueue.streamJobs());
drop(reportSpamJobQueue.streamJobs()); drop(reportSpamJobQueue.streamJobs());
drop(callLinkRefreshJobQueue.streamJobs());
drop(CallLinkDeleteManager.start()); drop(CallLinkDeleteManager.start());
} }
export async function shutdownAllJobQueues(): Promise<void> { export async function shutdownAllJobQueues(): Promise<void> {
await Promise.allSettled([ await Promise.allSettled([
callLinkRefreshJobQueue.shutdown(),
conversationJobQueue.shutdown(), conversationJobQueue.shutdown(),
groupAvatarJobQueue.shutdown(), groupAvatarJobQueue.shutdown(),
singleProtoJobQueue.shutdown(), singleProtoJobQueue.shutdown(),

View file

@ -78,6 +78,7 @@ import {
toCallHistoryFromUnusedCallLink, toCallHistoryFromUnusedCallLink,
} from '../util/callLinks'; } from '../util/callLinks';
import { isOlderThan } from '../util/timestamp'; import { isOlderThan } from '../util/timestamp';
import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue';
const MY_STORY_BYTES = uuidToBytes(MY_STORY_ID); const MY_STORY_BYTES = uuidToBytes(MY_STORY_ID);
@ -2024,11 +2025,7 @@ export async function mergeCallLinkRecord(
DataWriter.saveCallHistory(callHistory), DataWriter.saveCallHistory(callHistory),
]); ]);
// Refresh call link state via RingRTC and update in redux drop(callLinkRefreshJobQueue.add({ roomId: callLink.roomId }));
window.reduxActions.calling.handleCallLinkUpdate({
rootKey: rootKeyString,
adminKey: adminKeyString,
});
window.reduxActions.callHistory.addCallHistory(callHistory); window.reduxActions.callHistory.addCallHistory(callHistory);
} }