feat(backup): retry backup on failure
This commit is contained in:
parent
4520335ebc
commit
feb69ef7a7
5 changed files with 2387 additions and 3 deletions
3
app/drizzle/20260407172925_fresh_warlock/migration.sql
Normal file
3
app/drizzle/20260407172925_fresh_warlock/migration.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE `backup_schedules_table` ADD `failure_retry_count` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `backup_schedules_table` ADD `max_retries` integer DEFAULT 3 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `backup_schedules_table` ADD `retry_delay` integer DEFAULT 3600000 NOT NULL;
|
||||||
2331
app/drizzle/20260407172925_fresh_warlock/snapshot.json
Normal file
2331
app/drizzle/20260407172925_fresh_warlock/snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -311,6 +311,9 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
||||||
oneFileSystem: int("one_file_system", { mode: "boolean" }).notNull().default(false),
|
oneFileSystem: int("one_file_system", { mode: "boolean" }).notNull().default(false),
|
||||||
customResticParams: text("custom_restic_params", { mode: "json" }).$type<string[]>().default([]),
|
customResticParams: text("custom_restic_params", { mode: "json" }).$type<string[]>().default([]),
|
||||||
sortOrder: int("sort_order", { mode: "number" }).notNull().default(0),
|
sortOrder: int("sort_order", { mode: "number" }).notNull().default(0),
|
||||||
|
failureRetryCount: int("failure_retry_count").notNull().default(0),
|
||||||
|
maxRetries: int("max_retries").notNull().default(3),
|
||||||
|
retryDelay: int("retry_delay").notNull().default(3600000),
|
||||||
createdAt: int("created_at", { mode: "number" })
|
createdAt: int("created_at", { mode: "number" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`(unixepoch() * 1000)`),
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export const scheduleQueries = {
|
||||||
lastBackupAt?: number;
|
lastBackupAt?: number;
|
||||||
lastBackupError?: string | null;
|
lastBackupError?: string | null;
|
||||||
nextBackupAt?: number | null;
|
nextBackupAt?: number | null;
|
||||||
|
failureRetryCount?: number;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
return db
|
return db
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,7 @@ export async function finalizeSuccessfulBackup(
|
||||||
lastBackupStatus: finalStatus,
|
lastBackupStatus: finalStatus,
|
||||||
lastBackupError: finalStatus === "warning" ? warningDetails : null,
|
lastBackupError: finalStatus === "warning" ? warningDetails : null,
|
||||||
nextBackupAt: ctx.schedule.cronExpression ? calculateNextRun(ctx.schedule.cronExpression) : null,
|
nextBackupAt: ctx.schedule.cronExpression ? calculateNextRun(ctx.schedule.cronExpression) : null,
|
||||||
|
failureRetryCount: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (finalStatus === "warning") {
|
if (finalStatus === "warning") {
|
||||||
|
|
@ -219,10 +220,55 @@ export async function handleBackupFailure(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { schedule, volume, repository } = partialContext;
|
// Determine if the backup should be retried
|
||||||
|
const schedule = partialContext.schedule;
|
||||||
|
const currentRetryCount = schedule.failureRetryCount;
|
||||||
|
const maxRetries = schedule.maxRetries;
|
||||||
|
const shouldRetry = currentRetryCount < maxRetries;
|
||||||
|
|
||||||
|
if (shouldRetry) {
|
||||||
|
const nextBackupAt = Date.now() + schedule.retryDelay;
|
||||||
|
|
||||||
|
await scheduleQueries.updateStatus(scheduleId, organizationId, {
|
||||||
|
lastBackupAt: Date.now(),
|
||||||
|
lastBackupStatus: "error",
|
||||||
|
lastBackupError: errorDetails,
|
||||||
|
nextBackupAt,
|
||||||
|
failureRetryCount: currentRetryCount + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`Backup ${schedule?.name} failed. Scheduling retry ${currentRetryCount + 1}/${maxRetries} for 1 hour from now: ${errorMessage}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (partialContext?.volume && partialContext?.repository) {
|
||||||
|
serverEvents.emit("backup:completed", {
|
||||||
|
organizationId,
|
||||||
|
scheduleId: schedule!.shortId,
|
||||||
|
volumeName: partialContext.volume.name,
|
||||||
|
repositoryName: partialContext.repository.name,
|
||||||
|
status: "error",
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsService
|
||||||
|
.sendBackupNotification(scheduleId, "failure", {
|
||||||
|
volumeName: partialContext.volume.name,
|
||||||
|
repositoryName: partialContext.repository.name,
|
||||||
|
scheduleName: schedule!.name,
|
||||||
|
error: `${errorDetails}\n\nRetrying in 1 hour (attempt ${currentRetryCount + 1}/${maxRetries})`,
|
||||||
|
})
|
||||||
|
.catch((notifError) => {
|
||||||
|
logger.error(`Failed to send backup failure notification: ${toMessage(notifError)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { volume, repository } = partialContext;
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Backup ${schedule.name} failed for volume ${volume.name} to repository ${repository.name}: ${errorMessage}`,
|
`Backup ${schedule?.name} failed after ${maxRetries} retries for volume ${volume.name} to repository ${repository.name}: ${errorMessage}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
serverEvents.emit("backup:completed", {
|
serverEvents.emit("backup:completed", {
|
||||||
|
|
@ -238,7 +284,7 @@ export async function handleBackupFailure(
|
||||||
volumeName: volume.name,
|
volumeName: volume.name,
|
||||||
repositoryName: repository.name,
|
repositoryName: repository.name,
|
||||||
scheduleName: schedule.name,
|
scheduleName: schedule.name,
|
||||||
error: errorDetails,
|
error: `${errorDetails}\n\nFailed after ${maxRetries} retry attempts.`,
|
||||||
})
|
})
|
||||||
.catch((notifError) => {
|
.catch((notifError) => {
|
||||||
logger.error(`Failed to send backup failure notification: ${toMessage(notifError)}`);
|
logger.error(`Failed to send backup failure notification: ${toMessage(notifError)}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue