@ifc-lite/collab-server 0.5.1 → 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 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: new FsBlobStorage(dataDir),
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
- console.log(`[collab-server] listening at ${handle.url} (data: ${dataDir}, auth: ${tokenSecret ? 'room-token' : 'anonymous'}, registry: ${layerRegistryEnabled ? 'fs' : 'off'})`);
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":";AACA;;+DAE+D;AAE/D,iDAAiD;AAEjD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,wEAAwE;AACxE,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AACzE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,SAAS,CAAC;AAClD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,gBAAgB,CAAC;AAChE,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,CAAC;AAC9D,yEAAyE;AACzE,+EAA+E;AAC/E,+EAA+E;AAC/E,+EAA+E;AAC/E,sEAAsE;AACtE,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;AACpD,6EAA6E;AAC7E,0EAA0E;AAC1E,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,QAAQ,CACjD,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CACxD,CAAC;AACF,sEAAsE;AACtE,0EAA0E;AAC1E,MAAM,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC;AACnE,MAAM,gBAAgB,GAAG,kBAAkB;IACzC,CAAC,CAAC;QACE;YACE,GAAG,EAAE,kBAAkB;YACvB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,8BAA8B;gBAC5C,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,8BAA8B,EAAE;gBACxD,CAAC,CAAC,EAAE,CAAC;SACR;KACF;IACH,CAAC,CAAC,EAAE,CAAC;AAEP,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,QAAQ,GAAG,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,WAAW,CAAC;QAChF,0EAA0E;QAC1E,sEAAsE;QACtE,4EAA4E;QAC5E,8BAA8B;QAC9B,sCAAsC;QACtC,OAAO,CAAC,IAAI,CACV,QAAQ;YACN,CAAC,CAAC,kIAAkI;YACpI,CAAC,CAAC,yFAAyF,IAAI,gGAAgG,CAClM,CAAC;IACJ,CAAC;IACD,2EAA2E;IAC3E,0EAA0E;IAC1E,oCAAoC;IACpC,MAAM,aAAa,GAAG,WAAW;QAC/B,CAAC,CAAC,mBAAmB,CAAC;YAClB,MAAM,EAAE,WAAW;YACnB,GAAG,EAAE,OAAO;YACZ,eAAe,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,OAAO,CAAC;YACxE,wEAAwE;YACxE,uEAAuE;YACvE,+DAA+D;YAC/D,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,QAAQ,CACvC,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CACrD;SACF,CAAC;QACJ,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC;QACrC,IAAI;QACJ,IAAI;QACJ,WAAW,EAAE,IAAI,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,WAAW,EAAE,IAAI,aAAa,CAAC,OAAO,CAAC;QACvC,QAAQ;QACR,0EAA0E;QAC1E,0EAA0E;QAC1E,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,GAAG,CAAC,oBAAoB;YACtB,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,eAAe,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,EAAE;YACxF,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC,CAAC;IACH,sCAAsC;IACtC,OAAO,CAAC,GAAG,CACT,gCAAgC,MAAM,CAAC,GAAG,WAAW,OAAO,WAAW,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,eAAe,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,CACrK,CAAC;IAEF,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,sCAAsC;QACtC,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAC9C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,qEAAqE;QACrE,uEAAuE;QACvE,uEAAuE;QACvE,uDAAuD;QACvD,IAAI,aAAa,EAAE,CAAC;YAClB,IAAI,CAAC;gBACH,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC;YAC9B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,sCAAsC;gBACtC,OAAO,CAAC,KAAK,CACX,kIAAkI,EAClI,GAAG,CACJ,CAAC;gBACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
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"}
@@ -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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"}