fix: backup progress use simple polling & cache (#678)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled

#593
This commit is contained in:
Nico 2026-03-18 20:25:30 +01:00 committed by GitHub
parent 429b69ec92
commit f6f17cd61c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 23 additions and 39 deletions

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { ByteSize } from "~/client/components/bytes-size";
import { Card } from "~/client/components/ui/card";
import { Progress } from "~/client/components/ui/progress";
import { useServerEvents } from "~/client/hooks/use-server-events";
import { getBackupProgressOptions } from "~/client/api-client/@tanstack/react-query.gen";
import type { GetBackupProgressResponse } from "~/client/api-client/types.gen";
import { formatDuration } from "~/utils/utils";
import { formatBytes } from "~/utils/format-bytes";
@ -13,34 +13,11 @@ type Props = {
};
export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => {
const { addEventListener } = useServerEvents();
const [progress, setProgress] = useState<GetBackupProgressResponse>(initialProgress ?? null);
useEffect(() => {
const abortController = new AbortController();
addEventListener(
"backup:progress",
(progressData) => {
if (progressData.scheduleId === scheduleShortId) {
setProgress(progressData);
}
},
{ signal: abortController.signal },
);
addEventListener(
"backup:completed",
(completedData) => {
if (completedData.scheduleId === scheduleShortId) {
setProgress(null);
}
},
{ signal: abortController.signal },
);
return () => abortController.abort();
}, [addEventListener, scheduleShortId]);
const { data: progress } = useQuery({
...getBackupProgressOptions({ path: { shortId: scheduleShortId } }),
initialData: initialProgress,
refetchInterval: 1000,
});
const percentDone = progress ? Math.round(progress.percent_done * 100) : 0;
const currentFile = progress?.current_files?.[0] || "";

View file

@ -76,6 +76,7 @@ export function ScheduleDetailsPage(props: Props) {
const { data: schedule } = useSuspenseQuery({
...getBackupScheduleOptions({ path: { shortId: scheduleId } }),
refetchInterval: 1000,
});
const {

View file

@ -47,6 +47,7 @@ import { requireAuth } from "../auth/auth.middleware";
import { backupsExecutionService } from "./backups.execution";
import { logger } from "@zerobyte/core/node";
import { asShortId } from "~/server/utils/branded";
import { cache, cacheKeys } from "~/server/utils/cache";
export const backupScheduleController = new Hono()
.use(requireAuth)
@ -167,6 +168,10 @@ export const backupScheduleController = new Hono()
.get("/:shortId/progress", getBackupProgressDto, async (c) => {
const shortId = asShortId(c.req.param("shortId"));
const schedule = await backupsService.getScheduleByShortId(shortId);
if (schedule.lastBackupStatus !== "in_progress") {
cache.del(cacheKeys.backup.progress(schedule.id));
return c.json<GetBackupProgressDto>(null, 200);
}
const progress = backupsExecutionService.getBackupProgress(schedule.id);
return c.json<GetBackupProgressDto>(progress ?? null, 200);

View file

@ -16,10 +16,9 @@ import type { ResticBackupOutputDto } from "@zerobyte/core/restic";
import type { BackupProgressEventDto } from "~/schemas/events-dto";
const runningBackups = new Map<number, AbortController>();
const progressCache = new Map<number, BackupProgressEventDto>();
export const getBackupProgress = (scheduleId: number): BackupProgressEventDto | undefined =>
progressCache.get(scheduleId);
cache.get<BackupProgressEventDto>(cacheKeys.backup.progress(scheduleId));
interface BackupContext {
schedule: BackupSchedule;
@ -123,18 +122,16 @@ const runBackupOperation = async (ctx: BackupContext, signal: AbortSignal) => {
compressionMode: ctx.repository.compressionMode ?? "auto",
organizationId: ctx.organizationId,
onProgress: (progress) => {
progressCache.set(ctx.schedule.id, {
const progressEvent = {
scheduleId: ctx.schedule.shortId,
volumeName: ctx.volume.name,
repositoryName: ctx.repository.name,
...progress,
});
};
cache.set(cacheKeys.backup.progress(ctx.schedule.id), progressEvent, 60 * 60);
serverEvents.emit("backup:progress", {
organizationId: ctx.organizationId,
scheduleId: ctx.schedule.shortId,
volumeName: ctx.volume.name,
repositoryName: ctx.repository.name,
...progress,
...progressEvent,
});
},
});
@ -271,6 +268,7 @@ const executeBackup = async (scheduleId: number, manual = false): Promise<void>
}
const { context: ctx } = result;
cache.del(cacheKeys.backup.progress(scheduleId));
emitBackupStarted(ctx, scheduleId);
const nextBackupAt = calculateNextRun(ctx.schedule.cronExpression);
@ -297,7 +295,7 @@ const executeBackup = async (scheduleId: number, manual = false): Promise<void>
await handleBackupFailure(scheduleId, ctx.organizationId, error, ctx);
} finally {
runningBackups.delete(scheduleId);
progressCache.delete(scheduleId);
cache.del(cacheKeys.backup.progress(scheduleId));
}
};

View file

@ -106,6 +106,9 @@ export const createCache = (options: CacheOptions = {}) => {
};
export const cacheKeys = {
backup: {
progress: (scheduleId: number) => `backup:${scheduleId}:progress`,
},
repository: {
all: (repositoryId: string) => `repo:${repositoryId}:`,
snapshots: (repositoryId: string, backupId = "all") => `repo:${repositoryId}:snapshots:${backupId}`,