Compare commits

...

1 Commits

Author SHA1 Message Date
Sonarly Claude Code bc45b0e7e8 Fix missing throttleRetryAfter field metadata crash during message import
For workspaces that haven't run the v1.18 migration yet, the
throttleRetryAfter field doesn't exist in the workspace metadata.
Update calls that include this field cause formatData to throw,
crashing the messaging import job and breaking error recovery.

Wrap each update call that includes throttleRetryAfter in a try-catch
that retries without the field when the field metadata is missing,
allowing message import to work for both migrated and unmigrated
workspaces.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:38:30 +00:00
4 changed files with 198 additions and 63 deletions
@@ -112,13 +112,32 @@ export class MessageChannelSyncStatusService {
'messageFolder',
);
await messageChannelRepository.update(messageChannelIds, {
syncCursor: '',
syncStageStartedAt: null,
throttleFailureCount: 0,
throttleRetryAfter: null,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
});
try {
await messageChannelRepository.update(messageChannelIds, {
syncCursor: '',
syncStageStartedAt: null,
throttleFailureCount: 0,
throttleRetryAfter: null,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
});
} catch (updateError) {
if (
updateError instanceof Error &&
updateError.message.includes(
'Field metadata for field "throttleRetryAfter" is missing',
)
) {
await messageChannelRepository.update(messageChannelIds, {
syncCursor: '',
syncStageStartedAt: null,
throttleFailureCount: 0,
pendingGroupEmailsAction:
MessageChannelPendingGroupEmailsAction.NONE,
});
} else {
throw updateError;
}
}
await messageFolderRepository.update(
{ messageChannelId: In(messageChannelIds) },
@@ -222,14 +241,33 @@ export class MessageChannelSyncStatusService {
'messageChannel',
);
await messageChannelRepository.update(messageChannelIds, {
syncStatus: MessageChannelSyncStatus.ACTIVE,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
syncedAt: new Date().toISOString(),
});
try {
await messageChannelRepository.update(messageChannelIds, {
syncStatus: MessageChannelSyncStatus.ACTIVE,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
syncedAt: new Date().toISOString(),
});
} catch (updateError) {
if (
updateError instanceof Error &&
updateError.message.includes(
'Field metadata for field "throttleRetryAfter" is missing',
)
) {
await messageChannelRepository.update(messageChannelIds, {
syncStatus: MessageChannelSyncStatus.ACTIVE,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
throttleFailureCount: 0,
syncStageStartedAt: null,
syncedAt: new Date().toISOString(),
});
} else {
throw updateError;
}
}
}, authContext);
await this.metricsService.batchIncrementCounter({
@@ -306,11 +344,27 @@ export class MessageChannelSyncStatusService {
'messageChannel',
);
await messageChannelRepository.update(messageChannelIds, {
syncStage: MessageChannelSyncStage.FAILED,
syncStatus: syncStatus,
throttleRetryAfter: null,
});
try {
await messageChannelRepository.update(messageChannelIds, {
syncStage: MessageChannelSyncStage.FAILED,
syncStatus: syncStatus,
throttleRetryAfter: null,
});
} catch (updateError) {
if (
updateError instanceof Error &&
updateError.message.includes(
'Field metadata for field "throttleRetryAfter" is missing',
)
) {
await messageChannelRepository.update(messageChannelIds, {
syncStage: MessageChannelSyncStage.FAILED,
syncStatus: syncStatus,
});
} else {
throw updateError;
}
}
const metricsKey =
syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS
@@ -32,21 +32,47 @@ export class MessagingCursorService {
);
if (!folderId) {
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
syncCursor:
!messageChannel.syncCursor ||
nextSyncCursor > messageChannel.syncCursor
? nextSyncCursor
: messageChannel.syncCursor,
},
);
try {
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
syncCursor:
!messageChannel.syncCursor ||
nextSyncCursor > messageChannel.syncCursor
? nextSyncCursor
: messageChannel.syncCursor,
},
);
} catch (updateError) {
if (
updateError instanceof Error &&
updateError.message.includes(
'Field metadata for field "throttleRetryAfter" is missing',
)
) {
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
syncStageStartedAt: null,
syncCursor:
!messageChannel.syncCursor ||
nextSyncCursor > messageChannel.syncCursor
? nextSyncCursor
: messageChannel.syncCursor,
},
);
} else {
throw updateError;
}
}
} else {
await folderRepository.update(
{
@@ -56,16 +82,37 @@ export class MessagingCursorService {
syncCursor: nextSyncCursor,
},
);
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
},
);
try {
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
},
);
} catch (updateError) {
if (
updateError instanceof Error &&
updateError.message.includes(
'Field metadata for field "throttleRetryAfter" is missing',
)
) {
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
syncStageStartedAt: null,
},
);
} else {
throw updateError;
}
}
}
}, authContext);
}
@@ -174,14 +174,27 @@ export class MessageImportExceptionHandlerService {
? exception.throttleRetryAfter
: undefined;
await messageChannelRepository.update(
{ id: messageChannel.id },
{
throttleRetryAfter: isDefined(throttleRetryAfter)
? throttleRetryAfter.toISOString()
: null,
},
);
try {
await messageChannelRepository.update(
{ id: messageChannel.id },
{
throttleRetryAfter: isDefined(throttleRetryAfter)
? throttleRetryAfter.toISOString()
: null,
},
);
} catch (updateError) {
if (
!(
updateError instanceof Error &&
updateError.message.includes(
'Field metadata for field "throttleRetryAfter" is missing',
)
)
) {
throw updateError;
}
}
}, authContext);
switch (syncStep) {
@@ -202,16 +202,37 @@ export class MessagingMessagesImportService {
'messageChannel',
);
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
},
);
try {
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
throttleRetryAfter: null,
syncStageStartedAt: null,
},
);
} catch (updateError) {
if (
updateError instanceof Error &&
updateError.message.includes(
'Field metadata for field "throttleRetryAfter" is missing',
)
) {
await messageChannelRepository.update(
{
id: messageChannel.id,
},
{
throttleFailureCount: 0,
syncStageStartedAt: null,
},
);
} else {
throw updateError;
}
}
return await this.trackMessageImportCompleted(
messageChannel,