@camstack/system 1.2.21 → 1.2.23
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-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +17 -13
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1319 -1097
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1319 -1097
- package/dist/builtins/backup-orchestrator/schedule-store.d.ts +33 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.d.ts +16 -0
- package/dist/builtins/local-network/local-network.addon.js +30 -2
- package/dist/builtins/local-network/local-network.addon.mjs +30 -2
- package/dist/builtins/loki-logging/index.js +34 -12
- package/dist/builtins/loki-logging/index.mjs +34 -12
- package/dist/builtins/loki-logging/loki-logging.addon.d.ts +19 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +7 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +112 -6
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +112 -6
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +17 -1
- package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +5 -0
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-DmQ6thmF.mjs → dist-CIek6F9X.mjs} +536 -76
- package/dist/{dist-OQ1yU92D.js → dist-CS0pdC8l.js} +536 -76
- package/dist/index.js +58 -8
- package/dist/index.mjs +58 -8
- package/dist/kernel/capability-registry.d.ts +9 -0
- package/dist/kernel/transport/local-child-registry.d.ts +18 -0
- package/dist/kernel/transport/parent-unowned-call.d.ts +22 -0
- package/dist/logging/log-manager.d.ts +32 -1
- package/dist/logging/partitioned-log-buffer.d.ts +15 -3
- package/dist/{manifest-python-deps-B30qVAuW.mjs → manifest-python-deps-57PA4rHm.mjs} +15 -6
- package/dist/{manifest-python-deps-CnRW2Utv.js → manifest-python-deps-CqDPWqEE.js} +14 -5
- package/package.json +3 -2
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CS0pdC8l.js");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_chunk.__toESM(node_path);
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -14,6 +14,8 @@ var StorageOrchestratorService = class {
|
|
|
14
14
|
logger;
|
|
15
15
|
getProviders;
|
|
16
16
|
locationStore;
|
|
17
|
+
localNodeId;
|
|
18
|
+
getRemoteProvider;
|
|
17
19
|
locations = /* @__PURE__ */ new Map();
|
|
18
20
|
/**
|
|
19
21
|
* Declaration-driven cardinality source, injected by the addon after
|
|
@@ -31,10 +33,12 @@ var StorageOrchestratorService = class {
|
|
|
31
33
|
* early-boot path that predates the resolver wiring.
|
|
32
34
|
*/
|
|
33
35
|
nodeLocalResolver = null;
|
|
34
|
-
constructor(logger, getProviders, locationStore = null) {
|
|
36
|
+
constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
|
|
35
37
|
this.logger = logger;
|
|
36
38
|
this.getProviders = getProviders;
|
|
37
39
|
this.locationStore = locationStore;
|
|
40
|
+
this.localNodeId = localNodeId;
|
|
41
|
+
this.getRemoteProvider = getRemoteProvider;
|
|
38
42
|
}
|
|
39
43
|
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
40
44
|
hasStore() {
|
|
@@ -349,6 +353,12 @@ var StorageOrchestratorService = class {
|
|
|
349
353
|
* the right place to fix the config.
|
|
350
354
|
*/
|
|
351
355
|
async getProviderFor(location) {
|
|
356
|
+
const nodeId = location.nodeId;
|
|
357
|
+
if (nodeId !== void 0 && nodeId !== this.localNodeId) {
|
|
358
|
+
const remote = this.getRemoteProvider?.(nodeId);
|
|
359
|
+
if (remote) return remote;
|
|
360
|
+
throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
|
|
361
|
+
}
|
|
352
362
|
const providers = this.getProviders();
|
|
353
363
|
for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
|
|
354
364
|
throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
|
|
@@ -378,8 +388,8 @@ var StorageOrchestratorService = class {
|
|
|
378
388
|
let added = 0;
|
|
379
389
|
const declById = new Map(input.declarations.map((d) => [d.id, d]));
|
|
380
390
|
const rootFor = (d) => {
|
|
381
|
-
const
|
|
382
|
-
return node_path.join(
|
|
391
|
+
const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
|
|
392
|
+
return node_path.join(dedicated ?? input.basePath, d.id);
|
|
383
393
|
};
|
|
384
394
|
for (const d of input.declarations) {
|
|
385
395
|
if (this.hasAnyLocationOfType(d.id)) continue;
|
|
@@ -634,6 +644,53 @@ var StoragePressureManager = class {
|
|
|
634
644
|
} });
|
|
635
645
|
}
|
|
636
646
|
}
|
|
647
|
+
const caps = await this.deps.getLocationMaxUsedBytes?.() ?? /* @__PURE__ */ new Map();
|
|
648
|
+
for (const [locationId, maxBytes] of caps) {
|
|
649
|
+
if (relievedDevices.size > 0) {
|
|
650
|
+
const deviceKey = await this.deps.getLocationDeviceKey?.(locationId) ?? null;
|
|
651
|
+
if (deviceKey !== null && relievedDevices.has(deviceKey)) continue;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
await this.relieveToUsageCap(locationId, maxBytes);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
this.deps.logger.warn("storage pressure: usage-cap relief failed", { meta: {
|
|
657
|
+
locationId,
|
|
658
|
+
error: err instanceof Error ? err.message : String(err)
|
|
659
|
+
} });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
|
|
664
|
+
async relieveToUsageCap(locationId, maxBytes) {
|
|
665
|
+
const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
666
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
667
|
+
const providers = this.deps.getEvictableProviders();
|
|
668
|
+
const usages = await Promise.all(providers.map(async (p) => ({
|
|
669
|
+
p,
|
|
670
|
+
bytes: (await p.getEvictableUsage({ locationId })).bytes
|
|
671
|
+
})));
|
|
672
|
+
const totalUsed = usages.reduce((s, u) => s + u.bytes, 0);
|
|
673
|
+
const excess = totalUsed - maxBytes;
|
|
674
|
+
if (excess <= 0) return;
|
|
675
|
+
let reclaimed = 0;
|
|
676
|
+
for (const u of usages) {
|
|
677
|
+
if (u.bytes <= 0) continue;
|
|
678
|
+
const share = Math.max(1, Math.ceil(excess * u.bytes / totalUsed));
|
|
679
|
+
const res = await u.p.evict({
|
|
680
|
+
locationId,
|
|
681
|
+
targetBytes: share
|
|
682
|
+
});
|
|
683
|
+
reclaimed += res.reclaimedBytes;
|
|
684
|
+
}
|
|
685
|
+
this.deps.logger.info("storage pressure: usage-cap relief round", { meta: {
|
|
686
|
+
locationId,
|
|
687
|
+
round,
|
|
688
|
+
totalUsed,
|
|
689
|
+
maxBytes,
|
|
690
|
+
reclaimedBytes: reclaimed
|
|
691
|
+
} });
|
|
692
|
+
if (reclaimed <= 0) return;
|
|
693
|
+
}
|
|
637
694
|
}
|
|
638
695
|
/**
|
|
639
696
|
* Relieve one location until it recovers, nothing is evictable, or a round
|
|
@@ -772,13 +829,21 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
772
829
|
return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
|
|
773
830
|
};
|
|
774
831
|
const locationStore = this.resolveLocationStore();
|
|
775
|
-
const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore)
|
|
832
|
+
const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore, this.ctx.kernel.localNodeId ?? "hub", (nodeId) => {
|
|
833
|
+
const proxy = this.ctx.kernel.resolveNodeCapability?.("storage-provider", nodeId);
|
|
834
|
+
if (!proxy) return null;
|
|
835
|
+
return proxy;
|
|
836
|
+
});
|
|
776
837
|
this.service = service;
|
|
777
838
|
service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
|
|
778
839
|
await service.initialize();
|
|
779
840
|
const provider = {
|
|
780
841
|
listLocations: async ({ type }) => {
|
|
781
|
-
|
|
842
|
+
const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
|
|
843
|
+
return Promise.all(rows.map(async (loc) => ({
|
|
844
|
+
...loc,
|
|
845
|
+
capacity: await this.localCapacityOf(loc)
|
|
846
|
+
})));
|
|
782
847
|
},
|
|
783
848
|
getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
|
|
784
849
|
listLocationDeclarations: async () => service.listDeclarations(),
|
|
@@ -944,6 +1009,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
944
1009
|
getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
|
|
945
1010
|
getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
|
|
946
1011
|
getLocationThresholds: () => this.resolveLocationThresholds(),
|
|
1012
|
+
getLocationMaxUsedBytes: () => this.resolveLocationMaxUsedBytes(),
|
|
947
1013
|
getLocationDeviceKey: (id) => this.locationDeviceKey(id)
|
|
948
1014
|
});
|
|
949
1015
|
this.pressureTimer = setInterval(() => {
|
|
@@ -1053,6 +1119,44 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
1053
1119
|
}
|
|
1054
1120
|
return out;
|
|
1055
1121
|
}
|
|
1122
|
+
/** statfs capacity of a location's basePath, walking up to the nearest
|
|
1123
|
+
* existing ancestor. Null for remote-node locations or unstattable paths. */
|
|
1124
|
+
async localCapacityOf(loc) {
|
|
1125
|
+
if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
|
|
1126
|
+
const bp = loc.config["basePath"];
|
|
1127
|
+
if (typeof bp !== "string" || bp.length === 0) return null;
|
|
1128
|
+
try {
|
|
1129
|
+
let target = node_path.resolve(bp);
|
|
1130
|
+
for (;;) try {
|
|
1131
|
+
await node_fs_promises.access(target);
|
|
1132
|
+
break;
|
|
1133
|
+
} catch {
|
|
1134
|
+
const parent = node_path.dirname(target);
|
|
1135
|
+
if (!parent || parent === target) return null;
|
|
1136
|
+
target = parent;
|
|
1137
|
+
}
|
|
1138
|
+
const st = await node_fs_promises.statfs(target);
|
|
1139
|
+
return {
|
|
1140
|
+
totalBytes: st.blocks * st.bsize,
|
|
1141
|
+
availableBytes: st.bavail * st.bsize
|
|
1142
|
+
};
|
|
1143
|
+
} catch {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/** Per-location cap on EVICTABLE bytes (`config.maxUsedGb`): the operator's
|
|
1148
|
+
* "recordings may use at most N GB in total" — enforced by the pressure
|
|
1149
|
+
* manager alongside the free-percent guard. */
|
|
1150
|
+
async resolveLocationMaxUsedBytes() {
|
|
1151
|
+
const out = /* @__PURE__ */ new Map();
|
|
1152
|
+
const svc = this.service;
|
|
1153
|
+
if (!svc) return out;
|
|
1154
|
+
for (const loc of svc.listLocations()) {
|
|
1155
|
+
const raw = loc.config["maxUsedGb"];
|
|
1156
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, Math.floor(raw * 1024 ** 3));
|
|
1157
|
+
}
|
|
1158
|
+
return out;
|
|
1159
|
+
}
|
|
1056
1160
|
locationBasePath(locationId) {
|
|
1057
1161
|
const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
|
|
1058
1162
|
return typeof bp === "string" && bp.length > 0 ? bp : null;
|
|
@@ -1120,10 +1224,12 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
1120
1224
|
this.service.setRegistry(registry);
|
|
1121
1225
|
this.service.pruneUndeclaredSystemLocations();
|
|
1122
1226
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
1227
|
+
const backupRoot = process.env.CAMSTACK_BACKUP_ROOT?.trim();
|
|
1123
1228
|
this.service.seedDefaults({
|
|
1124
1229
|
providerId: "filesystem-storage",
|
|
1125
1230
|
basePath: this.ctx.nodeDataDir,
|
|
1126
1231
|
mediaBasePath: mediaRoot && mediaRoot.length > 0 ? mediaRoot : void 0,
|
|
1232
|
+
backupBasePath: backupRoot && backupRoot.length > 0 ? backupRoot : void 0,
|
|
1127
1233
|
declarations: registry.list()
|
|
1128
1234
|
});
|
|
1129
1235
|
await this.service.backfillNodeIds(HUB_NODE_ID);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-
|
|
1
|
+
import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-CIek6F9X.mjs";
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import { buildStorageLocationRegistry } from "@camstack/system";
|
|
@@ -7,6 +7,8 @@ var StorageOrchestratorService = class {
|
|
|
7
7
|
logger;
|
|
8
8
|
getProviders;
|
|
9
9
|
locationStore;
|
|
10
|
+
localNodeId;
|
|
11
|
+
getRemoteProvider;
|
|
10
12
|
locations = /* @__PURE__ */ new Map();
|
|
11
13
|
/**
|
|
12
14
|
* Declaration-driven cardinality source, injected by the addon after
|
|
@@ -24,10 +26,12 @@ var StorageOrchestratorService = class {
|
|
|
24
26
|
* early-boot path that predates the resolver wiring.
|
|
25
27
|
*/
|
|
26
28
|
nodeLocalResolver = null;
|
|
27
|
-
constructor(logger, getProviders, locationStore = null) {
|
|
29
|
+
constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
|
|
28
30
|
this.logger = logger;
|
|
29
31
|
this.getProviders = getProviders;
|
|
30
32
|
this.locationStore = locationStore;
|
|
33
|
+
this.localNodeId = localNodeId;
|
|
34
|
+
this.getRemoteProvider = getRemoteProvider;
|
|
31
35
|
}
|
|
32
36
|
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
33
37
|
hasStore() {
|
|
@@ -342,6 +346,12 @@ var StorageOrchestratorService = class {
|
|
|
342
346
|
* the right place to fix the config.
|
|
343
347
|
*/
|
|
344
348
|
async getProviderFor(location) {
|
|
349
|
+
const nodeId = location.nodeId;
|
|
350
|
+
if (nodeId !== void 0 && nodeId !== this.localNodeId) {
|
|
351
|
+
const remote = this.getRemoteProvider?.(nodeId);
|
|
352
|
+
if (remote) return remote;
|
|
353
|
+
throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
|
|
354
|
+
}
|
|
345
355
|
const providers = this.getProviders();
|
|
346
356
|
for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
|
|
347
357
|
throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
|
|
@@ -371,8 +381,8 @@ var StorageOrchestratorService = class {
|
|
|
371
381
|
let added = 0;
|
|
372
382
|
const declById = new Map(input.declarations.map((d) => [d.id, d]));
|
|
373
383
|
const rootFor = (d) => {
|
|
374
|
-
const
|
|
375
|
-
return path$1.join(
|
|
384
|
+
const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
|
|
385
|
+
return path$1.join(dedicated ?? input.basePath, d.id);
|
|
376
386
|
};
|
|
377
387
|
for (const d of input.declarations) {
|
|
378
388
|
if (this.hasAnyLocationOfType(d.id)) continue;
|
|
@@ -627,6 +637,53 @@ var StoragePressureManager = class {
|
|
|
627
637
|
} });
|
|
628
638
|
}
|
|
629
639
|
}
|
|
640
|
+
const caps = await this.deps.getLocationMaxUsedBytes?.() ?? /* @__PURE__ */ new Map();
|
|
641
|
+
for (const [locationId, maxBytes] of caps) {
|
|
642
|
+
if (relievedDevices.size > 0) {
|
|
643
|
+
const deviceKey = await this.deps.getLocationDeviceKey?.(locationId) ?? null;
|
|
644
|
+
if (deviceKey !== null && relievedDevices.has(deviceKey)) continue;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
await this.relieveToUsageCap(locationId, maxBytes);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
this.deps.logger.warn("storage pressure: usage-cap relief failed", { meta: {
|
|
650
|
+
locationId,
|
|
651
|
+
error: err instanceof Error ? err.message : String(err)
|
|
652
|
+
} });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
|
|
657
|
+
async relieveToUsageCap(locationId, maxBytes) {
|
|
658
|
+
const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
659
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
660
|
+
const providers = this.deps.getEvictableProviders();
|
|
661
|
+
const usages = await Promise.all(providers.map(async (p) => ({
|
|
662
|
+
p,
|
|
663
|
+
bytes: (await p.getEvictableUsage({ locationId })).bytes
|
|
664
|
+
})));
|
|
665
|
+
const totalUsed = usages.reduce((s, u) => s + u.bytes, 0);
|
|
666
|
+
const excess = totalUsed - maxBytes;
|
|
667
|
+
if (excess <= 0) return;
|
|
668
|
+
let reclaimed = 0;
|
|
669
|
+
for (const u of usages) {
|
|
670
|
+
if (u.bytes <= 0) continue;
|
|
671
|
+
const share = Math.max(1, Math.ceil(excess * u.bytes / totalUsed));
|
|
672
|
+
const res = await u.p.evict({
|
|
673
|
+
locationId,
|
|
674
|
+
targetBytes: share
|
|
675
|
+
});
|
|
676
|
+
reclaimed += res.reclaimedBytes;
|
|
677
|
+
}
|
|
678
|
+
this.deps.logger.info("storage pressure: usage-cap relief round", { meta: {
|
|
679
|
+
locationId,
|
|
680
|
+
round,
|
|
681
|
+
totalUsed,
|
|
682
|
+
maxBytes,
|
|
683
|
+
reclaimedBytes: reclaimed
|
|
684
|
+
} });
|
|
685
|
+
if (reclaimed <= 0) return;
|
|
686
|
+
}
|
|
630
687
|
}
|
|
631
688
|
/**
|
|
632
689
|
* Relieve one location until it recovers, nothing is evictable, or a round
|
|
@@ -765,13 +822,21 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
765
822
|
return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
|
|
766
823
|
};
|
|
767
824
|
const locationStore = this.resolveLocationStore();
|
|
768
|
-
const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore)
|
|
825
|
+
const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore, this.ctx.kernel.localNodeId ?? "hub", (nodeId) => {
|
|
826
|
+
const proxy = this.ctx.kernel.resolveNodeCapability?.("storage-provider", nodeId);
|
|
827
|
+
if (!proxy) return null;
|
|
828
|
+
return proxy;
|
|
829
|
+
});
|
|
769
830
|
this.service = service;
|
|
770
831
|
service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
|
|
771
832
|
await service.initialize();
|
|
772
833
|
const provider = {
|
|
773
834
|
listLocations: async ({ type }) => {
|
|
774
|
-
|
|
835
|
+
const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
|
|
836
|
+
return Promise.all(rows.map(async (loc) => ({
|
|
837
|
+
...loc,
|
|
838
|
+
capacity: await this.localCapacityOf(loc)
|
|
839
|
+
})));
|
|
775
840
|
},
|
|
776
841
|
getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
|
|
777
842
|
listLocationDeclarations: async () => service.listDeclarations(),
|
|
@@ -937,6 +1002,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
937
1002
|
getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
|
|
938
1003
|
getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
|
|
939
1004
|
getLocationThresholds: () => this.resolveLocationThresholds(),
|
|
1005
|
+
getLocationMaxUsedBytes: () => this.resolveLocationMaxUsedBytes(),
|
|
940
1006
|
getLocationDeviceKey: (id) => this.locationDeviceKey(id)
|
|
941
1007
|
});
|
|
942
1008
|
this.pressureTimer = setInterval(() => {
|
|
@@ -1046,6 +1112,44 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1046
1112
|
}
|
|
1047
1113
|
return out;
|
|
1048
1114
|
}
|
|
1115
|
+
/** statfs capacity of a location's basePath, walking up to the nearest
|
|
1116
|
+
* existing ancestor. Null for remote-node locations or unstattable paths. */
|
|
1117
|
+
async localCapacityOf(loc) {
|
|
1118
|
+
if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
|
|
1119
|
+
const bp = loc.config["basePath"];
|
|
1120
|
+
if (typeof bp !== "string" || bp.length === 0) return null;
|
|
1121
|
+
try {
|
|
1122
|
+
let target = path$1.resolve(bp);
|
|
1123
|
+
for (;;) try {
|
|
1124
|
+
await fs.access(target);
|
|
1125
|
+
break;
|
|
1126
|
+
} catch {
|
|
1127
|
+
const parent = path$1.dirname(target);
|
|
1128
|
+
if (!parent || parent === target) return null;
|
|
1129
|
+
target = parent;
|
|
1130
|
+
}
|
|
1131
|
+
const st = await fs.statfs(target);
|
|
1132
|
+
return {
|
|
1133
|
+
totalBytes: st.blocks * st.bsize,
|
|
1134
|
+
availableBytes: st.bavail * st.bsize
|
|
1135
|
+
};
|
|
1136
|
+
} catch {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/** Per-location cap on EVICTABLE bytes (`config.maxUsedGb`): the operator's
|
|
1141
|
+
* "recordings may use at most N GB in total" — enforced by the pressure
|
|
1142
|
+
* manager alongside the free-percent guard. */
|
|
1143
|
+
async resolveLocationMaxUsedBytes() {
|
|
1144
|
+
const out = /* @__PURE__ */ new Map();
|
|
1145
|
+
const svc = this.service;
|
|
1146
|
+
if (!svc) return out;
|
|
1147
|
+
for (const loc of svc.listLocations()) {
|
|
1148
|
+
const raw = loc.config["maxUsedGb"];
|
|
1149
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, Math.floor(raw * 1024 ** 3));
|
|
1150
|
+
}
|
|
1151
|
+
return out;
|
|
1152
|
+
}
|
|
1049
1153
|
locationBasePath(locationId) {
|
|
1050
1154
|
const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
|
|
1051
1155
|
return typeof bp === "string" && bp.length > 0 ? bp : null;
|
|
@@ -1113,10 +1217,12 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1113
1217
|
this.service.setRegistry(registry);
|
|
1114
1218
|
this.service.pruneUndeclaredSystemLocations();
|
|
1115
1219
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
1220
|
+
const backupRoot = process.env.CAMSTACK_BACKUP_ROOT?.trim();
|
|
1116
1221
|
this.service.seedDefaults({
|
|
1117
1222
|
providerId: "filesystem-storage",
|
|
1118
1223
|
basePath: this.ctx.nodeDataDir,
|
|
1119
1224
|
mediaBasePath: mediaRoot && mediaRoot.length > 0 ? mediaRoot : void 0,
|
|
1225
|
+
backupBasePath: backupRoot && backupRoot.length > 0 ? backupRoot : void 0,
|
|
1120
1226
|
declarations: registry.list()
|
|
1121
1227
|
});
|
|
1122
1228
|
await this.service.backfillNodeIds(HUB_NODE_ID);
|
|
@@ -54,6 +54,11 @@ export declare class StorageOrchestratorService {
|
|
|
54
54
|
* the provider appears. See {@link attachStore}.
|
|
55
55
|
*/
|
|
56
56
|
private locationStore;
|
|
57
|
+
/** This node's id — locations pinned to OTHER nodes dispatch remotely. */
|
|
58
|
+
private readonly localNodeId;
|
|
59
|
+
/** Cross-node provider resolver (hub wiring: a node-routed capability
|
|
60
|
+
* proxy over `storage-provider`). Absent → remote locations error. */
|
|
61
|
+
private readonly getRemoteProvider?;
|
|
57
62
|
private readonly locations;
|
|
58
63
|
/**
|
|
59
64
|
* Declaration-driven cardinality source, injected by the addon after
|
|
@@ -85,7 +90,12 @@ export declare class StorageOrchestratorService {
|
|
|
85
90
|
* service starts store-less and the addon calls {@link attachStore} once
|
|
86
91
|
* the provider appears. See {@link attachStore}.
|
|
87
92
|
*/
|
|
88
|
-
locationStore?: ILocationStore | null
|
|
93
|
+
locationStore?: ILocationStore | null,
|
|
94
|
+
/** This node's id — locations pinned to OTHER nodes dispatch remotely. */
|
|
95
|
+
localNodeId?: string,
|
|
96
|
+
/** Cross-node provider resolver (hub wiring: a node-routed capability
|
|
97
|
+
* proxy over `storage-provider`). Absent → remote locations error. */
|
|
98
|
+
getRemoteProvider?: ((nodeId: string) => IStorageProviderImpl | null) | undefined);
|
|
89
99
|
/** True once a persistence store is wired (constructor or {@link attachStore}). */
|
|
90
100
|
hasStore(): boolean;
|
|
91
101
|
/**
|
|
@@ -235,6 +245,12 @@ export declare class StorageOrchestratorService {
|
|
|
235
245
|
* `basePath` like everything else.
|
|
236
246
|
*/
|
|
237
247
|
readonly mediaBasePath?: string;
|
|
248
|
+
/**
|
|
249
|
+
* Root for declarations marked `defaultRoot: 'backup'` (the dedicated
|
|
250
|
+
* backup volume, `CAMSTACK_BACKUP_ROOT`, default `/backups`). Absent →
|
|
251
|
+
* backup declarations fall back to `basePath`.
|
|
252
|
+
*/
|
|
253
|
+
readonly backupBasePath?: string;
|
|
238
254
|
readonly declarations: readonly StorageLocationDeclaration[];
|
|
239
255
|
}): void;
|
|
240
256
|
/** Internal: check whether any location of the given type exists. */
|
|
@@ -24,6 +24,9 @@ export interface PressureManagerDeps {
|
|
|
24
24
|
readonly getEvictableProviders: () => readonly IStorageEvictableProvider[];
|
|
25
25
|
/** locationId → minFreePercent threshold; entries with `<= 0` have the guard off. */
|
|
26
26
|
readonly getLocationThresholds: () => Promise<ReadonlyMap<string, number>>;
|
|
27
|
+
/** locationId → hard cap on EVICTABLE bytes (`config.maxUsedGb`, operator's
|
|
28
|
+
* "recordings may use at most N GB total"). Optional — absent = no caps. */
|
|
29
|
+
readonly getLocationMaxUsedBytes?: () => Promise<ReadonlyMap<string, number>>;
|
|
27
30
|
/** Max relief rounds per location per sweep (backstop, default 8). */
|
|
28
31
|
readonly maxRounds?: number;
|
|
29
32
|
/** Extra headroom % freed beyond the threshold to avoid re-breaching immediately (default 1). */
|
|
@@ -53,6 +56,8 @@ export declare class StoragePressureManager {
|
|
|
53
56
|
constructor(deps: PressureManagerDeps);
|
|
54
57
|
/** Relieve every managed location currently under its threshold. */
|
|
55
58
|
sweep(): Promise<void>;
|
|
59
|
+
/** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
|
|
60
|
+
private relieveToUsageCap;
|
|
56
61
|
/**
|
|
57
62
|
* Relieve one location until it recovers, nothing is evictable, or a round
|
|
58
63
|
* makes no progress. Returns `true` when at least one relief round actually
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CS0pdC8l.js");
|
|
7
7
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
8
8
|
/**
|
|
9
9
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-
|
|
1
|
+
import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-CIek6F9X.mjs";
|
|
2
2
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
3
3
|
/**
|
|
4
4
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CS0pdC8l.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { at as BaseAddon, z as logDestinationCapability } from "../../dist-
|
|
1
|
+
import { at as BaseAddon, z as logDestinationCapability } from "../../dist-CIek6F9X.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import path from "node:path";
|