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
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:
parent
429b69ec92
commit
f6f17cd61c
5 changed files with 23 additions and 39 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
import { Card } from "~/client/components/ui/card";
|
import { Card } from "~/client/components/ui/card";
|
||||||
import { Progress } from "~/client/components/ui/progress";
|
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 type { GetBackupProgressResponse } from "~/client/api-client/types.gen";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
import { formatBytes } from "~/utils/format-bytes";
|
import { formatBytes } from "~/utils/format-bytes";
|
||||||
|
|
@ -13,34 +13,11 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => {
|
export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => {
|
||||||
const { addEventListener } = useServerEvents();
|
const { data: progress } = useQuery({
|
||||||
const [progress, setProgress] = useState<GetBackupProgressResponse>(initialProgress ?? null);
|
...getBackupProgressOptions({ path: { shortId: scheduleShortId } }),
|
||||||
|
initialData: initialProgress,
|
||||||
useEffect(() => {
|
refetchInterval: 1000,
|
||||||
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 percentDone = progress ? Math.round(progress.percent_done * 100) : 0;
|
const percentDone = progress ? Math.round(progress.percent_done * 100) : 0;
|
||||||
const currentFile = progress?.current_files?.[0] || "";
|
const currentFile = progress?.current_files?.[0] || "";
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export function ScheduleDetailsPage(props: Props) {
|
||||||
|
|
||||||
const { data: schedule } = useSuspenseQuery({
|
const { data: schedule } = useSuspenseQuery({
|
||||||
...getBackupScheduleOptions({ path: { shortId: scheduleId } }),
|
...getBackupScheduleOptions({ path: { shortId: scheduleId } }),
|
||||||
|
refetchInterval: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import { requireAuth } from "../auth/auth.middleware";
|
||||||
import { backupsExecutionService } from "./backups.execution";
|
import { backupsExecutionService } from "./backups.execution";
|
||||||
import { logger } from "@zerobyte/core/node";
|
import { logger } from "@zerobyte/core/node";
|
||||||
import { asShortId } from "~/server/utils/branded";
|
import { asShortId } from "~/server/utils/branded";
|
||||||
|
import { cache, cacheKeys } from "~/server/utils/cache";
|
||||||
|
|
||||||
export const backupScheduleController = new Hono()
|
export const backupScheduleController = new Hono()
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
|
|
@ -167,6 +168,10 @@ export const backupScheduleController = new Hono()
|
||||||
.get("/:shortId/progress", getBackupProgressDto, async (c) => {
|
.get("/:shortId/progress", getBackupProgressDto, async (c) => {
|
||||||
const shortId = asShortId(c.req.param("shortId"));
|
const shortId = asShortId(c.req.param("shortId"));
|
||||||
const schedule = await backupsService.getScheduleByShortId(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);
|
const progress = backupsExecutionService.getBackupProgress(schedule.id);
|
||||||
|
|
||||||
return c.json<GetBackupProgressDto>(progress ?? null, 200);
|
return c.json<GetBackupProgressDto>(progress ?? null, 200);
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,9 @@ import type { ResticBackupOutputDto } from "@zerobyte/core/restic";
|
||||||
import type { BackupProgressEventDto } from "~/schemas/events-dto";
|
import type { BackupProgressEventDto } from "~/schemas/events-dto";
|
||||||
|
|
||||||
const runningBackups = new Map<number, AbortController>();
|
const runningBackups = new Map<number, AbortController>();
|
||||||
const progressCache = new Map<number, BackupProgressEventDto>();
|
|
||||||
|
|
||||||
export const getBackupProgress = (scheduleId: number): BackupProgressEventDto | undefined =>
|
export const getBackupProgress = (scheduleId: number): BackupProgressEventDto | undefined =>
|
||||||
progressCache.get(scheduleId);
|
cache.get<BackupProgressEventDto>(cacheKeys.backup.progress(scheduleId));
|
||||||
|
|
||||||
interface BackupContext {
|
interface BackupContext {
|
||||||
schedule: BackupSchedule;
|
schedule: BackupSchedule;
|
||||||
|
|
@ -123,18 +122,16 @@ const runBackupOperation = async (ctx: BackupContext, signal: AbortSignal) => {
|
||||||
compressionMode: ctx.repository.compressionMode ?? "auto",
|
compressionMode: ctx.repository.compressionMode ?? "auto",
|
||||||
organizationId: ctx.organizationId,
|
organizationId: ctx.organizationId,
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
progressCache.set(ctx.schedule.id, {
|
const progressEvent = {
|
||||||
scheduleId: ctx.schedule.shortId,
|
scheduleId: ctx.schedule.shortId,
|
||||||
volumeName: ctx.volume.name,
|
volumeName: ctx.volume.name,
|
||||||
repositoryName: ctx.repository.name,
|
repositoryName: ctx.repository.name,
|
||||||
...progress,
|
...progress,
|
||||||
});
|
};
|
||||||
|
cache.set(cacheKeys.backup.progress(ctx.schedule.id), progressEvent, 60 * 60);
|
||||||
serverEvents.emit("backup:progress", {
|
serverEvents.emit("backup:progress", {
|
||||||
organizationId: ctx.organizationId,
|
organizationId: ctx.organizationId,
|
||||||
scheduleId: ctx.schedule.shortId,
|
...progressEvent,
|
||||||
volumeName: ctx.volume.name,
|
|
||||||
repositoryName: ctx.repository.name,
|
|
||||||
...progress,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -271,6 +268,7 @@ const executeBackup = async (scheduleId: number, manual = false): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { context: ctx } = result;
|
const { context: ctx } = result;
|
||||||
|
cache.del(cacheKeys.backup.progress(scheduleId));
|
||||||
emitBackupStarted(ctx, scheduleId);
|
emitBackupStarted(ctx, scheduleId);
|
||||||
|
|
||||||
const nextBackupAt = calculateNextRun(ctx.schedule.cronExpression);
|
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);
|
await handleBackupFailure(scheduleId, ctx.organizationId, error, ctx);
|
||||||
} finally {
|
} finally {
|
||||||
runningBackups.delete(scheduleId);
|
runningBackups.delete(scheduleId);
|
||||||
progressCache.delete(scheduleId);
|
cache.del(cacheKeys.backup.progress(scheduleId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,9 @@ export const createCache = (options: CacheOptions = {}) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cacheKeys = {
|
export const cacheKeys = {
|
||||||
|
backup: {
|
||||||
|
progress: (scheduleId: number) => `backup:${scheduleId}:progress`,
|
||||||
|
},
|
||||||
repository: {
|
repository: {
|
||||||
all: (repositoryId: string) => `repo:${repositoryId}:`,
|
all: (repositoryId: string) => `repo:${repositoryId}:`,
|
||||||
snapshots: (repositoryId: string, backupId = "all") => `repo:${repositoryId}:snapshots:${backupId}`,
|
snapshots: (repositoryId: string, backupId = "all") => `repo:${repositoryId}:snapshots:${backupId}`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue