@camstack/addon-pipeline 1.2.77 → 1.2.78
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/{addon-utils-bZcwLaVW.js → addon-utils-C-XbUkiG.js} +1 -1
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-DQZzL0Ri.js → dist-BdVCXl5n.js} +74 -5
- package/dist/{dist-HhcoV-Ky.mjs → dist-CsP_DikG.mjs} +74 -5
- package/dist/{event-loop-stall-monitor-CBOo1oh_.js → event-loop-stall-monitor-BOu8lGee.js} +1 -1
- package/dist/{event-loop-stall-monitor-CqBEWGSZ.mjs → event-loop-stall-monitor-C3cvE_Xk.mjs} +1 -1
- package/dist/{lazy-sharp-DIC1rpea.js → lazy-sharp-1LkmyWqV.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/recorder/index.js +293 -34
- package/dist/recorder/index.mjs +292 -33
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-De_xzvjU.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DUi-lR4R.mjs} +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-1QMyGZMB.mjs +26 -0
- package/dist/stream-broker/{hostInit-CDS5frmk.mjs → hostInit-DEtsjgBO.mjs} +1 -1
- package/dist/stream-broker/index.js +106 -23
- package/dist/stream-broker/index.mjs +106 -23
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DZVGVAPU.js → worker-protocol-B4fPjmXk.js} +1 -1
- package/dist/{worker-protocol-BNlLGLBV.mjs → worker-protocol-BRwzX3f_.mjs} +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DHJSxHH1.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const require_dist = require("../dist-
|
|
1
|
+
const require_dist = require("../dist-BdVCXl5n.js");
|
|
2
2
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
3
|
-
const require_addon_utils = require("../addon-utils-
|
|
3
|
+
const require_addon_utils = require("../addon-utils-C-XbUkiG.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_child_process = require("node:child_process");
|
|
6
6
|
let node_path = require("node:path");
|
|
@@ -390,7 +390,21 @@ async function readMfraTable(absPath, fileBytes) {
|
|
|
390
390
|
/** How long a table stays served. Directory asks are for recent footage far
|
|
391
391
|
* more often than not, and a bounded window keeps this store O(day), not
|
|
392
392
|
* O(uptime): ~30 bytes a segment ⇒ a day across 20 cameras is a few MB. */
|
|
393
|
-
var
|
|
393
|
+
var MFRA_RETAIN_MS = 26 * 36e5;
|
|
394
|
+
/**
|
|
395
|
+
* Whether a segment is recent enough for a captured table to ever be served.
|
|
396
|
+
*
|
|
397
|
+
* The gate the FINALIZE path must ask BEFORE reading the file. A live segment
|
|
398
|
+
* always passes; a recovered one almost never does, and capturing it is pure
|
|
399
|
+
* loss twice over — the table is unservable, and the read is a real `open` +
|
|
400
|
+
* two `read`s on the libuv threadpool. The D148 boot reconcile fired 122 400 of
|
|
401
|
+
* them, unawaited and unbounded, behind a 4-thread pool that the live
|
|
402
|
+
* SegmentWriters' own finalize I/O has to share: that is the "segment file
|
|
403
|
+
* never landed" (>30 s) on four cameras at once.
|
|
404
|
+
*/
|
|
405
|
+
function isMfraTableServable(startMs, nowMs) {
|
|
406
|
+
return startMs >= nowMs - MFRA_RETAIN_MS;
|
|
407
|
+
}
|
|
394
408
|
/**
|
|
395
409
|
* The RAM store the directory reads. Keys are `(deviceId, profile, startMs)`
|
|
396
410
|
* — exactly how a directory row identifies a segment. Insertion is
|
|
@@ -406,7 +420,16 @@ var MfraTableStore = class {
|
|
|
406
420
|
key(deviceId, profile, startMs) {
|
|
407
421
|
return `${deviceId}:${profile}:${startMs}`;
|
|
408
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Keep one table, unless it is already past the serve window.
|
|
425
|
+
*
|
|
426
|
+
* The out-of-window REJECT is not belt-and-braces: `prune` walks the
|
|
427
|
+
* insertion-ordered prefix and stops at the first live entry, so an expired
|
|
428
|
+
* entry appended AFTER a live one is never reachable again. A bulk recovery
|
|
429
|
+
* appends 122 400 of exactly that shape and the store grows without bound.
|
|
430
|
+
*/
|
|
409
431
|
record(deviceId, profile, startMs, table) {
|
|
432
|
+
if (!isMfraTableServable(startMs, this.now())) return;
|
|
410
433
|
this.tables.set(this.key(deviceId, profile, startMs), {
|
|
411
434
|
startMs,
|
|
412
435
|
table
|
|
@@ -427,7 +450,7 @@ var MfraTableStore = class {
|
|
|
427
450
|
}
|
|
428
451
|
/** Insertion order is time order, so the expired prefix is contiguous. */
|
|
429
452
|
prune() {
|
|
430
|
-
const cutoff = this.now() -
|
|
453
|
+
const cutoff = this.now() - MFRA_RETAIN_MS;
|
|
431
454
|
for (const [key, entry] of this.tables) {
|
|
432
455
|
if (entry.startMs >= cutoff) break;
|
|
433
456
|
this.tables.delete(key);
|
|
@@ -626,6 +649,38 @@ var RecordingIndex = class {
|
|
|
626
649
|
segments(deviceId, profile) {
|
|
627
650
|
return [...this.sortedSegments(deviceId, profile)];
|
|
628
651
|
}
|
|
652
|
+
/** True when `relPath` is currently indexed for `deviceId`. O(1). */
|
|
653
|
+
hasSegment(deviceId, relPath) {
|
|
654
|
+
return this.byDevice.get(deviceId)?.has(relPath) === true;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Indexed-segment count per HOUR-BUCKET directory (`<dev>/<profile>/Y/M/D/H`)
|
|
658
|
+
* for one (device, profile), in ONE pass over the device's rows.
|
|
659
|
+
*
|
|
660
|
+
* Exists because `SegmentStore.evict` has to answer "does this eviction cover
|
|
661
|
+
* every segment the bucket holds?" for every bucket in the round, and it used
|
|
662
|
+
* to answer it with `segments(deviceId, profile)` — a full COPY of the
|
|
663
|
+
* device-profile archive plus a full `startsWith` scan, PER BUCKET. That is
|
|
664
|
+
* O(buckets x archive), and D148 is what made it bite: before it, a retention
|
|
665
|
+
* round touched a handful of buckets at the retention edge; after it, the
|
|
666
|
+
* recovered backlog made ~8 900 sparse buckets across six weeks evictable in
|
|
667
|
+
* ONE round. Measured on that shape (1.63 M live rows + 122 400 victims):
|
|
668
|
+
* 182 596 ms of uninterrupted main-thread CPU, with no log line, which is the
|
|
669
|
+
* silent 100%-CPU phase the operator saw. One pass answers all of them.
|
|
670
|
+
*/
|
|
671
|
+
bucketCounts(deviceId, profile) {
|
|
672
|
+
const out = /* @__PURE__ */ new Map();
|
|
673
|
+
const m = this.byDevice.get(deviceId);
|
|
674
|
+
if (!m) return out;
|
|
675
|
+
for (const s of m.values()) {
|
|
676
|
+
if (s.profile !== profile) continue;
|
|
677
|
+
const cut = s.path.lastIndexOf("/");
|
|
678
|
+
if (cut <= 0) continue;
|
|
679
|
+
const bucketDir = s.path.slice(0, cut);
|
|
680
|
+
out.set(bucketDir, (out.get(bucketDir) ?? 0) + 1);
|
|
681
|
+
}
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
629
684
|
/** Aggregate accounting for a device across ALL its locations (use `accountingForLocation` for one location). */
|
|
630
685
|
accounting(deviceId) {
|
|
631
686
|
const m = this.byDevice.get(deviceId);
|
|
@@ -1258,6 +1313,7 @@ var SegmentStore = class SegmentStore {
|
|
|
1258
1313
|
async evict(location, rows) {
|
|
1259
1314
|
let reclaimed = 0;
|
|
1260
1315
|
const removed = [];
|
|
1316
|
+
const startedMs = this.deps.now?.() ?? Date.now();
|
|
1261
1317
|
const byBucket = /* @__PURE__ */ new Map();
|
|
1262
1318
|
for (const r of rows) {
|
|
1263
1319
|
const cut = r.path.lastIndexOf("/");
|
|
@@ -1266,13 +1322,29 @@ var SegmentStore = class SegmentStore {
|
|
|
1266
1322
|
if (group) group.push(r);
|
|
1267
1323
|
else byBucket.set(bucketDir, [r]);
|
|
1268
1324
|
}
|
|
1325
|
+
const census = /* @__PURE__ */ new Map();
|
|
1326
|
+
const countsFor = (deviceId, profile) => {
|
|
1327
|
+
const key = `${deviceId}:${profile}`;
|
|
1328
|
+
const cached = census.get(key);
|
|
1329
|
+
if (cached) return cached;
|
|
1330
|
+
const counts = this.deps.index.bucketCounts(deviceId, profile);
|
|
1331
|
+
census.set(key, counts);
|
|
1332
|
+
return counts;
|
|
1333
|
+
};
|
|
1334
|
+
const chunkSize = this.deps.evictChunkSize ?? SegmentStore.DEFAULT_EVICT_CHUNK;
|
|
1335
|
+
const yieldBetween = this.deps.yieldBetween ?? macrotask$2;
|
|
1336
|
+
let bucketsRemoved = 0;
|
|
1337
|
+
let handled = 0;
|
|
1269
1338
|
for (const [bucketDir, bucketRows] of byBucket) {
|
|
1270
|
-
if (bucketDir !== "" && this.bucketFullyEvicted(bucketDir, bucketRows)) try {
|
|
1339
|
+
if (bucketDir !== "" && this.bucketFullyEvicted(bucketDir, bucketRows, countsFor)) try {
|
|
1271
1340
|
await this.deps.removeDir(location, bucketDir);
|
|
1341
|
+
bucketsRemoved += 1;
|
|
1272
1342
|
for (const r of bucketRows) {
|
|
1273
1343
|
removed.push(r.path);
|
|
1274
1344
|
reclaimed += r.bytes;
|
|
1275
1345
|
}
|
|
1346
|
+
handled += 1;
|
|
1347
|
+
if (handled % chunkSize === 0) await yieldBetween();
|
|
1276
1348
|
continue;
|
|
1277
1349
|
} catch (err) {
|
|
1278
1350
|
this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", {
|
|
@@ -1293,15 +1365,47 @@ var SegmentStore = class SegmentStore {
|
|
|
1293
1365
|
error: err
|
|
1294
1366
|
});
|
|
1295
1367
|
}
|
|
1368
|
+
handled += 1;
|
|
1369
|
+
if (handled % chunkSize === 0) await yieldBetween();
|
|
1296
1370
|
}
|
|
1297
1371
|
if (removed.length > 0) {
|
|
1298
1372
|
this.deps.index.removeSegments(removed);
|
|
1299
|
-
|
|
1373
|
+
this.notifyEvicted(rows);
|
|
1300
1374
|
}
|
|
1301
1375
|
await this.pruneEmptyAncestors(location, [...byBucket.keys()]);
|
|
1376
|
+
this.deps.logger.info("recorder: eviction round complete", { meta: {
|
|
1377
|
+
location,
|
|
1378
|
+
rows: rows.length,
|
|
1379
|
+
buckets: byBucket.size,
|
|
1380
|
+
bucketsRemoved,
|
|
1381
|
+
removed: removed.length,
|
|
1382
|
+
reclaimed,
|
|
1383
|
+
ms: (this.deps.now?.() ?? Date.now()) - startedMs
|
|
1384
|
+
} });
|
|
1302
1385
|
return reclaimed;
|
|
1303
1386
|
}
|
|
1304
1387
|
/**
|
|
1388
|
+
* Tell the calendar cache what went, ONCE PER (device, UTC day).
|
|
1389
|
+
*
|
|
1390
|
+
* `CalendarIndex.invalidate` drops a whole UTC day, so a per-ROW call repeats
|
|
1391
|
+
* the same work for every segment of that day — and its hour-list sweep is
|
|
1392
|
+
* O(cached hours), in one uninterrupted synchronous block with no await in
|
|
1393
|
+
* it. Measured over a 122 400-row round with 5 000 cached hours: 15 285 ms of
|
|
1394
|
+
* frozen event loop, scaling linearly with how long the session has been
|
|
1395
|
+
* scrubbing. Deduped it is ~190 calls and unmeasurable.
|
|
1396
|
+
*/
|
|
1397
|
+
notifyEvicted(rows) {
|
|
1398
|
+
const notify = this.deps.onEvicted;
|
|
1399
|
+
if (!notify) return;
|
|
1400
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1401
|
+
for (const r of rows) {
|
|
1402
|
+
const key = `${r.deviceId}:${Math.floor(r.startMs / SegmentStore.DAY_MS)}`;
|
|
1403
|
+
if (seen.has(key)) continue;
|
|
1404
|
+
seen.add(key);
|
|
1405
|
+
notify(r.deviceId, r.startMs);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1305
1409
|
* Walk up from each emptied hour directory removing ancestors that are now
|
|
1306
1410
|
* empty, so that **a directory's existence means footage**.
|
|
1307
1411
|
*
|
|
@@ -1336,22 +1440,37 @@ var SegmentStore = class SegmentStore {
|
|
|
1336
1440
|
}
|
|
1337
1441
|
/** Hour of one UTC hour-bucket in ms. */
|
|
1338
1442
|
static HOUR_MS = 36e5;
|
|
1443
|
+
static DAY_MS = 864e5;
|
|
1444
|
+
/** Buckets between yields. Small enough that one chunk is milliseconds of
|
|
1445
|
+
* work now the per-bucket archive scan is gone. */
|
|
1446
|
+
static DEFAULT_EVICT_CHUNK = 25;
|
|
1339
1447
|
/**
|
|
1340
1448
|
* True when `victims` cover EVERY indexed segment of `bucketDir` AND the
|
|
1341
1449
|
* bucket's hour has fully elapsed. The current-hour guard matters because
|
|
1342
1450
|
* `onFinalized` relocates new segments INTO the current bucket concurrently —
|
|
1343
1451
|
* an rm racing that rename would orphan a just-indexed segment.
|
|
1452
|
+
*
|
|
1453
|
+
* The cover test is a COUNT against the round's one-pass census plus an O(1)
|
|
1454
|
+
* indexed-ness check per victim, never a scan of the device archive: a
|
|
1455
|
+
* bucketDir is `<dev>/<profile>/Y/M/D/H`, so it names exactly one
|
|
1456
|
+
* (device, profile), and the victims of a bucket are unique paths. Equal
|
|
1457
|
+
* counts with every victim indexed therefore means the same set.
|
|
1344
1458
|
*/
|
|
1345
|
-
bucketFullyEvicted(bucketDir, victims) {
|
|
1459
|
+
bucketFullyEvicted(bucketDir, victims, countsFor) {
|
|
1346
1460
|
const first = victims[0];
|
|
1347
1461
|
if (!first) return false;
|
|
1348
1462
|
const bucketStartMs = Math.floor(first.startMs / SegmentStore.HOUR_MS) * SegmentStore.HOUR_MS;
|
|
1349
1463
|
if ((this.deps.now?.() ?? Date.now()) < bucketStartMs + SegmentStore.HOUR_MS) return false;
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1464
|
+
const indexedInBucket = countsFor(first.deviceId, first.profile).get(bucketDir) ?? 0;
|
|
1465
|
+
if (indexedInBucket === 0) return false;
|
|
1466
|
+
const indexedVictims = /* @__PURE__ */ new Set();
|
|
1467
|
+
for (const v of victims) if (this.deps.index.hasSegment(v.deviceId, v.path)) indexedVictims.add(v.path);
|
|
1468
|
+
return indexedInBucket === indexedVictims.size;
|
|
1353
1469
|
}
|
|
1354
1470
|
};
|
|
1471
|
+
var macrotask$2 = () => new Promise((resolve) => {
|
|
1472
|
+
setImmediate(resolve);
|
|
1473
|
+
});
|
|
1355
1474
|
//#endregion
|
|
1356
1475
|
//#region src/recorder/still/still-source.ts
|
|
1357
1476
|
/**
|
|
@@ -2012,7 +2131,7 @@ var CalendarIndex = class {
|
|
|
2012
2131
|
const day = utcDayOf(atMs);
|
|
2013
2132
|
this.byDevice.get(deviceId)?.delete(day);
|
|
2014
2133
|
const prefix = `${deviceId}:`;
|
|
2015
|
-
for (const key of
|
|
2134
|
+
for (const key of this.hourSegs.keys()) {
|
|
2016
2135
|
if (!key.startsWith(prefix)) continue;
|
|
2017
2136
|
if (utcDayOf(Number(key.slice(key.lastIndexOf(":") + 1))) === day) this.hourSegs.delete(key);
|
|
2018
2137
|
}
|
|
@@ -4839,9 +4958,12 @@ async function resolveRecordingsLocations(api, logger) {
|
|
|
4839
4958
|
* (no footage yet) and any per-location failure are warned + skipped
|
|
4840
4959
|
* (best-effort), so one bad volume never blocks the rest of the hydrate.
|
|
4841
4960
|
*/
|
|
4842
|
-
async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger) {
|
|
4961
|
+
async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger, options = {}) {
|
|
4962
|
+
const yieldBetween = options.yieldBetween ?? macrotask$1;
|
|
4963
|
+
const chunkSize = options.chunkSize ?? HYDRATE_CHUNK;
|
|
4843
4964
|
for (const [root, aliases] of aliasesByRoot(locations)) {
|
|
4844
4965
|
const deviceDir = node_path.default.join(root, String(deviceId));
|
|
4966
|
+
const startedMs = Date.now();
|
|
4845
4967
|
let entries;
|
|
4846
4968
|
try {
|
|
4847
4969
|
entries = await node_fs.promises.readdir(deviceDir, { recursive: true });
|
|
@@ -4855,17 +4977,48 @@ async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger
|
|
|
4855
4977
|
});
|
|
4856
4978
|
continue;
|
|
4857
4979
|
}
|
|
4980
|
+
const walkedMs = Date.now();
|
|
4981
|
+
const locationForProfile = /* @__PURE__ */ new Map();
|
|
4858
4982
|
const pathsByLocation = /* @__PURE__ */ new Map();
|
|
4859
|
-
|
|
4983
|
+
let handled = 0;
|
|
4984
|
+
for (const entry of entries) {
|
|
4985
|
+
if (!entry.endsWith(".m4s")) continue;
|
|
4860
4986
|
const relative = `${deviceId}/${entry.split(node_path.default.sep).join("/")}`;
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4987
|
+
const profile = relative.split("/")[1] ?? "";
|
|
4988
|
+
let locationId = locationForProfile.get(profile);
|
|
4989
|
+
if (locationId === void 0) {
|
|
4990
|
+
locationId = locationForHydratedProfile(aliases, profile).id;
|
|
4991
|
+
locationForProfile.set(profile, locationId);
|
|
4992
|
+
}
|
|
4993
|
+
const paths = pathsByLocation.get(locationId);
|
|
4863
4994
|
if (paths) paths.push(relative);
|
|
4864
|
-
else pathsByLocation.set(
|
|
4995
|
+
else pathsByLocation.set(locationId, [relative]);
|
|
4996
|
+
handled += 1;
|
|
4997
|
+
if (handled % chunkSize === 0) await yieldBetween();
|
|
4998
|
+
}
|
|
4999
|
+
let indexed = 0;
|
|
5000
|
+
for (const [locationId, relPaths] of pathsByLocation) {
|
|
5001
|
+
index.hydrateDevice(deviceId, locationId, relPaths);
|
|
5002
|
+
indexed += relPaths.length;
|
|
4865
5003
|
}
|
|
4866
|
-
|
|
5004
|
+
logger.info("recorder: hydrated a device from one recordings root", {
|
|
5005
|
+
tags: { deviceId },
|
|
5006
|
+
meta: {
|
|
5007
|
+
root,
|
|
5008
|
+
entries: entries.length,
|
|
5009
|
+
indexed,
|
|
5010
|
+
locationIds: [...pathsByLocation.keys()],
|
|
5011
|
+
walkMs: walkedMs - startedMs,
|
|
5012
|
+
ms: Date.now() - startedMs
|
|
5013
|
+
}
|
|
5014
|
+
});
|
|
4867
5015
|
}
|
|
4868
5016
|
}
|
|
5017
|
+
/** Entries classified between yields in {@link hydrateDeviceFromStorage}. */
|
|
5018
|
+
var HYDRATE_CHUNK = 2e4;
|
|
5019
|
+
var macrotask$1 = () => new Promise((resolve) => {
|
|
5020
|
+
setImmediate(resolve);
|
|
5021
|
+
});
|
|
4869
5022
|
/** `<deviceId>/<profile>/YYYY/MM/DD/HH` for an hour bucket (paths are UTC). */
|
|
4870
5023
|
function hourDirRelPath(deviceId, profile, hourStartMs) {
|
|
4871
5024
|
const d = new Date(hourStartMs);
|
|
@@ -6949,20 +7102,30 @@ var RetentionSweeper = class {
|
|
|
6949
7102
|
this.deps.logger.warn("recorder: retention sweep could not read device configs", { meta: { error: require_dist.errMsg(err) } });
|
|
6950
7103
|
return;
|
|
6951
7104
|
}
|
|
7105
|
+
const withPolicy = [...configs].filter(([, config]) => hasRetentionPolicy(config));
|
|
7106
|
+
const devicesSkipped = configs.size - withPolicy.length;
|
|
7107
|
+
this.deps.logger.info("recorder: retention sweep starting", { meta: {
|
|
7108
|
+
devices: withPolicy.map(([deviceId]) => deviceId),
|
|
7109
|
+
skipped: devicesSkipped
|
|
7110
|
+
} });
|
|
6952
7111
|
let devicesSwept = 0;
|
|
6953
|
-
let devicesSkipped = 0;
|
|
6954
7112
|
let bucketsPruned = 0;
|
|
6955
7113
|
let bytesReclaimed = 0;
|
|
6956
|
-
for (const [deviceId
|
|
6957
|
-
|
|
6958
|
-
devicesSkipped += 1;
|
|
6959
|
-
continue;
|
|
6960
|
-
}
|
|
7114
|
+
for (const [deviceId] of withPolicy) {
|
|
7115
|
+
const startedMs = Date.now();
|
|
6961
7116
|
try {
|
|
6962
7117
|
const result = await this.deps.prune(deviceId);
|
|
6963
7118
|
devicesSwept += 1;
|
|
6964
7119
|
bucketsPruned += result.deletedBuckets;
|
|
6965
7120
|
bytesReclaimed += result.reclaimedBytes;
|
|
7121
|
+
this.deps.logger.info("recorder: retention prune complete for device", {
|
|
7122
|
+
tags: { deviceId },
|
|
7123
|
+
meta: {
|
|
7124
|
+
deletedBuckets: result.deletedBuckets,
|
|
7125
|
+
reclaimedBytes: result.reclaimedBytes,
|
|
7126
|
+
ms: Date.now() - startedMs
|
|
7127
|
+
}
|
|
7128
|
+
});
|
|
6966
7129
|
} catch (err) {
|
|
6967
7130
|
this.deps.logger.warn("recorder: retention prune failed for device", {
|
|
6968
7131
|
tags: { deviceId },
|
|
@@ -6982,6 +7145,63 @@ var RetentionSweeper = class {
|
|
|
6982
7145
|
}
|
|
6983
7146
|
};
|
|
6984
7147
|
//#endregion
|
|
7148
|
+
//#region src/recorder/loop-pacer.ts
|
|
7149
|
+
var DEFAULT_BUSY_LAG_MS = 25;
|
|
7150
|
+
var DEFAULT_BACKOFF_STEP_MS = 25;
|
|
7151
|
+
var DEFAULT_MAX_BACKOFF_MS = 250;
|
|
7152
|
+
var LoopPacer = class {
|
|
7153
|
+
deps;
|
|
7154
|
+
consecutive = 0;
|
|
7155
|
+
hops = 0;
|
|
7156
|
+
contendedHops = 0;
|
|
7157
|
+
pausedMs = 0;
|
|
7158
|
+
maxLagMs = 0;
|
|
7159
|
+
constructor(deps) {
|
|
7160
|
+
this.deps = deps;
|
|
7161
|
+
}
|
|
7162
|
+
/**
|
|
7163
|
+
* One paced yield. Always hands the loop back at least once; sleeps longer
|
|
7164
|
+
* while the previous hop shows the loop is contended, and forgets the
|
|
7165
|
+
* back-off as soon as one hop comes back clean.
|
|
7166
|
+
*/
|
|
7167
|
+
async hop() {
|
|
7168
|
+
const busyLagMs = this.deps.busyLagMs ?? DEFAULT_BUSY_LAG_MS;
|
|
7169
|
+
const stepMs = this.deps.backoffStepMs ?? DEFAULT_BACKOFF_STEP_MS;
|
|
7170
|
+
const maxMs = this.deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
7171
|
+
const before = this.deps.now();
|
|
7172
|
+
await this.deps.sleep(0);
|
|
7173
|
+
const lagMs = this.deps.now() - before;
|
|
7174
|
+
this.hops += 1;
|
|
7175
|
+
if (lagMs > this.maxLagMs) this.maxLagMs = lagMs;
|
|
7176
|
+
if (lagMs < busyLagMs) {
|
|
7177
|
+
this.consecutive = 0;
|
|
7178
|
+
return;
|
|
7179
|
+
}
|
|
7180
|
+
this.consecutive += 1;
|
|
7181
|
+
this.contendedHops += 1;
|
|
7182
|
+
const pauseMs = Math.min(stepMs * this.consecutive, maxMs);
|
|
7183
|
+
this.pausedMs += pauseMs;
|
|
7184
|
+
await this.deps.sleep(pauseMs);
|
|
7185
|
+
}
|
|
7186
|
+
stats() {
|
|
7187
|
+
return {
|
|
7188
|
+
hops: this.hops,
|
|
7189
|
+
contendedHops: this.contendedHops,
|
|
7190
|
+
pausedMs: this.pausedMs,
|
|
7191
|
+
maxLagMs: this.maxLagMs
|
|
7192
|
+
};
|
|
7193
|
+
}
|
|
7194
|
+
};
|
|
7195
|
+
/** The production pacer: real clock, real timers. */
|
|
7196
|
+
function nodeLoopPacer() {
|
|
7197
|
+
return new LoopPacer({
|
|
7198
|
+
now: () => Date.now(),
|
|
7199
|
+
sleep: (ms) => new Promise((resolve) => {
|
|
7200
|
+
setTimeout(resolve, ms);
|
|
7201
|
+
})
|
|
7202
|
+
});
|
|
7203
|
+
}
|
|
7204
|
+
//#endregion
|
|
6985
7205
|
//#region src/recorder/addon/staging-reconcile.ts
|
|
6986
7206
|
/**
|
|
6987
7207
|
* Recover `.rec-tmp` segments that a writer restart orphaned.
|
|
@@ -7089,6 +7309,14 @@ var KNOWN_PROFILES = new Set([
|
|
|
7089
7309
|
]);
|
|
7090
7310
|
/** Default orphans per chunk. 590/high alone had 5 863 of them. */
|
|
7091
7311
|
var DEFAULT_CHUNK_SIZE = 100;
|
|
7312
|
+
/**
|
|
7313
|
+
* Orphans between progress lines.
|
|
7314
|
+
*
|
|
7315
|
+
* A directory of 5 863 files takes minutes over shfs and used to report only
|
|
7316
|
+
* when it finished, so "is it still going, and how far in" had no answer at
|
|
7317
|
+
* all — the exact shape of the blackout this pass was written to end.
|
|
7318
|
+
*/
|
|
7319
|
+
var PROGRESS_EVERY = 1e3;
|
|
7092
7320
|
var macrotask = () => new Promise((resolve) => {
|
|
7093
7321
|
setTimeout(resolve, 0);
|
|
7094
7322
|
});
|
|
@@ -7135,6 +7363,13 @@ async function recoverStagingOrphans(dir, input, deps) {
|
|
|
7135
7363
|
failed: 0,
|
|
7136
7364
|
aborted: false
|
|
7137
7365
|
};
|
|
7366
|
+
deps.logger.info("recorder: staging recovery starting on a directory", { meta: {
|
|
7367
|
+
dir,
|
|
7368
|
+
found: plan.length,
|
|
7369
|
+
staged: names.length,
|
|
7370
|
+
oldestMs: plan[0]?.startMs,
|
|
7371
|
+
newestMs: plan[plan.length - 1]?.startMs
|
|
7372
|
+
} });
|
|
7138
7373
|
const chunkSize = deps.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
7139
7374
|
const yieldBetween = deps.yieldBetween ?? macrotask;
|
|
7140
7375
|
let recovered = 0;
|
|
@@ -7165,6 +7400,13 @@ async function recoverStagingOrphans(dir, input, deps) {
|
|
|
7165
7400
|
error: err instanceof Error ? err.message : String(err)
|
|
7166
7401
|
} });
|
|
7167
7402
|
}
|
|
7403
|
+
if ((i + 1) % PROGRESS_EVERY === 0 && i + 1 < plan.length) deps.logger.info("recorder: staging recovery progress", { meta: {
|
|
7404
|
+
dir,
|
|
7405
|
+
done: i + 1,
|
|
7406
|
+
found: plan.length,
|
|
7407
|
+
recovered,
|
|
7408
|
+
failed
|
|
7409
|
+
} });
|
|
7168
7410
|
if ((i + 1) % chunkSize === 0 && i + 1 < plan.length) await yieldBetween();
|
|
7169
7411
|
}
|
|
7170
7412
|
deps.logger.info("recorder: recovered segments a writer restart had orphaned in staging", { meta: {
|
|
@@ -7209,7 +7451,16 @@ async function recoverAllStagedOrphans(deps) {
|
|
|
7209
7451
|
let recovered = 0;
|
|
7210
7452
|
let failed = 0;
|
|
7211
7453
|
let aborted = false;
|
|
7212
|
-
|
|
7454
|
+
const startedMs = Date.now();
|
|
7455
|
+
const injectedYield = deps.yieldBetween;
|
|
7456
|
+
const pacer = injectedYield === void 0 ? nodeLoopPacer() : null;
|
|
7457
|
+
const yieldBetween = injectedYield ?? (() => pacer === null ? macrotask() : pacer.hop());
|
|
7458
|
+
const locations = deps.locations();
|
|
7459
|
+
deps.logger.info("recorder: staging boot reconcile starting", { meta: {
|
|
7460
|
+
locations: locations.map((l) => l.id),
|
|
7461
|
+
bootMs: deps.bootMs
|
|
7462
|
+
} });
|
|
7463
|
+
for (const location of locations) {
|
|
7213
7464
|
const stagingRoot = `${location.root}/${STAGING_DIR_NAME}`;
|
|
7214
7465
|
let deviceDirs;
|
|
7215
7466
|
try {
|
|
@@ -7250,7 +7501,7 @@ async function recoverAllStagedOrphans(deps) {
|
|
|
7250
7501
|
}),
|
|
7251
7502
|
logger: withDeviceTag(deps.logger, deviceId),
|
|
7252
7503
|
chunkSize: deps.chunkSize,
|
|
7253
|
-
yieldBetween
|
|
7504
|
+
yieldBetween,
|
|
7254
7505
|
shouldStop: deps.shouldStop
|
|
7255
7506
|
});
|
|
7256
7507
|
found += report.found;
|
|
@@ -7260,12 +7511,14 @@ async function recoverAllStagedOrphans(deps) {
|
|
|
7260
7511
|
}
|
|
7261
7512
|
}
|
|
7262
7513
|
}
|
|
7263
|
-
|
|
7514
|
+
deps.logger.info("recorder: staging boot reconcile complete", { meta: {
|
|
7264
7515
|
dirs,
|
|
7265
7516
|
found,
|
|
7266
7517
|
recovered,
|
|
7267
7518
|
failed,
|
|
7268
|
-
aborted
|
|
7519
|
+
aborted,
|
|
7520
|
+
ms: Date.now() - startedMs,
|
|
7521
|
+
...pacer === null ? {} : { pacing: pacer.stats() }
|
|
7269
7522
|
} });
|
|
7270
7523
|
return {
|
|
7271
7524
|
dirs,
|
|
@@ -7475,6 +7728,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
7475
7728
|
this.calendar.invalidate(deviceId, atMs);
|
|
7476
7729
|
},
|
|
7477
7730
|
onIndexed: (row, absPath) => {
|
|
7731
|
+
if (!isMfraTableServable(row.startMs, Date.now())) return;
|
|
7478
7732
|
readMfraTable(absPath, row.bytes).then((table) => {
|
|
7479
7733
|
this.mfraTables.record(row.deviceId, row.profile, row.startMs, table);
|
|
7480
7734
|
}).catch((err) => {
|
|
@@ -7492,13 +7746,18 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
7492
7746
|
if (!loc) throw new Error(`recorder: unknown recordings location ${locationId}`);
|
|
7493
7747
|
await node_fs.promises.rmdir(node_path.default.join(loc.root, relDir));
|
|
7494
7748
|
},
|
|
7495
|
-
logger: {
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7749
|
+
logger: {
|
|
7750
|
+
warn: (message, extras) => {
|
|
7751
|
+
if (isSegmentStoreLogExtras(extras)) this.ctx.logger.warn(message, {
|
|
7752
|
+
tags: extras.tags,
|
|
7753
|
+
meta: { error: extras.error }
|
|
7754
|
+
});
|
|
7755
|
+
else this.ctx.logger.warn(message, { meta: { extras } });
|
|
7756
|
+
},
|
|
7757
|
+
info: (message, extras) => {
|
|
7758
|
+
this.ctx.logger.info(message, { meta: extras.meta });
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7502
7761
|
};
|
|
7503
7762
|
this.segmentStore = new SegmentStore(segmentStoreDeps);
|
|
7504
7763
|
const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(ingestOwner.ownerNodeId));
|