refactor: use backend cache in restic operations (#295)
This commit is contained in:
parent
79d5f47b19
commit
a2ee223ee7
3 changed files with 64 additions and 11 deletions
|
|
@ -6,6 +6,7 @@ import { db } from "../../db/db";
|
||||||
import { backupSchedulesTable, backupScheduleMirrorsTable, repositoriesTable, volumesTable } from "../../db/schema";
|
import { backupSchedulesTable, backupScheduleMirrorsTable, repositoriesTable, volumesTable } from "../../db/schema";
|
||||||
import { restic } from "../../utils/restic";
|
import { restic } from "../../utils/restic";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
import { cache } from "../../utils/cache";
|
||||||
import { getVolumePath } from "../volumes/helpers";
|
import { getVolumePath } from "../volumes/helpers";
|
||||||
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody, UpdateScheduleMirrorsBody } from "./backups.dto";
|
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody, UpdateScheduleMirrorsBody } from "./backups.dto";
|
||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
|
|
@ -329,6 +330,8 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
||||||
|
|
||||||
const finalStatus = exitCode === 0 ? "success" : "warning";
|
const finalStatus = exitCode === 0 ? "success" : "warning";
|
||||||
|
|
||||||
|
cache.delByPrefix(`snapshots:${repository.id}:`);
|
||||||
|
|
||||||
const nextBackupAt = calculateNextRun(schedule.cronExpression);
|
const nextBackupAt = calculateNextRun(schedule.cronExpression);
|
||||||
await db
|
await db
|
||||||
.update(backupSchedulesTable)
|
.update(backupSchedulesTable)
|
||||||
|
|
@ -488,6 +491,7 @@ const runForget = async (scheduleId: number, repositoryId?: string) => {
|
||||||
const releaseLock = await repoMutex.acquireExclusive(repository.id, `forget:${scheduleId}`);
|
const releaseLock = await repoMutex.acquireExclusive(repository.id, `forget:${scheduleId}`);
|
||||||
try {
|
try {
|
||||||
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.shortId });
|
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.shortId });
|
||||||
|
cache.delByPrefix(`snapshots:${repository.id}:`);
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock();
|
releaseLock();
|
||||||
}
|
}
|
||||||
|
|
@ -619,6 +623,7 @@ const copyToMirrors = async (
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await restic.copy(sourceRepository.config, mirror.repository.config, { tag: schedule.shortId });
|
await restic.copy(sourceRepository.config, mirror.repository.config, { tag: schedule.shortId });
|
||||||
|
cache.delByPrefix(`snapshots:${mirror.repository.id}:`);
|
||||||
} finally {
|
} finally {
|
||||||
releaseSource();
|
releaseSource();
|
||||||
releaseMirror();
|
releaseMirror();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { toMessage } from "../../utils/errors";
|
||||||
import { generateShortId } from "../../utils/id";
|
import { generateShortId } from "../../utils/id";
|
||||||
import { restic } from "../../utils/restic";
|
import { restic } from "../../utils/restic";
|
||||||
import { cryptoUtils } from "../../utils/crypto";
|
import { cryptoUtils } from "../../utils/crypto";
|
||||||
|
import { cache } from "../../utils/cache";
|
||||||
import { repoMutex } from "../../core/repository-mutex";
|
import { repoMutex } from "../../core/repository-mutex";
|
||||||
import {
|
import {
|
||||||
repositoryConfigSchema,
|
repositoryConfigSchema,
|
||||||
|
|
@ -143,6 +144,9 @@ const deleteRepository = async (id: string) => {
|
||||||
// TODO: Add cleanup logic for the actual restic repository files
|
// TODO: Add cleanup logic for the actual restic repository files
|
||||||
|
|
||||||
await db.delete(repositoriesTable).where(eq(repositoriesTable.id, repository.id));
|
await db.delete(repositoriesTable).where(eq(repositoriesTable.id, repository.id));
|
||||||
|
|
||||||
|
cache.delByPrefix(`snapshots:${repository.id}:`);
|
||||||
|
cache.delByPrefix(`ls:${repository.id}:`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -160,6 +164,12 @@ const listSnapshots = async (id: string, backupId?: string) => {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheKey = `snapshots:${repository.id}:${backupId || "all"}`;
|
||||||
|
const cached = cache.get<Awaited<ReturnType<typeof restic.snapshots>>>(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
const releaseLock = await repoMutex.acquireShared(repository.id, "snapshots");
|
const releaseLock = await repoMutex.acquireShared(repository.id, "snapshots");
|
||||||
try {
|
try {
|
||||||
let snapshots = [];
|
let snapshots = [];
|
||||||
|
|
@ -170,6 +180,8 @@ const listSnapshots = async (id: string, backupId?: string) => {
|
||||||
snapshots = await restic.snapshots(repository.config);
|
snapshots = await restic.snapshots(repository.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.set(cacheKey, snapshots);
|
||||||
|
|
||||||
return snapshots;
|
return snapshots;
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock();
|
releaseLock();
|
||||||
|
|
@ -183,6 +195,15 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string)
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheKey = `ls:${repository.id}:${snapshotId}:${path || "root"}`;
|
||||||
|
const cached = cache.get<Awaited<ReturnType<typeof restic.ls>>>(cacheKey);
|
||||||
|
if (cached?.snapshot) {
|
||||||
|
return {
|
||||||
|
snapshot: cached.snapshot,
|
||||||
|
files: cached.nodes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const releaseLock = await repoMutex.acquireShared(repository.id, `ls:${snapshotId}`);
|
const releaseLock = await repoMutex.acquireShared(repository.id, `ls:${snapshotId}`);
|
||||||
try {
|
try {
|
||||||
const result = await restic.ls(repository.config, snapshotId, path);
|
const result = await restic.ls(repository.config, snapshotId, path);
|
||||||
|
|
@ -191,7 +212,7 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string)
|
||||||
throw new NotFoundError("Snapshot not found or empty");
|
throw new NotFoundError("Snapshot not found or empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const response = {
|
||||||
snapshot: {
|
snapshot: {
|
||||||
id: result.snapshot.id,
|
id: result.snapshot.id,
|
||||||
short_id: result.snapshot.short_id,
|
short_id: result.snapshot.short_id,
|
||||||
|
|
@ -201,6 +222,10 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string)
|
||||||
},
|
},
|
||||||
files: result.nodes,
|
files: result.nodes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cache.set(cacheKey, result);
|
||||||
|
|
||||||
|
return response;
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock();
|
releaseLock();
|
||||||
}
|
}
|
||||||
|
|
@ -248,19 +273,26 @@ const getSnapshotDetails = async (id: string, snapshotId: string) => {
|
||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseLock = await repoMutex.acquireShared(repository.id, `snapshot_details:${snapshotId}`);
|
const cacheKey = `snapshots:${repository.id}:all`;
|
||||||
try {
|
let snapshots = cache.get<Awaited<ReturnType<typeof restic.snapshots>>>(cacheKey);
|
||||||
const snapshots = await restic.snapshots(repository.config);
|
|
||||||
const snapshot = snapshots.find((snap) => snap.id === snapshotId || snap.short_id === snapshotId);
|
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshots) {
|
||||||
throw new NotFoundError("Snapshot not found");
|
const releaseLock = await repoMutex.acquireShared(repository.id, `snapshot_details:${snapshotId}`);
|
||||||
|
try {
|
||||||
|
snapshots = await restic.snapshots(repository.config);
|
||||||
|
cache.set(cacheKey, snapshots);
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshot;
|
|
||||||
} finally {
|
|
||||||
releaseLock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const snapshot = snapshots.find((snap) => snap.id === snapshotId || snap.short_id === snapshotId);
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
throw new NotFoundError("Snapshot not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkHealth = async (repositoryId: string) => {
|
const checkHealth = async (repositoryId: string) => {
|
||||||
|
|
@ -388,6 +420,8 @@ const deleteSnapshot = async (id: string, snapshotId: string) => {
|
||||||
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:${snapshotId}`);
|
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:${snapshotId}`);
|
||||||
try {
|
try {
|
||||||
await restic.deleteSnapshot(repository.config, snapshotId);
|
await restic.deleteSnapshot(repository.config, snapshotId);
|
||||||
|
cache.delByPrefix(`snapshots:${repository.id}:`);
|
||||||
|
cache.delByPrefix(`ls:${repository.id}:${snapshotId}:`);
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock();
|
releaseLock();
|
||||||
}
|
}
|
||||||
|
|
@ -403,6 +437,10 @@ const deleteSnapshots = async (id: string, snapshotIds: string[]) => {
|
||||||
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:bulk`);
|
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:bulk`);
|
||||||
try {
|
try {
|
||||||
await restic.deleteSnapshots(repository.config, snapshotIds);
|
await restic.deleteSnapshots(repository.config, snapshotIds);
|
||||||
|
cache.delByPrefix(`snapshots:${repository.id}:`);
|
||||||
|
for (const snapshotId of snapshotIds) {
|
||||||
|
cache.delByPrefix(`ls:${repository.id}:${snapshotId}:`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock();
|
releaseLock();
|
||||||
}
|
}
|
||||||
|
|
@ -422,6 +460,10 @@ const tagSnapshots = async (
|
||||||
const releaseLock = await repoMutex.acquireExclusive(repository.id, `tag:bulk`);
|
const releaseLock = await repoMutex.acquireExclusive(repository.id, `tag:bulk`);
|
||||||
try {
|
try {
|
||||||
await restic.tagSnapshots(repository.config, snapshotIds, tags);
|
await restic.tagSnapshots(repository.config, snapshotIds, tags);
|
||||||
|
cache.delByPrefix(`snapshots:${repository.id}:`);
|
||||||
|
for (const snapshotId of snapshotIds) {
|
||||||
|
cache.delByPrefix(`ls:${repository.id}:${snapshotId}:`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock();
|
releaseLock();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ export const createCache = (options: CacheOptions = {}) => {
|
||||||
stmt.run(key);
|
stmt.run(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const delByPrefix = (prefix: string) => {
|
||||||
|
const stmt = db.prepare("DELETE FROM cache WHERE key LIKE ?");
|
||||||
|
stmt.run(`${prefix}%`);
|
||||||
|
};
|
||||||
|
|
||||||
const getByPrefix = <T>(prefix: string): { key: string; value: T }[] => {
|
const getByPrefix = <T>(prefix: string): { key: string; value: T }[] => {
|
||||||
const stmt = db.prepare("SELECT key, value, expiration FROM cache WHERE key LIKE ?");
|
const stmt = db.prepare("SELECT key, value, expiration FROM cache WHERE key LIKE ?");
|
||||||
const rows = stmt.all(`${prefix}%`) as { key: string; value: string; expiration: number }[];
|
const rows = stmt.all(`${prefix}%`) as { key: string; value: string; expiration: number }[];
|
||||||
|
|
@ -88,6 +93,7 @@ export const createCache = (options: CacheOptions = {}) => {
|
||||||
set,
|
set,
|
||||||
get,
|
get,
|
||||||
del,
|
del,
|
||||||
|
delByPrefix,
|
||||||
getByPrefix,
|
getByPrefix,
|
||||||
clear,
|
clear,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue