@camstack/system 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/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.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.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/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.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.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- 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/index.d.ts +2 -0
- package/dist/builtins/storage-orchestrator/index.js +1 -0
- package/dist/builtins/storage-orchestrator/index.mjs +2 -2
- package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +73 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +2 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +468 -8
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +468 -9
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +7 -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-eBPa-ZBZ.mjs → dist-CIIFhXqm.mjs} +367 -46
- package/dist/{dist-DWZce90k.js → dist-cINpngi1.js} +378 -45
- package/dist/index.js +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +4 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { It as parseJsonObject, ct as storageCapability, g as StorageMigrationJobSchema, gt as BaseAddon, h as StorageLocationTypeSchema, lt as storageMigrationCapability, ot as settingsStoreCapability } from "../../dist-CIIFhXqm.mjs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path$1 from "node:path";
|
|
4
5
|
import { buildStorageLocationRegistry } from "@camstack/system";
|
|
@@ -234,6 +235,52 @@ var StorageOrchestratorService = class {
|
|
|
234
235
|
return null;
|
|
235
236
|
}
|
|
236
237
|
/**
|
|
238
|
+
* Repoint several logical defaults as one in-memory commit. The migration
|
|
239
|
+
* coordinator persists its `repointing` phase before calling this method, so
|
|
240
|
+
* a process crash during the backing-store writes is recovered by replaying
|
|
241
|
+
* the same idempotent target set before anyone resumes writers.
|
|
242
|
+
*/
|
|
243
|
+
async setDefaultLocations(targetByType) {
|
|
244
|
+
if (targetByType.size === 0) return;
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
const nextById = new Map(this.locations);
|
|
247
|
+
const touched = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const [type, targetId] of targetByType) {
|
|
249
|
+
const target = nextById.get(targetId);
|
|
250
|
+
if (!target) throw new Error(`Storage location "${targetId}" not found`);
|
|
251
|
+
if (target.type !== type) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${type}"`);
|
|
252
|
+
for (const [id, location] of nextById) {
|
|
253
|
+
if (location.type !== type) continue;
|
|
254
|
+
const isDefault = id === targetId;
|
|
255
|
+
if (location.isDefault === isDefault) continue;
|
|
256
|
+
const updated = {
|
|
257
|
+
...location,
|
|
258
|
+
isDefault,
|
|
259
|
+
updatedAt: now
|
|
260
|
+
};
|
|
261
|
+
nextById.set(id, updated);
|
|
262
|
+
touched.set(id, updated);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (this.locationStore) {
|
|
266
|
+
const previous = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const id of touched.keys()) {
|
|
268
|
+
const location = this.locations.get(id);
|
|
269
|
+
if (location) previous.set(id, location);
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
for (const location of touched.values()) await this.locationStore.upsert(location);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
for (const location of previous.values()) try {
|
|
275
|
+
await this.locationStore.upsert(location);
|
|
276
|
+
} catch {}
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this.locations.clear();
|
|
281
|
+
for (const [id, location] of nextById) this.locations.set(id, location);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
237
284
|
* Insert or update a location. If `isDefault: true`, atomically
|
|
238
285
|
* demotes any other default for the same type — operators always see
|
|
239
286
|
* exactly one default per type.
|
|
@@ -928,6 +975,355 @@ var StoragePressureManager = class {
|
|
|
928
975
|
}
|
|
929
976
|
};
|
|
930
977
|
//#endregion
|
|
978
|
+
//#region src/builtins/storage-orchestrator/storage-migration-coordinator.ts
|
|
979
|
+
var STORAGE_CLASSES = [
|
|
980
|
+
"recordings",
|
|
981
|
+
"recordingsLow",
|
|
982
|
+
"eventMedia"
|
|
983
|
+
];
|
|
984
|
+
var PARTICIPANTS = [
|
|
985
|
+
"pipeline",
|
|
986
|
+
"recorder",
|
|
987
|
+
"analytics"
|
|
988
|
+
];
|
|
989
|
+
var REVERSE_PARTICIPANTS = [
|
|
990
|
+
"analytics",
|
|
991
|
+
"recorder",
|
|
992
|
+
"pipeline"
|
|
993
|
+
];
|
|
994
|
+
/**
|
|
995
|
+
* Durable orchestration layer over the independent recorder and event-media
|
|
996
|
+
* movers. It owns the only public migration state machine; participants expose
|
|
997
|
+
* intentionally narrow lease/move primitives and never alter camera settings.
|
|
998
|
+
*/
|
|
999
|
+
var StorageMigrationCoordinator = class {
|
|
1000
|
+
deps;
|
|
1001
|
+
active = null;
|
|
1002
|
+
runPromise = null;
|
|
1003
|
+
startReserved = false;
|
|
1004
|
+
constructor(deps) {
|
|
1005
|
+
this.deps = deps;
|
|
1006
|
+
}
|
|
1007
|
+
async plan(input) {
|
|
1008
|
+
const moves = [];
|
|
1009
|
+
for (const storageClass of STORAGE_CLASSES) {
|
|
1010
|
+
const targetId = input.destinations[storageClass];
|
|
1011
|
+
if (targetId === void 0) continue;
|
|
1012
|
+
const source = this.deps.locations.getDefaultLocation(storageClass);
|
|
1013
|
+
if (!source) throw new Error(`No default storage location for "${storageClass}"`);
|
|
1014
|
+
const target = this.deps.locations.getLocationById(targetId);
|
|
1015
|
+
if (!target) throw new Error(`Storage location "${targetId}" not found`);
|
|
1016
|
+
if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
|
|
1017
|
+
if (source.id === target.id) throw new Error(`Storage location "${targetId}" is already the "${storageClass}" default`);
|
|
1018
|
+
moves.push({
|
|
1019
|
+
storageClass,
|
|
1020
|
+
fromLocationId: source.id,
|
|
1021
|
+
toLocationId: target.id,
|
|
1022
|
+
moverJobId: null,
|
|
1023
|
+
state: null,
|
|
1024
|
+
error: null
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (moves.length === 0) throw new Error("select at least one storage class");
|
|
1028
|
+
return {
|
|
1029
|
+
destinations: input.destinations,
|
|
1030
|
+
moves: moves.map(({ storageClass, fromLocationId, toLocationId }) => ({
|
|
1031
|
+
storageClass,
|
|
1032
|
+
fromLocationId,
|
|
1033
|
+
toLocationId
|
|
1034
|
+
}))
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
async start(input) {
|
|
1038
|
+
if (this.startReserved) throw new Error("storage migration is already active");
|
|
1039
|
+
this.startReserved = true;
|
|
1040
|
+
try {
|
|
1041
|
+
const existing = await this.status();
|
|
1042
|
+
if (existing && isTerminal(existing) && existing.pausedParticipants.length > 0) {
|
|
1043
|
+
await this.releaseAfterTerminal(existing);
|
|
1044
|
+
await this.persist(existing);
|
|
1045
|
+
if (existing.pausedParticipants.length > 0) throw new Error(`storage migration ${existing.jobId} still holds maintenance leases`);
|
|
1046
|
+
}
|
|
1047
|
+
if (existing && !isTerminal(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
|
|
1048
|
+
const plan = await this.plan(input);
|
|
1049
|
+
const now = this.deps.now();
|
|
1050
|
+
const job = {
|
|
1051
|
+
jobId: this.deps.newId(),
|
|
1052
|
+
phase: "planning",
|
|
1053
|
+
destinations: input.destinations,
|
|
1054
|
+
throttleMbps: input.throttleMbps ?? 40,
|
|
1055
|
+
moves: plan.moves.map((move) => ({
|
|
1056
|
+
...move,
|
|
1057
|
+
moverJobId: null,
|
|
1058
|
+
state: null,
|
|
1059
|
+
error: null
|
|
1060
|
+
})),
|
|
1061
|
+
pauseLeaseId: null,
|
|
1062
|
+
pausedParticipants: [],
|
|
1063
|
+
repointed: false,
|
|
1064
|
+
cancelRequested: false,
|
|
1065
|
+
startedAt: now,
|
|
1066
|
+
updatedAt: now,
|
|
1067
|
+
finishedAt: null,
|
|
1068
|
+
error: null
|
|
1069
|
+
};
|
|
1070
|
+
this.active = job;
|
|
1071
|
+
await this.persist(job);
|
|
1072
|
+
this.runPromise = this.run(job);
|
|
1073
|
+
this.runPromise;
|
|
1074
|
+
return job.jobId;
|
|
1075
|
+
} finally {
|
|
1076
|
+
this.startReserved = false;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async status(jobId) {
|
|
1080
|
+
const job = this.active ?? await this.deps.state.get();
|
|
1081
|
+
if (jobId !== void 0 && job?.jobId !== jobId) return null;
|
|
1082
|
+
return job;
|
|
1083
|
+
}
|
|
1084
|
+
async cancel(jobId) {
|
|
1085
|
+
const job = await this.status(jobId);
|
|
1086
|
+
if (!job || isTerminal(job) || job.repointed) return false;
|
|
1087
|
+
job.cancelRequested = true;
|
|
1088
|
+
await this.persist(job);
|
|
1089
|
+
await Promise.all(job.moves.filter((move) => move.moverJobId !== null).map((move) => this.cancelMove(move)));
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
/** Boot recovery resumes a durable unfinished state. A missing in-memory
|
|
1093
|
+
* child mover is recreated from the same copy-if-absent input. */
|
|
1094
|
+
async recover() {
|
|
1095
|
+
const job = await this.deps.state.get();
|
|
1096
|
+
if (!job) return;
|
|
1097
|
+
if (job.cancelRequested) {
|
|
1098
|
+
await Promise.all(job.moves.filter((move) => move.moverJobId !== null).map((move) => this.cancelMove(move)));
|
|
1099
|
+
await this.waitForMoves(job);
|
|
1100
|
+
await this.releaseAfterTerminal(job);
|
|
1101
|
+
job.phase = "cancelled";
|
|
1102
|
+
job.finishedAt ??= this.deps.now();
|
|
1103
|
+
await this.persist(job);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (isTerminal(job)) {
|
|
1107
|
+
if (job.pausedParticipants.length > 0) {
|
|
1108
|
+
await this.releaseAfterTerminal(job);
|
|
1109
|
+
await this.persist(job);
|
|
1110
|
+
}
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
this.active = job;
|
|
1114
|
+
job.pauseLeaseId ??= this.deps.newId();
|
|
1115
|
+
if (job.repointed && job.phase === "resuming") job.phase = "refreshing";
|
|
1116
|
+
await this.persist(job);
|
|
1117
|
+
try {
|
|
1118
|
+
await this.pauseParticipants(job, true);
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
job.error = err instanceof Error ? err.message : String(err);
|
|
1121
|
+
await this.persist(job);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (this.runPromise === null) {
|
|
1125
|
+
this.runPromise = this.run(job);
|
|
1126
|
+
this.runPromise;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
async run(job) {
|
|
1130
|
+
try {
|
|
1131
|
+
if (job.phase === "planning") await this.setPhase(job, "pausing");
|
|
1132
|
+
if (job.phase === "pausing") {
|
|
1133
|
+
job.pauseLeaseId ??= job.jobId;
|
|
1134
|
+
await this.pauseParticipants(job);
|
|
1135
|
+
if (job.cancelRequested) return this.finishCancelled(job);
|
|
1136
|
+
await this.setPhase(job, "moving");
|
|
1137
|
+
}
|
|
1138
|
+
if (job.phase === "moving") {
|
|
1139
|
+
await this.startOrResumeMoves(job);
|
|
1140
|
+
await this.waitForMoves(job);
|
|
1141
|
+
if (job.cancelRequested) return this.finishCancelled(job);
|
|
1142
|
+
await this.setPhase(job, "verifying");
|
|
1143
|
+
}
|
|
1144
|
+
if (job.phase === "verifying") {
|
|
1145
|
+
await this.verifyMoves(job);
|
|
1146
|
+
if (job.cancelRequested) return this.finishCancelled(job);
|
|
1147
|
+
await this.setPhase(job, "repointing");
|
|
1148
|
+
}
|
|
1149
|
+
if (job.phase === "repointing") {
|
|
1150
|
+
const targets = new Map(job.moves.map((move) => [move.storageClass, move.toLocationId]));
|
|
1151
|
+
await this.deps.locations.setDefaultLocations(targets);
|
|
1152
|
+
job.repointed = true;
|
|
1153
|
+
await this.setPhase(job, "refreshing");
|
|
1154
|
+
}
|
|
1155
|
+
if (job.phase === "refreshing") {
|
|
1156
|
+
const leaseId = requireLease(job);
|
|
1157
|
+
if (job.moves.some((move) => move.storageClass !== "eventMedia")) await this.deps.participants.recorder.refresh(leaseId);
|
|
1158
|
+
if (job.moves.some((move) => move.storageClass === "eventMedia")) await this.deps.participants.analytics.refresh(leaseId);
|
|
1159
|
+
await this.setPhase(job, "resuming");
|
|
1160
|
+
}
|
|
1161
|
+
if (job.phase === "resuming") {
|
|
1162
|
+
await this.resumeParticipants(job);
|
|
1163
|
+
await this.setPhase(job, "done");
|
|
1164
|
+
}
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
job.error = err instanceof Error ? err.message : String(err);
|
|
1167
|
+
if (job.repointed) {
|
|
1168
|
+
job.phase = "refreshing";
|
|
1169
|
+
job.finishedAt = null;
|
|
1170
|
+
await this.persist(job);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
job.phase = "failed";
|
|
1174
|
+
job.finishedAt = this.deps.now();
|
|
1175
|
+
await this.releaseAfterTerminal(job);
|
|
1176
|
+
await this.persist(job);
|
|
1177
|
+
} finally {
|
|
1178
|
+
if (isTerminal(job)) {
|
|
1179
|
+
await this.releaseAfterTerminal(job);
|
|
1180
|
+
this.active = job;
|
|
1181
|
+
}
|
|
1182
|
+
this.runPromise = null;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async pauseParticipants(job, reacquire = false) {
|
|
1186
|
+
const leaseId = requireLease(job);
|
|
1187
|
+
for (const name of PARTICIPANTS) {
|
|
1188
|
+
if (job.cancelRequested) return;
|
|
1189
|
+
if (!reacquire && job.pausedParticipants.includes(name)) continue;
|
|
1190
|
+
await this.participant(name).pause(leaseId);
|
|
1191
|
+
if (!job.pausedParticipants.includes(name)) job.pausedParticipants.push(name);
|
|
1192
|
+
await this.persist(job);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
async resumeParticipants(job) {
|
|
1196
|
+
const leaseId = requireLease(job);
|
|
1197
|
+
for (const name of REVERSE_PARTICIPANTS) {
|
|
1198
|
+
if (!job.pausedParticipants.includes(name)) continue;
|
|
1199
|
+
await this.participant(name).resume(leaseId);
|
|
1200
|
+
job.pausedParticipants = job.pausedParticipants.filter((participant) => participant !== name);
|
|
1201
|
+
await this.persist(job);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async releaseAfterTerminal(job) {
|
|
1205
|
+
if (job.pauseLeaseId === null) return;
|
|
1206
|
+
for (const name of REVERSE_PARTICIPANTS) {
|
|
1207
|
+
if (!job.pausedParticipants.includes(name)) continue;
|
|
1208
|
+
try {
|
|
1209
|
+
await this.participant(name).resume(job.pauseLeaseId);
|
|
1210
|
+
job.pausedParticipants = job.pausedParticipants.filter((participant) => participant !== name);
|
|
1211
|
+
await this.persist(job);
|
|
1212
|
+
} catch {}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async startOrResumeMoves(job) {
|
|
1216
|
+
if (job.cancelRequested) return;
|
|
1217
|
+
const mediaMove = job.moves.find((move) => move.storageClass === "eventMedia");
|
|
1218
|
+
if (mediaMove?.moverJobId === null) {
|
|
1219
|
+
mediaMove.moverJobId = await this.startMove(job, mediaMove);
|
|
1220
|
+
await this.persist(job);
|
|
1221
|
+
}
|
|
1222
|
+
const footageMoves = job.moves.filter((move) => move.storageClass !== "eventMedia");
|
|
1223
|
+
for (let index = 0; index < footageMoves.length; index++) {
|
|
1224
|
+
const move = footageMoves[index];
|
|
1225
|
+
if (move.moverJobId !== null) continue;
|
|
1226
|
+
if (!footageMoves.slice(0, index).every((predecessor) => predecessor.state === "done")) return;
|
|
1227
|
+
move.moverJobId = await this.startMove(job, move);
|
|
1228
|
+
await this.persist(job);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async waitForMoves(job) {
|
|
1233
|
+
const sleep = this.deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1234
|
+
const pollMs = this.deps.pollMs ?? 250;
|
|
1235
|
+
for (;;) {
|
|
1236
|
+
if (!job.cancelRequested) await this.startOrResumeMoves(job);
|
|
1237
|
+
let complete = true;
|
|
1238
|
+
for (const [index, move] of job.moves.entries()) {
|
|
1239
|
+
if (job.cancelRequested && move.moverJobId === null) continue;
|
|
1240
|
+
if (move.storageClass !== "eventMedia" && job.moves.slice(0, index).filter((predecessor) => predecessor.storageClass !== "eventMedia").some((predecessor) => predecessor.state !== "done")) {
|
|
1241
|
+
complete = false;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (move.moverJobId === null) {
|
|
1245
|
+
if (!job.cancelRequested) complete = false;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const status = await this.moveStatus(move);
|
|
1249
|
+
if (status === null) {
|
|
1250
|
+
if (job.cancelRequested) {
|
|
1251
|
+
move.state = "cancelled";
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
move.moverJobId = await this.startMove(job, move);
|
|
1255
|
+
move.state = "running";
|
|
1256
|
+
await this.persist(job);
|
|
1257
|
+
complete = false;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
move.state = status.state;
|
|
1261
|
+
move.error = status.error;
|
|
1262
|
+
if (status.state === "failed") throw new Error(move.error ?? `${move.storageClass} move failed`);
|
|
1263
|
+
if (status.state === "cancelled") {
|
|
1264
|
+
job.cancelRequested = true;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (status.state !== "done") complete = false;
|
|
1268
|
+
}
|
|
1269
|
+
await this.persist(job);
|
|
1270
|
+
if (complete) return;
|
|
1271
|
+
await sleep(pollMs);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async verifyMoves(job) {
|
|
1275
|
+
for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
|
|
1276
|
+
}
|
|
1277
|
+
async startMove(job, move) {
|
|
1278
|
+
if (move.storageClass === "eventMedia") return (await this.deps.participants.analytics.startMove({
|
|
1279
|
+
toLocationId: move.toLocationId,
|
|
1280
|
+
throttleMbps: job.throttleMbps,
|
|
1281
|
+
leaseId: requireLease(job)
|
|
1282
|
+
})).jobId;
|
|
1283
|
+
return (await this.deps.participants.recorder.startMove({
|
|
1284
|
+
fromLocationId: move.fromLocationId,
|
|
1285
|
+
toLocationId: move.toLocationId,
|
|
1286
|
+
footageClass: move.storageClass,
|
|
1287
|
+
throttleMbps: job.throttleMbps,
|
|
1288
|
+
leaseId: requireLease(job)
|
|
1289
|
+
})).jobId;
|
|
1290
|
+
}
|
|
1291
|
+
async moveStatus(move) {
|
|
1292
|
+
if (move.moverJobId === null) return null;
|
|
1293
|
+
return move.storageClass === "eventMedia" ? this.deps.participants.analytics.getMove(move.moverJobId) : this.deps.participants.recorder.getMove(move.moverJobId);
|
|
1294
|
+
}
|
|
1295
|
+
async cancelMove(move) {
|
|
1296
|
+
if (move.moverJobId === null) return;
|
|
1297
|
+
if (move.storageClass === "eventMedia") await this.deps.participants.analytics.cancelMove(move.moverJobId);
|
|
1298
|
+
else await this.deps.participants.recorder.cancelMove(move.moverJobId);
|
|
1299
|
+
}
|
|
1300
|
+
participant(name) {
|
|
1301
|
+
return this.deps.participants[name];
|
|
1302
|
+
}
|
|
1303
|
+
async setPhase(job, phase) {
|
|
1304
|
+
job.phase = phase;
|
|
1305
|
+
if (isTerminal(job)) job.finishedAt = this.deps.now();
|
|
1306
|
+
await this.persist(job);
|
|
1307
|
+
}
|
|
1308
|
+
async finishCancelled(job) {
|
|
1309
|
+
job.phase = "cancelled";
|
|
1310
|
+
job.finishedAt = this.deps.now();
|
|
1311
|
+
await this.releaseAfterTerminal(job);
|
|
1312
|
+
await this.persist(job);
|
|
1313
|
+
}
|
|
1314
|
+
async persist(job) {
|
|
1315
|
+
job.updatedAt = this.deps.now();
|
|
1316
|
+
await this.deps.state.set(job);
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
function requireLease(job) {
|
|
1320
|
+
if (job.pauseLeaseId === null) throw new Error("storage migration has no maintenance lease");
|
|
1321
|
+
return job.pauseLeaseId;
|
|
1322
|
+
}
|
|
1323
|
+
function isTerminal(job) {
|
|
1324
|
+
return job.phase === "done" || job.phase === "failed" || job.phase === "cancelled";
|
|
1325
|
+
}
|
|
1326
|
+
//#endregion
|
|
931
1327
|
//#region src/builtins/storage-orchestrator/storage-orchestrator.addon.ts
|
|
932
1328
|
/**
|
|
933
1329
|
* `storage-orchestrator` builtin — singleton owner of the consumer-
|
|
@@ -973,6 +1369,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
973
1369
|
pressureManager = null;
|
|
974
1370
|
pressureTimer = null;
|
|
975
1371
|
pressureSweepInFlight = false;
|
|
1372
|
+
migration = null;
|
|
976
1373
|
/**
|
|
977
1374
|
* Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
|
|
978
1375
|
* synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
|
|
@@ -1166,6 +1563,57 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1166
1563
|
}
|
|
1167
1564
|
}
|
|
1168
1565
|
};
|
|
1566
|
+
const migration = new StorageMigrationCoordinator({
|
|
1567
|
+
locations: service,
|
|
1568
|
+
state: this.state("storage-migration", StorageMigrationJobSchema.nullable(), null),
|
|
1569
|
+
participants: {
|
|
1570
|
+
pipeline: {
|
|
1571
|
+
pause: async (leaseId) => {
|
|
1572
|
+
await this.ctx.api.pipelineOrchestrator.pauseForStorageMigration.mutate({ leaseId });
|
|
1573
|
+
},
|
|
1574
|
+
resume: async (leaseId) => {
|
|
1575
|
+
await this.ctx.api.pipelineOrchestrator.resumeForStorageMigration.mutate({ leaseId });
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
recorder: {
|
|
1579
|
+
pause: async (leaseId) => {
|
|
1580
|
+
await this.ctx.api.recording.pauseForStorageMigration.mutate({ leaseId });
|
|
1581
|
+
},
|
|
1582
|
+
resume: async (leaseId) => {
|
|
1583
|
+
await this.ctx.api.recording.resumeForStorageMigration.mutate({ leaseId });
|
|
1584
|
+
},
|
|
1585
|
+
startMove: (input) => this.ctx.api.recording.startStorageMigrationMove.mutate(input),
|
|
1586
|
+
getMove: (jobId) => this.ctx.api.recording.getStorageMigrationMoveStatus.query({ jobId }),
|
|
1587
|
+
cancelMove: async (jobId) => (await this.ctx.api.recording.cancelStorageMigrationMove.mutate({ jobId })).cancelled,
|
|
1588
|
+
refresh: async (leaseId) => {
|
|
1589
|
+
await this.ctx.api.recording.refreshStorageLocationsForMigration.mutate({ leaseId });
|
|
1590
|
+
}
|
|
1591
|
+
},
|
|
1592
|
+
analytics: {
|
|
1593
|
+
pause: async (leaseId) => {
|
|
1594
|
+
await this.ctx.api.pipelineAnalytics.pauseForStorageMigration.mutate({ leaseId });
|
|
1595
|
+
},
|
|
1596
|
+
resume: async (leaseId) => {
|
|
1597
|
+
await this.ctx.api.pipelineAnalytics.resumeForStorageMigration.mutate({ leaseId });
|
|
1598
|
+
},
|
|
1599
|
+
startMove: (input) => this.ctx.api.pipelineAnalytics.startStorageMigrationMove.mutate(input),
|
|
1600
|
+
getMove: (jobId) => this.ctx.api.pipelineAnalytics.getStorageMigrationMoveStatus.query({ jobId }),
|
|
1601
|
+
cancelMove: async (jobId) => (await this.ctx.api.pipelineAnalytics.cancelStorageMigrationMove.mutate({ jobId })).cancelled,
|
|
1602
|
+
refresh: async (leaseId) => {
|
|
1603
|
+
await this.ctx.api.pipelineAnalytics.refreshStorageLocationsForMigration.mutate({ leaseId });
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
},
|
|
1607
|
+
now: () => Date.now(),
|
|
1608
|
+
newId: () => randomUUID()
|
|
1609
|
+
});
|
|
1610
|
+
this.migration = migration;
|
|
1611
|
+
const migrationProvider = {
|
|
1612
|
+
plan: (input) => migration.plan(input),
|
|
1613
|
+
start: async (input) => ({ jobId: await migration.start(input) }),
|
|
1614
|
+
status: ({ jobId }) => migration.status(jobId),
|
|
1615
|
+
cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) })
|
|
1616
|
+
};
|
|
1169
1617
|
await this.seedFromDeclarations();
|
|
1170
1618
|
const eventBus = this.ctx.eventBus;
|
|
1171
1619
|
if (eventBus) {
|
|
@@ -1190,13 +1638,20 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1190
1638
|
this.runPressureSweep();
|
|
1191
1639
|
}, PRESSURE_SWEEP_INTERVAL_MS);
|
|
1192
1640
|
this.ctx.logger.info("Storage orchestrator initialized");
|
|
1193
|
-
return [
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1641
|
+
return [
|
|
1642
|
+
{
|
|
1643
|
+
capability: storageCapability,
|
|
1644
|
+
provider
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
capability: storageMigrationCapability,
|
|
1648
|
+
provider: migrationProvider
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
capability: settingsStoreCapability,
|
|
1652
|
+
provider: createDataStoreDispatch(getEngines)
|
|
1653
|
+
}
|
|
1654
|
+
];
|
|
1200
1655
|
}
|
|
1201
1656
|
async onShutdown() {
|
|
1202
1657
|
for (const dispose of this.seedSubscriptionDisposers) try {
|
|
@@ -1210,6 +1665,10 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1210
1665
|
this.pressureManager = null;
|
|
1211
1666
|
this.service?.clearSessions();
|
|
1212
1667
|
this.service = null;
|
|
1668
|
+
this.migration = null;
|
|
1669
|
+
}
|
|
1670
|
+
async onHubReachable() {
|
|
1671
|
+
await this.migration?.recover();
|
|
1213
1672
|
}
|
|
1214
1673
|
/**
|
|
1215
1674
|
* Resolve the live engine from the capability registry and wrap it in a
|
|
@@ -1431,4 +1890,4 @@ var StorageOrchestratorAddon = class extends BaseAddon {
|
|
|
1431
1890
|
}
|
|
1432
1891
|
};
|
|
1433
1892
|
//#endregion
|
|
1434
|
-
export { StorageOrchestratorAddon, StorageOrchestratorAddon as default,
|
|
1893
|
+
export { StorageOrchestratorAddon, StorageOrchestratorAddon as default, SqliteLocationStore as n, StorageOrchestratorService as r, StorageMigrationCoordinator as t };
|
|
@@ -193,6 +193,13 @@ export declare class StorageOrchestratorService {
|
|
|
193
193
|
backfillNodeIds(hubNodeId: string): Promise<void>;
|
|
194
194
|
listLocations(filter?: ListLocationsFilter): readonly StorageLocation[];
|
|
195
195
|
getDefaultLocation(type: string): StorageLocation | null;
|
|
196
|
+
/**
|
|
197
|
+
* Repoint several logical defaults as one in-memory commit. The migration
|
|
198
|
+
* coordinator persists its `repointing` phase before calling this method, so
|
|
199
|
+
* a process crash during the backing-store writes is recovered by replaying
|
|
200
|
+
* the same idempotent target set before anyone resumes writers.
|
|
201
|
+
*/
|
|
202
|
+
setDefaultLocations(targetByType: ReadonlyMap<string, string>): Promise<void>;
|
|
196
203
|
/**
|
|
197
204
|
* Insert or update a location. If `isDefault: true`, atomically
|
|
198
205
|
* demotes any other default for the same type — operators always see
|
|
@@ -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-cINpngi1.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 {
|
|
1
|
+
import { Nt as hydrateSchema, gt as BaseAddon, ht as errMsg } from "../../dist-CIIFhXqm.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-cINpngi1.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 {
|
|
1
|
+
import { J as logDestinationCapability, gt as BaseAddon } from "../../dist-CIIFhXqm.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";
|