fix: wrong cache key for snapshot retention tags (#574)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Enhanced error handling in retention category calculations to gracefully handle missing or invalid repositories, ensuring the system returns safe defaults instead of failing * Optimized repository cache validation and lookup mechanisms for improved performance * Strengthened repository validation checks to prevent potential calculation failures <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
29967d7e4e
commit
dd9083ce7a
2 changed files with 81 additions and 13 deletions
|
|
@ -10,6 +10,8 @@ import { repositoriesTable } from "~/server/db/schema";
|
||||||
import { generateShortId } from "~/server/utils/id";
|
import { generateShortId } from "~/server/utils/id";
|
||||||
import { restic } from "~/server/utils/restic";
|
import { restic } from "~/server/utils/restic";
|
||||||
import { createTestSession } from "~/test/helpers/auth";
|
import { createTestSession } from "~/test/helpers/auth";
|
||||||
|
import { createTestBackupSchedule } from "~/test/helpers/backup";
|
||||||
|
import { cache, cacheKeys } from "~/server/utils/cache";
|
||||||
import { repositoriesService } from "../repositories.service";
|
import { repositoriesService } from "../repositories.service";
|
||||||
|
|
||||||
describe("repositoriesService.createRepository", () => {
|
describe("repositoriesService.createRepository", () => {
|
||||||
|
|
@ -303,3 +305,69 @@ describe("repositoriesService.dumpSnapshot", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("repositoriesService.getRetentionCategories", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recomputes retention categories after repository cache invalidation", async () => {
|
||||||
|
const { organizationId, user } = await createTestSession();
|
||||||
|
const schedule = await createTestBackupSchedule({ organizationId, retentionPolicy: { keepLast: 1 } });
|
||||||
|
|
||||||
|
const repository = await db.query.repositoriesTable.findFirst({ where: { id: schedule.repositoryId } });
|
||||||
|
|
||||||
|
expect(repository).toBeTruthy();
|
||||||
|
if (!repository) {
|
||||||
|
throw new Error("Repository should exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldSnapshotId = "snapshot-old";
|
||||||
|
const newSnapshotId = "snapshot-new";
|
||||||
|
const buildForgetResponse = (snapshotId: string) => ({
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
tags: [schedule.shortId],
|
||||||
|
host: "host",
|
||||||
|
paths: ["/data"],
|
||||||
|
keep: [],
|
||||||
|
remove: null,
|
||||||
|
reasons: [
|
||||||
|
{
|
||||||
|
snapshot: {
|
||||||
|
id: snapshotId,
|
||||||
|
short_id: snapshotId,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
tree: "tree",
|
||||||
|
paths: ["/data"],
|
||||||
|
hostname: "host",
|
||||||
|
},
|
||||||
|
matches: ["last snapshot"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const forgetSpy = spyOn(restic, "forget");
|
||||||
|
forgetSpy.mockResolvedValueOnce(buildForgetResponse(oldSnapshotId));
|
||||||
|
forgetSpy.mockResolvedValueOnce(buildForgetResponse(newSnapshotId));
|
||||||
|
|
||||||
|
const firstCategories = await withContext({ organizationId, userId: user.id }, () =>
|
||||||
|
repositoriesService.getRetentionCategories(repository.shortId, schedule.shortId),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(firstCategories.get(oldSnapshotId)).toEqual(["last"]);
|
||||||
|
|
||||||
|
cache.delByPrefix(cacheKeys.repository.all(repository.id));
|
||||||
|
|
||||||
|
const secondCategories = await withContext({ organizationId, userId: user.id }, () =>
|
||||||
|
repositoriesService.getRetentionCategories(repository.shortId, schedule.shortId),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(secondCategories.get(newSnapshotId)).toEqual(["last"]);
|
||||||
|
expect(secondCategories.has(oldSnapshotId)).toBe(false);
|
||||||
|
expect(forgetSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -809,25 +809,25 @@ const getRetentionCategories = async (repositoryId: ShortId, scheduleId?: ShortI
|
||||||
return new Map<string, RetentionCategory[]>();
|
return new Map<string, RetentionCategory[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = cacheKeys.repository.retention(repositoryId, scheduleId);
|
|
||||||
const cached = cache.get<Record<string, RetentionCategory[]>>(cacheKey);
|
|
||||||
|
|
||||||
if (cached && Object.keys(cached).length > 0) {
|
|
||||||
return new Map(Object.entries(cached));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [schedule, repositoryResult] = await Promise.all([
|
const repository = await findRepository(repositoryId);
|
||||||
backupsService.getScheduleByShortId(scheduleId),
|
if (!repository) {
|
||||||
repositoriesService.getRepository(repositoryId),
|
return new Map<string, RetentionCategory[]>();
|
||||||
]);
|
}
|
||||||
|
|
||||||
|
const cacheKey = cacheKeys.repository.retention(repository.id, scheduleId);
|
||||||
|
const cached = cache.get<Record<string, RetentionCategory[]>>(cacheKey);
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
return new Map(Object.entries(cached));
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = await backupsService.getScheduleByShortId(scheduleId);
|
||||||
|
|
||||||
if (!schedule?.retentionPolicy) {
|
if (!schedule?.retentionPolicy) {
|
||||||
return new Map<string, RetentionCategory[]>();
|
return new Map<string, RetentionCategory[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { repository } = repositoryResult;
|
|
||||||
|
|
||||||
const dryRunResults = await restic.forget(repository.config, schedule.retentionPolicy, {
|
const dryRunResults = await restic.forget(repository.config, schedule.retentionPolicy, {
|
||||||
tag: scheduleId,
|
tag: scheduleId,
|
||||||
organizationId: getOrganizationId(),
|
organizationId: getOrganizationId(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue