Avoid race when downloading group avatars post-import

This commit is contained in:
trevor-signal
2026-02-11 08:55:26 -05:00
committed by GitHub
parent ff9d247cb2
commit 291000f297
2 changed files with 46 additions and 17 deletions

View File

@@ -11,6 +11,8 @@ import type { JOB_STATUS } from './JobQueue.std.js';
import { JobQueue } from './JobQueue.std.js'; import { JobQueue } from './JobQueue.std.js';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore.preload.js'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore.preload.js';
import { parseUnknown } from '../util/schemas.std.js'; import { parseUnknown } from '../util/schemas.std.js';
import { waitForOnline } from '../util/waitForOnline.dom.js';
import { isOnline } from '../textsecure/WebAPI.preload.js';
const groupAvatarJobDataSchema = z.object({ const groupAvatarJobDataSchema = z.object({
conversationId: z.string(), conversationId: z.string(),
@@ -29,6 +31,8 @@ export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> {
{ attempt, log }: Readonly<{ attempt: number; log: LoggerType }> { attempt, log }: Readonly<{ attempt: number; log: LoggerType }>
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> { ): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
const { conversationId, newAvatarUrl } = data; const { conversationId, newAvatarUrl } = data;
await waitForOnline({ server: { isOnline } });
const logId = `groupAvatarJobQueue(${conversationId}, attempt=${attempt})`; const logId = `groupAvatarJobQueue(${conversationId}, attempt=${attempt})`;
const convo = window.ConversationController.get(conversationId); const convo = window.ConversationController.get(conversationId);
@@ -43,15 +47,21 @@ export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> {
return undefined; return undefined;
} }
// Generate correct attributes patch await convo.queueJob('GroupAvatarJobQueue', async () => {
const patch = await applyNewAvatar({ if (convo.attributes.remoteAvatarUrl !== newAvatarUrl) {
newAvatarUrl, return;
attributes, }
logId,
});
convo.set(patch); // Generate correct attributes patch
await DataWriter.updateConversation(convo.attributes); const patch = await applyNewAvatar({
newAvatarUrl,
attributes,
logId,
});
convo.set(patch);
await DataWriter.updateConversation(convo.attributes);
});
return undefined; return undefined;
} }
@@ -60,5 +70,5 @@ export class GroupAvatarJobQueue extends JobQueue<GroupAvatarJobData> {
export const groupAvatarJobQueue = new GroupAvatarJobQueue({ export const groupAvatarJobQueue = new GroupAvatarJobQueue({
store: jobQueueDatabaseStore, store: jobQueueDatabaseStore,
queueType: 'groupAvatar', queueType: 'groupAvatar',
maxAttempts: 25, maxAttempts: 5,
}); });

View File

@@ -279,7 +279,6 @@ export class BackupImportStream extends Writable {
#customColorById = new Map<number, CustomColorDataType>(); #customColorById = new Map<number, CustomColorDataType>();
#releaseNotesRecipientId: Long | undefined; #releaseNotesRecipientId: Long | undefined;
#releaseNotesChatId: Long | undefined; #releaseNotesChatId: Long | undefined;
#pendingGroupAvatars = new Map<string, string>();
#pinnedMessages: Array<PinnedMessageParams> = []; #pinnedMessages: Array<PinnedMessageParams> = [];
#frameErrorCount: number = 0; #frameErrorCount: number = 0;
#backupTier: BackupLevel | undefined; #backupTier: BackupLevel | undefined;
@@ -422,7 +421,19 @@ export class BackupImportStream extends Writable {
// conversation's last message, which uses redux selectors) // conversation's last message, which uses redux selectors)
await loadAllAndReinitializeRedux(); await loadAllAndReinitializeRedux();
const allConversations = window.ConversationController.getAll(); const allConversations = window.ConversationController.getAll().sort(
(convoA, convoB) => {
if (convoA.get('isPinned')) {
return -1;
}
if (convoB.get('isPinned')) {
return 1;
}
return (
(convoB.get('active_at') ?? 0) - (convoA.get('active_at') ?? 0)
);
}
);
// Update last message in every active conversation now that we have // Update last message in every active conversation now that we have
// them loaded into memory. // them loaded into memory.
@@ -445,12 +456,21 @@ export class BackupImportStream extends Writable {
// Schedule group avatar download. // Schedule group avatar download.
await pMap( await pMap(
[...this.#pendingGroupAvatars.entries()], allConversations,
async ([conversationId, newAvatarUrl]) => { async conversation => {
if (this.options.type === 'cross-client-integration-test') { if (this.options.type === 'cross-client-integration-test') {
return; return;
} }
await groupAvatarJobQueue.add({ conversationId, newAvatarUrl }); if (
!isGroup(conversation.attributes) ||
!conversation.get('remoteAvatarUrl')
) {
return;
}
await groupAvatarJobQueue.add({
conversationId: conversation.get('id'),
newAvatarUrl: conversation.get('remoteAvatarUrl'),
});
}, },
{ concurrency: MAX_CONCURRENCY } { concurrency: MAX_CONCURRENCY }
); );
@@ -1218,6 +1238,7 @@ export class BackupImportStream extends Writable {
url: avatarUrl, url: avatarUrl,
} }
: undefined, : undefined,
remoteAvatarUrl: dropNull(avatarUrl),
color: fromAvatarColor(group.avatarColor), color: fromAvatarColor(group.avatarColor),
colorFromPrimary: dropNull(group.avatarColor), colorFromPrimary: dropNull(group.avatarColor),
@@ -1319,9 +1340,7 @@ export class BackupImportStream extends Writable {
: undefined, : undefined,
announcementsOnly: dropNull(announcementsOnly), announcementsOnly: dropNull(announcementsOnly),
}; };
if (avatarUrl) {
this.#pendingGroupAvatars.set(attrs.id, avatarUrl);
}
if (group.blocked) { if (group.blocked) {
await itemStorage.blocked.addBlockedGroup(groupId); await itemStorage.blocked.addBlockedGroup(groupId);
} }