@ifc-lite/collab-server 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +13 -2
- package/dist/bin.js.map +1 -1
- package/dist/blob-gc-worker.d.ts +57 -0
- package/dist/blob-gc-worker.d.ts.map +1 -0
- package/dist/blob-gc-worker.js +207 -0
- package/dist/blob-gc-worker.js.map +1 -0
- package/dist/blob-gc.d.ts +69 -0
- package/dist/blob-gc.d.ts.map +1 -0
- package/dist/blob-gc.js +208 -0
- package/dist/blob-gc.js.map +1 -0
- package/dist/blob-route.d.ts +16 -0
- package/dist/blob-route.d.ts.map +1 -1
- package/dist/blob-route.js +58 -12
- package/dist/blob-route.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/room-manager.d.ts +53 -3
- package/dist/room-manager.d.ts.map +1 -1
- package/dist/room-manager.js +111 -3
- package/dist/room-manager.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +62 -1
- package/dist/server.js.map +1 -1
- package/package.json +9 -8
package/dist/bin.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
/** CLI entry point: `ifc-lite-collab-server`. */
|
|
6
6
|
import { FilePersistence, startCollabServer } from './server.js';
|
|
7
7
|
import { FsBlobStorage } from './blob-route.js';
|
|
8
|
+
import { resolveBlobGcConfig, startBlobGc } from './blob-gc-worker.js';
|
|
8
9
|
import { FsLayerRegistry } from './layer-registry-fs.js';
|
|
9
10
|
import { createAccessControl } from './access-control.js';
|
|
10
11
|
// `PORT` is the convention most hosts inject (Railway, Render, Fly, …).
|
|
@@ -63,6 +64,8 @@ async function main() {
|
|
|
63
64
|
trustForwardedFor: ['1', 'true'].includes((process.env.COLLAB_TRUST_PROXY ?? '').toLowerCase()),
|
|
64
65
|
})
|
|
65
66
|
: null;
|
|
67
|
+
const blobStorage = new FsBlobStorage(dataDir);
|
|
68
|
+
const blobGcConfig = resolveBlobGcConfig();
|
|
66
69
|
const handle = await startCollabServer({
|
|
67
70
|
port,
|
|
68
71
|
host,
|
|
@@ -70,7 +73,7 @@ async function main() {
|
|
|
70
73
|
// Disk-backed blobs (not the in-RAM default): mesh blobs dominate a room's
|
|
71
74
|
// size, so keeping them in memory made memory the top hosting cost and lost
|
|
72
75
|
// them on restart. On a mounted volume this is durable and far cheaper.
|
|
73
|
-
blobStorage
|
|
76
|
+
blobStorage,
|
|
74
77
|
maxRooms,
|
|
75
78
|
// Evict idle rooms so a long-lived server doesn't accumulate loaded rooms
|
|
76
79
|
// up to `maxRooms` and then reject all new connections (see const above).
|
|
@@ -81,10 +84,18 @@ async function main() {
|
|
|
81
84
|
...(accessControl ? accessControl.serverOptions : {}),
|
|
82
85
|
});
|
|
83
86
|
// eslint-disable-next-line no-console
|
|
84
|
-
|
|
87
|
+
const blobGc = startBlobGc({
|
|
88
|
+
dataDir,
|
|
89
|
+
storage: blobStorage,
|
|
90
|
+
roomManager: handle.roomManager,
|
|
91
|
+
config: blobGcConfig,
|
|
92
|
+
});
|
|
93
|
+
// eslint-disable-next-line no-console
|
|
94
|
+
console.log(`[collab-server] listening at ${handle.url} (data: ${dataDir}, auth: ${tokenSecret ? 'room-token' : 'anonymous'}, registry: ${layerRegistryEnabled ? 'fs' : 'off'}, blob-gc: ${blobGc ? `every ${Math.round(blobGcConfig.intervalMs / 60000)}m` : 'off'})`);
|
|
85
95
|
const shutdown = async () => {
|
|
86
96
|
// eslint-disable-next-line no-console
|
|
87
97
|
console.log('[collab-server] shutting down…');
|
|
98
|
+
blobGc?.stop();
|
|
88
99
|
await handle.stop();
|
|
89
100
|
// A SIGTERM during the persist debounce (or mid-write) must not lose
|
|
90
101
|
// claims/revocations: await the pending/in-flight state write. flush()
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type BlobGcResult, type GcBlobStorage } from './blob-gc.js';
|
|
2
|
+
import type { RoomManager } from './room-manager.js';
|
|
3
|
+
export interface BlobGcWorkerOptions {
|
|
4
|
+
readonly dataDir: string;
|
|
5
|
+
readonly storage: GcBlobStorage;
|
|
6
|
+
readonly roomManager?: RoomManager;
|
|
7
|
+
readonly intervalMs?: number;
|
|
8
|
+
readonly graceMs?: number;
|
|
9
|
+
readonly now?: () => number;
|
|
10
|
+
/** Counters so a permanently failing sweep is visible on `/metrics`. */
|
|
11
|
+
readonly counters?: {
|
|
12
|
+
sweep?: () => void;
|
|
13
|
+
failure?: () => void;
|
|
14
|
+
deleted?: (n: number) => void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Periodic blob sweep. Mirrors `SnapshotWorker`: unref'd timer, one sweep in
|
|
19
|
+
* flight at a time, failures logged rather than thrown at the timer.
|
|
20
|
+
*
|
|
21
|
+
* Every sweep logs a line, INCLUDING zero-delete sweeps. A GC that silently
|
|
22
|
+
* stops running is the same defect class as the one that caused #2790, so the
|
|
23
|
+
* healthy case has to be visible too, and failures increment a counter.
|
|
24
|
+
*/
|
|
25
|
+
export declare class BlobGcWorker {
|
|
26
|
+
private timer;
|
|
27
|
+
private inflight;
|
|
28
|
+
private readonly opts;
|
|
29
|
+
constructor(opts: BlobGcWorkerOptions);
|
|
30
|
+
start(): void;
|
|
31
|
+
stop(): void;
|
|
32
|
+
runOnce(): Promise<BlobGcResult>;
|
|
33
|
+
private sweep;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve blob-GC settings from the environment.
|
|
37
|
+
*
|
|
38
|
+
* Exported, and `main` calls it, specifically so the wiring can be tested. A
|
|
39
|
+
* test that constructs its own worker would pass with this wiring deleted,
|
|
40
|
+
* which is the failure mode that let #2790 happen: a sweep that silently never
|
|
41
|
+
* runs looks exactly like a sweep with nothing to do.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveBlobGcConfig(env?: NodeJS.ProcessEnv): {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
intervalMs: number;
|
|
46
|
+
graceMs: number;
|
|
47
|
+
};
|
|
48
|
+
/** Arm the periodic blob sweep, or return null when disabled. */
|
|
49
|
+
export declare function startBlobGc(deps: {
|
|
50
|
+
dataDir: string;
|
|
51
|
+
storage: GcBlobStorage;
|
|
52
|
+
roomManager?: RoomManager;
|
|
53
|
+
config: ReturnType<typeof resolveBlobGcConfig>;
|
|
54
|
+
/** Forwarded so a permanently failing sweep is visible on `/metrics`. */
|
|
55
|
+
counters?: BlobGcWorkerOptions['counters'];
|
|
56
|
+
}): BlobGcWorker | null;
|
|
57
|
+
//# sourceMappingURL=blob-gc-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-gc-worker.d.ts","sourceRoot":"","sources":["../src/blob-gc-worker.ts"],"names":[],"mappings":"AAmBA,OAAO,EAML,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiCrD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CACjG;AAED;;;;;;;GAOG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;gBAE/B,IAAI,EAAE,mBAAmB;IAIrC,KAAK,IAAI,IAAI;IA6Bb,IAAI,IAAI,IAAI;IAKN,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC;YAQxB,KAAK;CA8CpB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,CAoBA;AA6BD,iEAAiE;AACjE,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,MAAM,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC;CAC5C,GAAG,YAAY,GAAG,IAAI,CAuBtB"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* Blob GC lifecycle and configuration policy.
|
|
6
|
+
*
|
|
7
|
+
* Split from `blob-gc.ts`, which had grown past this repo's ~400-line limit by
|
|
8
|
+
* carrying two separable concerns: WHAT to collect (reference scanning,
|
|
9
|
+
* planning, deletion - still there) and WHEN and WHETHER to run
|
|
10
|
+
* (scheduling, environment parsing, safety bounds - here).
|
|
11
|
+
*
|
|
12
|
+
* The seam is worth having beyond the line count: everything here is policy an
|
|
13
|
+
* operator can get wrong, and it is where the two #2804 defects lived - a
|
|
14
|
+
* schedule that never fired at boot, and a grace floor set at the destructive
|
|
15
|
+
* value.
|
|
16
|
+
*/
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { DEFAULT_BLOB_GC_GRACE_MS, applyBlobGc, collectLiveBlobRefs, collectPersistedBlobRefs, planBlobGc, } from './blob-gc.js';
|
|
19
|
+
import { defaultMetrics } from './metrics.js';
|
|
20
|
+
/**
|
|
21
|
+
* Smallest grace window an operator may configure.
|
|
22
|
+
*
|
|
23
|
+
* Zero used to be legal, and zero is the destructive value: it makes `cutoff`
|
|
24
|
+
* equal to `now`, so EVERY unreferenced blob is condemned regardless of age,
|
|
25
|
+
* including one uploaded milliseconds earlier by an in-flight share whose doc
|
|
26
|
+
* reference has not landed yet. The grace window exists precisely to cover that
|
|
27
|
+
* PUT-then-reference gap, so allowing it to be switched off defeats the only
|
|
28
|
+
* protection the sweep has.
|
|
29
|
+
*
|
|
30
|
+
* A minute is far above the real race (a single HTTP round trip) while still
|
|
31
|
+
* letting an operator sweep aggressively. The bound belongs where the behaviour
|
|
32
|
+
* stops being safe, not where the type stops being valid; tests that need a
|
|
33
|
+
* tighter window pass `graceMs` to the worker directly rather than through the
|
|
34
|
+
* environment.
|
|
35
|
+
*/
|
|
36
|
+
const MIN_BLOB_GC_GRACE_MS = 60_000;
|
|
37
|
+
/**
|
|
38
|
+
* Largest delay `setInterval` accepts (2^31 - 1 ms, about 24.8 days).
|
|
39
|
+
*
|
|
40
|
+
* Node clamps anything ABOVE this to 1ms, exactly as it does for NaN and for
|
|
41
|
+
* values below 1. So an over-large interval is not "sweeps rarely", it is
|
|
42
|
+
* "sweeps a thousand times a second" - the same flood as a zero, reached from
|
|
43
|
+
* the opposite direction. Rejected rather than clamped, since a number that
|
|
44
|
+
* large is a mistake either way.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
47
|
+
/**
|
|
48
|
+
* Periodic blob sweep. Mirrors `SnapshotWorker`: unref'd timer, one sweep in
|
|
49
|
+
* flight at a time, failures logged rather than thrown at the timer.
|
|
50
|
+
*
|
|
51
|
+
* Every sweep logs a line, INCLUDING zero-delete sweeps. A GC that silently
|
|
52
|
+
* stops running is the same defect class as the one that caused #2790, so the
|
|
53
|
+
* healthy case has to be visible too, and failures increment a counter.
|
|
54
|
+
*/
|
|
55
|
+
export class BlobGcWorker {
|
|
56
|
+
timer = null;
|
|
57
|
+
inflight = null;
|
|
58
|
+
opts;
|
|
59
|
+
constructor(opts) {
|
|
60
|
+
this.opts = opts;
|
|
61
|
+
}
|
|
62
|
+
start() {
|
|
63
|
+
if (this.timer)
|
|
64
|
+
return;
|
|
65
|
+
// Sweep once at boot, BEFORE arming the interval. `setInterval` does not
|
|
66
|
+
// fire immediately, so with the default 6h period a process that restarts
|
|
67
|
+
// more often than that never completes a sweep at all: the GC is present
|
|
68
|
+
// in the code and absent in effect, and the volume fills exactly as in
|
|
69
|
+
// #2790. Hosted deploys restart on redeploy, OOM and platform events, so
|
|
70
|
+
// this is the normal case rather than an edge one.
|
|
71
|
+
//
|
|
72
|
+
// It is deliberately not awaited: startup must not block on a disk scan,
|
|
73
|
+
// and a failure here is counted and logged like any other sweep.
|
|
74
|
+
void this.runOnce().catch((err) => {
|
|
75
|
+
this.opts.counters?.failure?.();
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.error('[blob-gc] initial sweep failed:', err);
|
|
78
|
+
});
|
|
79
|
+
this.timer = setInterval(() => {
|
|
80
|
+
void this.runOnce().catch((err) => {
|
|
81
|
+
this.opts.counters?.failure?.();
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.error('[blob-gc] sweep failed:', err);
|
|
84
|
+
});
|
|
85
|
+
}, this.opts.intervalMs ?? 6 * 60 * 60 * 1000);
|
|
86
|
+
this.timer.unref?.();
|
|
87
|
+
}
|
|
88
|
+
stop() {
|
|
89
|
+
if (this.timer)
|
|
90
|
+
clearInterval(this.timer);
|
|
91
|
+
this.timer = null;
|
|
92
|
+
}
|
|
93
|
+
async runOnce() {
|
|
94
|
+
if (this.inflight)
|
|
95
|
+
return this.inflight;
|
|
96
|
+
this.inflight = this.sweep().finally(() => {
|
|
97
|
+
this.inflight = null;
|
|
98
|
+
});
|
|
99
|
+
return this.inflight;
|
|
100
|
+
}
|
|
101
|
+
async sweep() {
|
|
102
|
+
const now = this.opts.now?.() ?? Date.now();
|
|
103
|
+
const graceMs = this.opts.graceMs ?? DEFAULT_BLOB_GC_GRACE_MS;
|
|
104
|
+
// Live scan first: see the module note on the unload race.
|
|
105
|
+
const live = await collectLiveBlobRefs(this.opts.roomManager);
|
|
106
|
+
const persisted = await collectPersistedBlobRefs(this.opts.dataDir);
|
|
107
|
+
const referenced = new Set([...live, ...persisted.refs]);
|
|
108
|
+
const plan = await planBlobGc({
|
|
109
|
+
blobsDir: path.join(this.opts.dataDir, 'blobs'),
|
|
110
|
+
referenced,
|
|
111
|
+
roomLogs: persisted.roomLogs,
|
|
112
|
+
graceMs,
|
|
113
|
+
now,
|
|
114
|
+
});
|
|
115
|
+
// Re-check live references immediately before deleting. Between the scan
|
|
116
|
+
// and this point a LOADED room can gain a reference to an already-old blob
|
|
117
|
+
// through an ordinary CRDT update, and the per-hash lock only serialises
|
|
118
|
+
// against PUTs, not against doc writes. Re-reading the in-memory rooms is
|
|
119
|
+
// cheap and closes that window for every room the server has loaded.
|
|
120
|
+
//
|
|
121
|
+
// It does NOT make the race impossible: a reference landing between this
|
|
122
|
+
// check and the unlink still loses. It is narrow in practice because the
|
|
123
|
+
// client always re-PUTs a blob before referencing it (`HttpBlobStore.put`
|
|
124
|
+
// has no dedupe), which refreshes mtime and takes the lock, and because a
|
|
125
|
+
// fork copies references out of a room that already holds them, so those
|
|
126
|
+
// blobs were never candidates.
|
|
127
|
+
const recheck = await collectLiveBlobRefs(this.opts.roomManager);
|
|
128
|
+
const safe = plan.deleteHashes.filter((h) => !recheck.has(h));
|
|
129
|
+
const skipped = plan.deleteHashes.length - safe.length;
|
|
130
|
+
if (skipped > 0) {
|
|
131
|
+
// eslint-disable-next-line no-console
|
|
132
|
+
console.log(`[blob-gc] ${skipped} blob(s) became referenced mid-sweep; keeping them`);
|
|
133
|
+
}
|
|
134
|
+
const result = await applyBlobGc(this.opts.storage, { ...plan, deleteHashes: safe }, now - graceMs);
|
|
135
|
+
this.opts.counters?.sweep?.();
|
|
136
|
+
if (result.deletedBlobs > 0)
|
|
137
|
+
this.opts.counters?.deleted?.(result.deletedBlobs);
|
|
138
|
+
// eslint-disable-next-line no-console
|
|
139
|
+
console.log(`[blob-gc] swept ${plan.roomLogs} room log(s): deleted ${result.deletedBlobs} blob(s), ` +
|
|
140
|
+
`${result.deletedTmp} stale upload(s), kept ${result.kept}`);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resolve blob-GC settings from the environment.
|
|
146
|
+
*
|
|
147
|
+
* Exported, and `main` calls it, specifically so the wiring can be tested. A
|
|
148
|
+
* test that constructs its own worker would pass with this wiring deleted,
|
|
149
|
+
* which is the failure mode that let #2790 happen: a sweep that silently never
|
|
150
|
+
* runs looks exactly like a sweep with nothing to do.
|
|
151
|
+
*/
|
|
152
|
+
export function resolveBlobGcConfig(env = process.env) {
|
|
153
|
+
const flag = env.COLLAB_BLOB_GC;
|
|
154
|
+
return {
|
|
155
|
+
// Default ON. Blobs are never otherwise deleted, so an operator who does
|
|
156
|
+
// nothing must still be protected from the inode exhaustion in #2790.
|
|
157
|
+
enabled: flag !== '0' && flag !== 'false',
|
|
158
|
+
intervalMs: duration(env.COLLAB_BLOB_GC_INTERVAL_MS, 6 * 60 * 60 * 1000, 'COLLAB_BLOB_GC_INTERVAL_MS', 1, MAX_TIMER_DELAY_MS),
|
|
159
|
+
graceMs: duration(env.COLLAB_BLOB_GC_GRACE_MS, DEFAULT_BLOB_GC_GRACE_MS, 'COLLAB_BLOB_GC_GRACE_MS', MIN_BLOB_GC_GRACE_MS),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Parse a duration env var, rejecting anything not finite and in range.
|
|
164
|
+
*
|
|
165
|
+
* A bare `Number()` here is a data-loss bug, not a style issue: `Number('')`
|
|
166
|
+
* and `Number('abc')` give 0 and NaN. A NaN grace makes `cutoff` NaN, and
|
|
167
|
+
* `stat.mtimeMs >= NaN` is FALSE, so `planBlobGc` falls through and condemns
|
|
168
|
+
* EVERY unreferenced blob regardless of age, destroying the race protection.
|
|
169
|
+
* A NaN or zero interval makes `setInterval` fire about every millisecond.
|
|
170
|
+
* Fail loudly at startup instead: a misconfigured sweep must not run at all.
|
|
171
|
+
*/
|
|
172
|
+
function duration(raw, fallback, name, min, max = Number.MAX_SAFE_INTEGER) {
|
|
173
|
+
if (raw === undefined || raw === '')
|
|
174
|
+
return fallback;
|
|
175
|
+
const n = Number(raw);
|
|
176
|
+
if (!Number.isFinite(n) || n < min || n > max) {
|
|
177
|
+
throw new Error(`[blob-gc] ${name}=${JSON.stringify(raw)} is not a finite number in [${min}, ${max}]; refusing to start`);
|
|
178
|
+
}
|
|
179
|
+
return n;
|
|
180
|
+
}
|
|
181
|
+
/** Arm the periodic blob sweep, or return null when disabled. */
|
|
182
|
+
export function startBlobGc(deps) {
|
|
183
|
+
if (!deps.config.enabled)
|
|
184
|
+
return null;
|
|
185
|
+
// Default the counters onto the package metrics singleton, which is what
|
|
186
|
+
// `startCollabServer` publishes at `/metrics`. Without this the "a failing
|
|
187
|
+
// sweep is visible" claim above is false: `bin.ts` is the only production
|
|
188
|
+
// construction path and a failing sweep would produce console output only.
|
|
189
|
+
const sweeps = defaultMetrics.counter('collab_blob_gc_sweeps_total', 'Blob GC sweeps completed');
|
|
190
|
+
const failures = defaultMetrics.counter('collab_blob_gc_failures_total', 'Blob GC sweeps that threw');
|
|
191
|
+
const removed = defaultMetrics.counter('collab_blob_gc_deleted_total', 'Blobs deleted by GC');
|
|
192
|
+
const worker = new BlobGcWorker({
|
|
193
|
+
dataDir: deps.dataDir,
|
|
194
|
+
storage: deps.storage,
|
|
195
|
+
roomManager: deps.roomManager,
|
|
196
|
+
intervalMs: deps.config.intervalMs,
|
|
197
|
+
graceMs: deps.config.graceMs,
|
|
198
|
+
counters: deps.counters ?? {
|
|
199
|
+
sweep: () => sweeps.inc(),
|
|
200
|
+
failure: () => failures.inc(),
|
|
201
|
+
deleted: (n) => removed.inc(n),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
worker.start();
|
|
205
|
+
return worker;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=blob-gc-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-gc-worker.js","sourceRoot":"","sources":["../src/blob-gc-worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { RoomManager } from './room-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* How long an unreferenced blob must sit untouched before it can be collected.
|
|
4
|
+
*
|
|
5
|
+
* A client PUTs every blob BEFORE writing any hash into the doc, so there is
|
|
6
|
+
* always a window in which a perfectly good blob has no reference yet. 24h is
|
|
7
|
+
* far beyond that window while still bounding growth. Referenced blobs are
|
|
8
|
+
* immune at any age, so this only sizes the race margin, not retention.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_BLOB_GC_GRACE_MS: number;
|
|
11
|
+
export interface BlobGcPlan {
|
|
12
|
+
/** Blob hashes safe to delete. */
|
|
13
|
+
readonly deleteHashes: string[];
|
|
14
|
+
/** Absolute paths of stale `<hash>.tmp-*` upload leftovers. */
|
|
15
|
+
readonly deleteTmpPaths: string[];
|
|
16
|
+
/** Blobs kept because they are referenced or inside the grace window. */
|
|
17
|
+
readonly kept: number;
|
|
18
|
+
/** How many room logs contributed references (for the log line). */
|
|
19
|
+
readonly roomLogs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface BlobRefScan {
|
|
22
|
+
readonly refs: Set<string>;
|
|
23
|
+
readonly roomLogs: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Union of blob hashes referenced by every room log persisted under `dataDir`.
|
|
27
|
+
*
|
|
28
|
+
* Enumerates FILES rather than asking the RoomManager, so rooms that are
|
|
29
|
+
* persisted but not currently loaded are included. Throws on anything it
|
|
30
|
+
* cannot read; see the module header.
|
|
31
|
+
*/
|
|
32
|
+
export declare function collectPersistedBlobRefs(dataDir: string): Promise<BlobRefScan>;
|
|
33
|
+
/**
|
|
34
|
+
* Union of blob hashes referenced by rooms currently loaded in memory.
|
|
35
|
+
*
|
|
36
|
+
* Run this BEFORE the disk scan. `RoomManager.unload` removes a room from its
|
|
37
|
+
* map before awaiting the compact inside `destroy()`, so a room can be briefly
|
|
38
|
+
* invisible to the live scan while its final state has not yet landed on disk.
|
|
39
|
+
* Scanning live-then-disk means an unload racing the sweep is most likely to be
|
|
40
|
+
* caught by the disk half. The residual - references that exist ONLY in memory,
|
|
41
|
+
* which needs appends to have been failing - is accepted and documented.
|
|
42
|
+
*/
|
|
43
|
+
export declare function collectLiveBlobRefs(roomManager?: RoomManager): Promise<Set<string>>;
|
|
44
|
+
export interface PlanBlobGcOptions {
|
|
45
|
+
readonly blobsDir: string;
|
|
46
|
+
readonly referenced: ReadonlySet<string>;
|
|
47
|
+
readonly roomLogs: number;
|
|
48
|
+
readonly graceMs?: number;
|
|
49
|
+
readonly now?: number;
|
|
50
|
+
}
|
|
51
|
+
/** Decide what to delete. Pure apart from reading the blob directory. */
|
|
52
|
+
export declare function planBlobGc(opts: PlanBlobGcOptions): Promise<BlobGcPlan>;
|
|
53
|
+
/** Blob storage able to delete only if the file is older than a cutoff. */
|
|
54
|
+
export interface GcBlobStorage {
|
|
55
|
+
deleteIfOlderThan(hash: string, cutoffEpochMs: number): Promise<boolean>;
|
|
56
|
+
}
|
|
57
|
+
export interface BlobGcResult {
|
|
58
|
+
readonly deletedBlobs: number;
|
|
59
|
+
readonly deletedTmp: number;
|
|
60
|
+
readonly kept: number;
|
|
61
|
+
readonly roomLogs: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Execute a plan. Deletion goes through `deleteIfOlderThan` rather than a bare
|
|
65
|
+
* unlink so a blob re-uploaded between planning and applying survives: the
|
|
66
|
+
* storage re-checks mtime under the same per-hash lock the writer takes.
|
|
67
|
+
*/
|
|
68
|
+
export declare function applyBlobGc(storage: GcBlobStorage, plan: BlobGcPlan, cutoffEpochMs: number): Promise<BlobGcResult>;
|
|
69
|
+
//# sourceMappingURL=blob-gc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-gc.d.ts","sourceRoot":"","sources":["../src/blob-gc.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAQrD;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAAsB,CAAC;AAI5D,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;IAClC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAuCpF;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAUzF;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,yEAAyE;AACzE,wBAAsB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAmD7E;AAED,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,UAAU,EAChB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAevB"}
|
package/dist/blob-gc.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* Garbage collection for content-addressed blobs.
|
|
6
|
+
*
|
|
7
|
+
* Blobs are one file per mesh and, until this module, were NEVER deleted:
|
|
8
|
+
* `retention.ts` covers room LOGS only and `bin.ts` wired no blob retention.
|
|
9
|
+
* That is why production ran out of INODES rather than bytes (#2790): 305,741
|
|
10
|
+
* blobs against a 5 GB volume's 305,175 inodes, with only 2.9 GB of 4.9 GB
|
|
11
|
+
* used. Mean blob size is well under the 16 KB-per-inode default ratio, so
|
|
12
|
+
* inodes exhaust before bytes at ANY volume size. Growing the volume buys
|
|
13
|
+
* time; this is the structural fix.
|
|
14
|
+
*
|
|
15
|
+
* # Safety model
|
|
16
|
+
*
|
|
17
|
+
* Deleting a referenced blob is silent, permanent geometry loss, so the sweep
|
|
18
|
+
* is built to fail CLOSED. It deletes a blob only when ALL of these hold:
|
|
19
|
+
*
|
|
20
|
+
* 1. No PERSISTED room log references it. Every `*.log` in the data dir is
|
|
21
|
+
* read, not just the rooms currently loaded in memory - a room that is
|
|
22
|
+
* merely idle must not have its geometry collected.
|
|
23
|
+
* 2. No LOADED room references it. A room whose newest updates have not yet
|
|
24
|
+
* been compacted to disk is covered by the live scan.
|
|
25
|
+
* 3. It is older than `graceMs`. Clients PUT a blob and only then write its
|
|
26
|
+
* hash into the doc (`geometry-sync.ts`), so a fresh blob is legitimately
|
|
27
|
+
* unreferenced for a moment. The grace window covers that race.
|
|
28
|
+
*
|
|
29
|
+
* Blobs are shared BETWEEN rooms: the same content hash is reused by any room
|
|
30
|
+
* with the same mesh, and branch forks copy refs into sibling rooms without
|
|
31
|
+
* re-uploading. So references are unioned across every room and a blob
|
|
32
|
+
* survives if ANY room still points at it.
|
|
33
|
+
*
|
|
34
|
+
* # Absence must not read as success
|
|
35
|
+
*
|
|
36
|
+
* Every way of "seeing no references" that is not genuinely "no references"
|
|
37
|
+
* aborts the whole sweep rather than deleting:
|
|
38
|
+
*
|
|
39
|
+
* - an unreadable log directory, an undecodable room name, or a doc that
|
|
40
|
+
* throws on `applyUpdate`
|
|
41
|
+
* - **a non-empty log file that `FilePersistence.load` returns `null` for.**
|
|
42
|
+
* `load` does not throw on a corrupt log: an empty file, a truncated length
|
|
43
|
+
* prefix, or any garbage yielding zero complete frames all return `null`
|
|
44
|
+
* (`persistence.ts`). Treating that as "this room references nothing" would
|
|
45
|
+
* delete every blob belonging to a damaged room. A genuinely 0-byte log is
|
|
46
|
+
* the one safe case, since it cannot hide a reference.
|
|
47
|
+
* - zero room logs found while blobs exist, which is what a mispointed
|
|
48
|
+
* `dataDir` looks like.
|
|
49
|
+
*/
|
|
50
|
+
import * as fs from 'node:fs';
|
|
51
|
+
import * as path from 'node:path';
|
|
52
|
+
import * as Y from 'yjs';
|
|
53
|
+
import { collectReferencedBlobHashes } from '@ifc-lite/collab';
|
|
54
|
+
import { FilePersistence } from './persistence.js';
|
|
55
|
+
/** Match exactly 32 lowercase hex chars (the client's `fnv128` output). */
|
|
56
|
+
const HASH_REGEX = /^[a-f0-9]{32}$/;
|
|
57
|
+
/** In-flight upload leftovers: `<hash>.tmp-<uuid>`. */
|
|
58
|
+
const TMP_REGEX = /^[a-f0-9]{32}\.tmp-/;
|
|
59
|
+
/**
|
|
60
|
+
* How long an unreferenced blob must sit untouched before it can be collected.
|
|
61
|
+
*
|
|
62
|
+
* A client PUTs every blob BEFORE writing any hash into the doc, so there is
|
|
63
|
+
* always a window in which a perfectly good blob has no reference yet. 24h is
|
|
64
|
+
* far beyond that window while still bounding growth. Referenced blobs are
|
|
65
|
+
* immune at any age, so this only sizes the race margin, not retention.
|
|
66
|
+
*/
|
|
67
|
+
export const DEFAULT_BLOB_GC_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
68
|
+
/**
|
|
69
|
+
* Union of blob hashes referenced by every room log persisted under `dataDir`.
|
|
70
|
+
*
|
|
71
|
+
* Enumerates FILES rather than asking the RoomManager, so rooms that are
|
|
72
|
+
* persisted but not currently loaded are included. Throws on anything it
|
|
73
|
+
* cannot read; see the module header.
|
|
74
|
+
*/
|
|
75
|
+
export async function collectPersistedBlobRefs(dataDir) {
|
|
76
|
+
const refs = new Set();
|
|
77
|
+
let roomLogs = 0;
|
|
78
|
+
// Mirrors the existing room-log scan in `access-control.ts`: files only, so
|
|
79
|
+
// the `blobs/` and `layer-registry/` subdirectories are skipped.
|
|
80
|
+
const entries = await fs.promises.readdir(dataDir, { withFileTypes: true });
|
|
81
|
+
const persistence = new FilePersistence({ dataDir });
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry.isFile() || !entry.name.endsWith('.log'))
|
|
84
|
+
continue;
|
|
85
|
+
roomLogs += 1;
|
|
86
|
+
const roomId = decodeURIComponent(entry.name.slice(0, -'.log'.length));
|
|
87
|
+
const update = await persistence.load(roomId);
|
|
88
|
+
if (update === null) {
|
|
89
|
+
// `load` returns null for corrupt logs as well as empty ones. A 0-byte
|
|
90
|
+
// file cannot hide a reference; anything larger might, so refuse to
|
|
91
|
+
// treat it as an empty room.
|
|
92
|
+
const stat = await fs.promises.stat(path.join(dataDir, entry.name));
|
|
93
|
+
if (stat.size > 0) {
|
|
94
|
+
throw new Error(`[blob-gc] room log ${entry.name} is ${stat.size} bytes but parsed to nothing; ` +
|
|
95
|
+
'refusing to sweep, since a log we cannot read may hold blob references');
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const doc = new Y.Doc();
|
|
100
|
+
try {
|
|
101
|
+
Y.applyUpdate(doc, update);
|
|
102
|
+
for (const hash of collectReferencedBlobHashes(doc))
|
|
103
|
+
refs.add(hash);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
doc.destroy();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { refs, roomLogs };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Union of blob hashes referenced by rooms currently loaded in memory.
|
|
113
|
+
*
|
|
114
|
+
* Run this BEFORE the disk scan. `RoomManager.unload` removes a room from its
|
|
115
|
+
* map before awaiting the compact inside `destroy()`, so a room can be briefly
|
|
116
|
+
* invisible to the live scan while its final state has not yet landed on disk.
|
|
117
|
+
* Scanning live-then-disk means an unload racing the sweep is most likely to be
|
|
118
|
+
* caught by the disk half. The residual - references that exist ONLY in memory,
|
|
119
|
+
* which needs appends to have been failing - is accepted and documented.
|
|
120
|
+
*/
|
|
121
|
+
export async function collectLiveBlobRefs(roomManager) {
|
|
122
|
+
const refs = new Set();
|
|
123
|
+
if (!roomManager)
|
|
124
|
+
return refs;
|
|
125
|
+
for (const roomId of roomManager.list()) {
|
|
126
|
+
const pending = roomManager.peek(roomId);
|
|
127
|
+
if (!pending)
|
|
128
|
+
continue;
|
|
129
|
+
const room = await pending;
|
|
130
|
+
for (const hash of collectReferencedBlobHashes(room.doc))
|
|
131
|
+
refs.add(hash);
|
|
132
|
+
}
|
|
133
|
+
return refs;
|
|
134
|
+
}
|
|
135
|
+
/** Decide what to delete. Pure apart from reading the blob directory. */
|
|
136
|
+
export async function planBlobGc(opts) {
|
|
137
|
+
const graceMs = opts.graceMs ?? DEFAULT_BLOB_GC_GRACE_MS;
|
|
138
|
+
const now = opts.now ?? Date.now();
|
|
139
|
+
const cutoff = now - graceMs;
|
|
140
|
+
let names;
|
|
141
|
+
try {
|
|
142
|
+
names = await fs.promises.readdir(opts.blobsDir);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
// A server that has never stored a blob has no blobs dir yet: it is
|
|
146
|
+
// created lazily by FsBlobStorage. Nothing to do, and emphatically not a
|
|
147
|
+
// failure - a loud error every sweep would train operators to ignore the
|
|
148
|
+
// one log line that makes a dead GC visible.
|
|
149
|
+
if (err.code === 'ENOENT') {
|
|
150
|
+
return { deleteHashes: [], deleteTmpPaths: [], kept: 0, roomLogs: opts.roomLogs };
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
const deleteHashes = [];
|
|
155
|
+
const deleteTmpPaths = [];
|
|
156
|
+
let kept = 0;
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
const full = path.join(opts.blobsDir, name);
|
|
159
|
+
if (TMP_REGEX.test(name)) {
|
|
160
|
+
const stat = await fs.promises.stat(full).catch(() => null);
|
|
161
|
+
if (stat && stat.mtimeMs < cutoff)
|
|
162
|
+
deleteTmpPaths.push(full);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!HASH_REGEX.test(name))
|
|
166
|
+
continue;
|
|
167
|
+
if (opts.referenced.has(name)) {
|
|
168
|
+
kept += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const stat = await fs.promises.stat(full).catch(() => null);
|
|
172
|
+
if (!stat || stat.mtimeMs >= cutoff) {
|
|
173
|
+
kept += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
deleteHashes.push(name);
|
|
177
|
+
}
|
|
178
|
+
if (opts.roomLogs === 0 && deleteHashes.length > 0) {
|
|
179
|
+
throw new Error(`[blob-gc] found ${deleteHashes.length} unreferenced blob(s) but ZERO room logs; ` +
|
|
180
|
+
'refusing to sweep, since this is what a wrong dataDir looks like');
|
|
181
|
+
}
|
|
182
|
+
return { deleteHashes, deleteTmpPaths, kept, roomLogs: opts.roomLogs };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Execute a plan. Deletion goes through `deleteIfOlderThan` rather than a bare
|
|
186
|
+
* unlink so a blob re-uploaded between planning and applying survives: the
|
|
187
|
+
* storage re-checks mtime under the same per-hash lock the writer takes.
|
|
188
|
+
*/
|
|
189
|
+
export async function applyBlobGc(storage, plan, cutoffEpochMs) {
|
|
190
|
+
let deletedBlobs = 0;
|
|
191
|
+
for (const hash of plan.deleteHashes) {
|
|
192
|
+
if (await storage.deleteIfOlderThan(hash, cutoffEpochMs))
|
|
193
|
+
deletedBlobs += 1;
|
|
194
|
+
}
|
|
195
|
+
let deletedTmp = 0;
|
|
196
|
+
for (const tmp of plan.deleteTmpPaths) {
|
|
197
|
+
try {
|
|
198
|
+
await fs.promises.unlink(tmp);
|
|
199
|
+
deletedTmp += 1;
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err.code !== 'ENOENT')
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { deletedBlobs, deletedTmp, kept: plan.kept, roomLogs: plan.roomLogs };
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=blob-gc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-gc.js","sourceRoot":"","sources":["../src/blob-gc.ts"],"names":[],"mappings":"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"}
|