From f6f17cd61cb0a7e22faae247678b9ee362d9e992 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:25:30 +0100 Subject: [PATCH] fix: backup progress use simple polling & cache (#678) #593 --- .../components/backup-progress-card.tsx | 37 ++++--------------- .../modules/backups/routes/backup-details.tsx | 1 + .../modules/backups/backups.controller.ts | 5 +++ .../modules/backups/backups.execution.ts | 16 ++++---- app/server/utils/cache.ts | 3 ++ 5 files changed, 23 insertions(+), 39 deletions(-) diff --git a/app/client/modules/backups/components/backup-progress-card.tsx b/app/client/modules/backups/components/backup-progress-card.tsx index 8381d26c..0e6f4140 100644 --- a/app/client/modules/backups/components/backup-progress-card.tsx +++ b/app/client/modules/backups/components/backup-progress-card.tsx @@ -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(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] || ""; diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index 16ab75b9..7a521f90 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -76,6 +76,7 @@ export function ScheduleDetailsPage(props: Props) { const { data: schedule } = useSuspenseQuery({ ...getBackupScheduleOptions({ path: { shortId: scheduleId } }), + refetchInterval: 1000, }); const { diff --git a/app/server/modules/backups/backups.controller.ts b/app/server/modules/backups/backups.controller.ts index 2b349ba0..41c54352 100644 --- a/app/server/modules/backups/backups.controller.ts +++ b/app/server/modules/backups/backups.controller.ts @@ -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(null, 200); + } const progress = backupsExecutionService.getBackupProgress(schedule.id); return c.json(progress ?? null, 200); diff --git a/app/server/modules/backups/backups.execution.ts b/app/server/modules/backups/backups.execution.ts index f446514a..11a8ba4e 100644 --- a/app/server/modules/backups/backups.execution.ts +++ b/app/server/modules/backups/backups.execution.ts @@ -16,10 +16,9 @@ import type { ResticBackupOutputDto } from "@zerobyte/core/restic"; import type { BackupProgressEventDto } from "~/schemas/events-dto"; const runningBackups = new Map(); -const progressCache = new Map(); export const getBackupProgress = (scheduleId: number): BackupProgressEventDto | undefined => - progressCache.get(scheduleId); + cache.get(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 } 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 await handleBackupFailure(scheduleId, ctx.organizationId, error, ctx); } finally { runningBackups.delete(scheduleId); - progressCache.delete(scheduleId); + cache.del(cacheKeys.backup.progress(scheduleId)); } }; diff --git a/app/server/utils/cache.ts b/app/server/utils/cache.ts index a5bda652..c628878e 100644 --- a/app/server/utils/cache.ts +++ b/app/server/utils/cache.ts @@ -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}`,