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