Path: blob/master/src/packages/file-server/btrfs/snapshots.ts
1539 views
import { type SubvolumeSnapshots } from "./subvolume-snapshots";1import getLogger from "@cocalc/backend/logger";23const logger = getLogger("file-server:btrfs:snapshots");45const DATE_REGEXP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;67// Lengths of time in minutes to keep snapshots8// (code below assumes these are listed in ORDER from shortest to longest)9export const SNAPSHOT_INTERVALS_MS = {10frequent: 15 * 1000 * 60,11daily: 60 * 24 * 1000 * 60,12weekly: 60 * 24 * 7 * 1000 * 60,13monthly: 60 * 24 * 7 * 4 * 1000 * 60,14};1516// How many of each type of snapshot to retain17export const DEFAULT_SNAPSHOT_COUNTS = {18frequent: 24,19daily: 14,20weekly: 7,21monthly: 4,22} as SnapshotCounts;2324export interface SnapshotCounts {25frequent: number;26daily: number;27weekly: number;28monthly: number;29}3031export async function updateRollingSnapshots({32snapshots,33counts,34}: {35snapshots: SubvolumeSnapshots;36counts?: Partial<SnapshotCounts>;37}) {38counts = { ...DEFAULT_SNAPSHOT_COUNTS, ...counts };3940const changed = await snapshots.hasUnsavedChanges();41logger.debug("updateRollingSnapshots", {42name: snapshots.subvolume.name,43counts,44changed,45});46if (!changed) {47// definitely no data written since most recent snapshot, so nothing to do48return;49}5051// get exactly the iso timestamp snapshot names:52const snapshotNames = (await snapshots.ls())53.map((x) => x.name)54.filter((name) => DATE_REGEXP.test(name));55snapshotNames.sort();56if (snapshotNames.length > 0) {57const age = Date.now() - new Date(snapshotNames.slice(-1)[0]).valueOf();58for (const key in SNAPSHOT_INTERVALS_MS) {59if (counts[key]) {60if (age < SNAPSHOT_INTERVALS_MS[key]) {61// no need to snapshot since there is already a sufficiently recent snapshot62logger.debug("updateRollingSnapshots: no need to snapshot", {63name: snapshots.subvolume.name,64});65return;66}67// counts[key] nonzero and snapshot is old enough so we'll be making a snapshot68break;69}70}71}7273// make a new snapshot74const name = new Date().toISOString();75await snapshots.create(name);76// delete extra snapshots77snapshotNames.push(name);78const toDelete = snapshotsToDelete({ counts, snapshots: snapshotNames });79for (const expired of toDelete) {80try {81await snapshots.delete(expired);82} catch {83// some snapshots can't be deleted, e.g., they were used for the last send.84}85}86}8788function snapshotsToDelete({ counts, snapshots }): string[] {89if (snapshots.length == 0) {90// nothing to do91return [];92}9394// sorted from BIGGEST to smallest95const times = snapshots.map((x) => new Date(x).valueOf());96times.reverse();97const save = new Set<number>();98for (const type in counts) {99const count = counts[type];100const length_ms = SNAPSHOT_INTERVALS_MS[type];101102// Pick the first count newest snapshots at intervals of length103// length_ms milliseconds.104let n = 0,105i = 0,106last_tm = 0;107while (n < count && i < times.length) {108const tm = times[i];109if (!last_tm || tm <= last_tm - length_ms) {110save.add(tm);111last_tm = tm;112n += 1; // found one more113}114i += 1; // move to next snapshot115}116}117return snapshots.filter((x) => !save.has(new Date(x).valueOf()));118}119120121