@camstack/addon-pipeline-orchestrator 1.1.21 → 1.1.22
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/_stub.js +1 -1
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DVzgRExi.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CQmCy3VK.mjs} +3 -3
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DdDNCPJ5.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BA0gpKU7.mjs} +1 -1
- package/dist/{hostInit-CEqZfaom.mjs → hostInit-ibn4yaBr.mjs} +3 -3
- package/dist/index.js +340 -55
- package/dist/index.mjs +340 -55
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -15699,7 +15699,17 @@ var AgentAddonConfigSchema = object({
|
|
|
15699
15699
|
});
|
|
15700
15700
|
var AgentPipelineSettingsSchema = object({
|
|
15701
15701
|
addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
|
|
15702
|
-
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
15702
|
+
maxCameras: number().int().nonnegative().nullable().default(null),
|
|
15703
|
+
/** Per-node detection weight (relative share for the quota balancer). */
|
|
15704
|
+
detectWeight: number().positive().optional(),
|
|
15705
|
+
/** Node is eligible to run the detection pipeline (decode + inference). */
|
|
15706
|
+
detect: boolean().optional(),
|
|
15707
|
+
/** Node is eligible to host decoder sessions. */
|
|
15708
|
+
decode: boolean().optional(),
|
|
15709
|
+
/** Node is eligible to run audio-analyzer sessions. */
|
|
15710
|
+
audio: boolean().optional(),
|
|
15711
|
+
/** Node is eligible to be the ingest / source-owner (serve the restream). */
|
|
15712
|
+
ingest: boolean().optional()
|
|
15703
15713
|
});
|
|
15704
15714
|
var CameraPipelineForAgentSchema = object({
|
|
15705
15715
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -16133,6 +16143,38 @@ var pipelineOrchestratorCapability = {
|
|
|
16133
16143
|
kind: "mutation",
|
|
16134
16144
|
auth: "admin"
|
|
16135
16145
|
}),
|
|
16146
|
+
/**
|
|
16147
|
+
* Set a node's detection WEIGHT (relative share for the quota balancer).
|
|
16148
|
+
* `null` clears it (back to the implicit weight 1). Unpinned cameras
|
|
16149
|
+
* distribute proportionally to weight; `maxCameras` still clamps on top.
|
|
16150
|
+
*/
|
|
16151
|
+
setAgentDetectWeight: method(object({
|
|
16152
|
+
agentNodeId: string(),
|
|
16153
|
+
detectWeight: number().positive().nullable()
|
|
16154
|
+
}), object({ success: literal(true) }), {
|
|
16155
|
+
kind: "mutation",
|
|
16156
|
+
auth: "admin"
|
|
16157
|
+
}),
|
|
16158
|
+
/**
|
|
16159
|
+
* Set one node's placement CAPABILITIES — the per-node record that
|
|
16160
|
+
* replaced the four flat orchestrator lists (`enabledNodes`,
|
|
16161
|
+
* `enabledDecoderNodes`, `enabledAudioNodes`, `remoteSourcingNodes`).
|
|
16162
|
+
* Each flag is optional in the patch: omit a flag to leave it unchanged,
|
|
16163
|
+
* pass `null` to reset it to the node default (`hub` → capable, every
|
|
16164
|
+
* other node → not). Detection/decode/audio eligibility is derived from
|
|
16165
|
+
* these flags across the cluster; changing them re-derives the enabled
|
|
16166
|
+
* sets and reconciles dispatch immediately.
|
|
16167
|
+
*/
|
|
16168
|
+
setAgentCapabilities: method(object({
|
|
16169
|
+
agentNodeId: string(),
|
|
16170
|
+
detect: boolean().nullable().optional(),
|
|
16171
|
+
decode: boolean().nullable().optional(),
|
|
16172
|
+
audio: boolean().nullable().optional(),
|
|
16173
|
+
ingest: boolean().nullable().optional()
|
|
16174
|
+
}), object({ success: literal(true) }), {
|
|
16175
|
+
kind: "mutation",
|
|
16176
|
+
auth: "admin"
|
|
16177
|
+
}),
|
|
16136
16178
|
/** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
|
|
16137
16179
|
getCameraSettings: method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()),
|
|
16138
16180
|
/** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
|
|
@@ -22039,6 +22081,18 @@ Object.freeze({
|
|
|
22039
22081
|
addonId: null,
|
|
22040
22082
|
access: "create"
|
|
22041
22083
|
},
|
|
22084
|
+
"pipelineOrchestrator.setAgentCapabilities": {
|
|
22085
|
+
capName: "pipeline-orchestrator",
|
|
22086
|
+
capScope: "system",
|
|
22087
|
+
addonId: null,
|
|
22088
|
+
access: "create"
|
|
22089
|
+
},
|
|
22090
|
+
"pipelineOrchestrator.setAgentDetectWeight": {
|
|
22091
|
+
capName: "pipeline-orchestrator",
|
|
22092
|
+
capScope: "system",
|
|
22093
|
+
addonId: null,
|
|
22094
|
+
access: "create"
|
|
22095
|
+
},
|
|
22042
22096
|
"pipelineOrchestrator.setAgentMaxCameras": {
|
|
22043
22097
|
capName: "pipeline-orchestrator",
|
|
22044
22098
|
capScope: "system",
|
|
@@ -24182,6 +24236,23 @@ function computeCapacityScore(load) {
|
|
|
24182
24236
|
return load.attachedCameras * Math.max(load.avgInferenceFps, 0) + Math.max(load.queueDepthTotal, 0);
|
|
24183
24237
|
}
|
|
24184
24238
|
/**
|
|
24239
|
+
* Node detection weight, defaulting to 1 when absent/invalid. A higher weight
|
|
24240
|
+
* makes a node preferred proportionally more (see {@link BalancerInput.weights}).
|
|
24241
|
+
*/
|
|
24242
|
+
function nodeWeight(nodeId, weights) {
|
|
24243
|
+
const w = weights?.[nodeId];
|
|
24244
|
+
return w !== void 0 && w > 0 ? w : 1;
|
|
24245
|
+
}
|
|
24246
|
+
/**
|
|
24247
|
+
* Weight-adjusted capacity score — the value the balancer minimises. Dividing
|
|
24248
|
+
* the raw capacity score by the node's weight makes a heavier node accumulate
|
|
24249
|
+
* proportionally more load before it loses preference, yielding weight-
|
|
24250
|
+
* proportional distribution while still preferring drained/idle nodes.
|
|
24251
|
+
*/
|
|
24252
|
+
function weightedScore(load, weights) {
|
|
24253
|
+
return computeCapacityScore(load) / nodeWeight(load.nodeId, weights);
|
|
24254
|
+
}
|
|
24255
|
+
/**
|
|
24185
24256
|
* Returns true when the node has remaining capacity.
|
|
24186
24257
|
* A node is eligible iff `cap` is unlimited (null/absent/<=0) OR
|
|
24187
24258
|
* its `attachedCameras` count is strictly less than the cap.
|
|
@@ -24247,8 +24318,9 @@ function balance(input) {
|
|
|
24247
24318
|
};
|
|
24248
24319
|
const best = eligible.map((node) => ({
|
|
24249
24320
|
node,
|
|
24250
|
-
score: computeCapacityScore(node)
|
|
24251
|
-
|
|
24321
|
+
score: computeCapacityScore(node),
|
|
24322
|
+
sortKey: weightedScore(node, input.weights)
|
|
24323
|
+
})).toSorted((a, b) => a.sortKey - b.sortKey || a.node.nodeId.localeCompare(b.node.nodeId))[0];
|
|
24252
24324
|
return {
|
|
24253
24325
|
kind: "assigned",
|
|
24254
24326
|
agentNodeId: best.node.nodeId,
|
|
@@ -24350,7 +24422,12 @@ var StoredAgentAddonConfigSchema = object({
|
|
|
24350
24422
|
});
|
|
24351
24423
|
var StoredAgentPipelineSettingsSchema = object({
|
|
24352
24424
|
addonDefaults: record(string(), StoredAgentAddonConfigSchema).readonly(),
|
|
24353
|
-
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
24425
|
+
maxCameras: number().int().nonnegative().nullable().default(null),
|
|
24426
|
+
detectWeight: number().positive().optional(),
|
|
24427
|
+
detect: boolean().optional(),
|
|
24428
|
+
decode: boolean().optional(),
|
|
24429
|
+
audio: boolean().optional(),
|
|
24430
|
+
ingest: boolean().optional()
|
|
24354
24431
|
});
|
|
24355
24432
|
var AgentSettingsMapSchema = record(string(), StoredAgentPipelineSettingsSchema);
|
|
24356
24433
|
var StoredCameraStepOverridePatchSchema = object({
|
|
@@ -24889,6 +24966,13 @@ var OrchestratorDiagnosticsSchema = object({
|
|
|
24889
24966
|
knownRunnerNodes: array(string()),
|
|
24890
24967
|
cachedAgentLoadNodeIds: array(string()),
|
|
24891
24968
|
enabledNodes: array(string()),
|
|
24969
|
+
enabledDecoderNodes: array(string()),
|
|
24970
|
+
enabledAudioNodes: array(string()),
|
|
24971
|
+
clusterRoles: object({
|
|
24972
|
+
ingestNode: string(),
|
|
24973
|
+
recordingNode: string(),
|
|
24974
|
+
audioNode: string()
|
|
24975
|
+
}),
|
|
24892
24976
|
assignedDeviceCount: number().int().min(0),
|
|
24893
24977
|
cameraConfigCount: number().int().min(0),
|
|
24894
24978
|
activeDetectionCount: number().int().min(0)
|
|
@@ -25005,36 +25089,48 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25005
25089
|
failoverPolicy = { ...DEFAULT_FAILOVER_POLICY };
|
|
25006
25090
|
/**
|
|
25007
25091
|
* Hub-wide allow-list of node ids eligible to run the detection
|
|
25008
|
-
* pipeline.
|
|
25009
|
-
*
|
|
25010
|
-
*
|
|
25011
|
-
* The list is a strict whitelist —
|
|
25012
|
-
* for other capabilities (metrics, logs,
|
|
25013
|
-
* never receive camera assignments.
|
|
25092
|
+
* pipeline. DERIVED (placement-model redesign) from the per-node
|
|
25093
|
+
* capability store (`agentSettings[node].detect`) — no longer a flat
|
|
25094
|
+
* `enabledNodes` multiselect. Recomputed by {@link refreshNodeCapabilities}
|
|
25095
|
+
* on boot and on every capability write. The list is a strict whitelist —
|
|
25096
|
+
* disabled nodes stay connected for other capabilities (metrics, logs,
|
|
25097
|
+
* cluster-wide events) but never receive camera assignments.
|
|
25014
25098
|
*/
|
|
25015
25099
|
enabledNodes = ["hub"];
|
|
25016
25100
|
/**
|
|
25017
25101
|
* Hub-wide allow-list of node ids eligible to run decoder sessions.
|
|
25018
|
-
*
|
|
25102
|
+
* DERIVED from the per-node capability store (`agentSettings[node].decode`).
|
|
25019
25103
|
* Defaults to `['hub']` — the hub always has a decoder available.
|
|
25020
25104
|
*/
|
|
25021
25105
|
enabledDecoderNodes = ["hub"];
|
|
25022
25106
|
/**
|
|
25023
|
-
*
|
|
25024
|
-
*
|
|
25025
|
-
*
|
|
25026
|
-
*
|
|
25027
|
-
*
|
|
25028
|
-
*
|
|
25107
|
+
* Nodes allowed to run the REMOTE-SOURCE leg — dial a camera's source-owner
|
|
25108
|
+
* restream over the LAN and decode it locally. Held for the source-owner
|
|
25109
|
+
* plumbing (`source-owner.ts`) but currently ALWAYS EMPTY: the cross-node
|
|
25110
|
+
* frame transport is a separate activation phase (placement-model §9 P1).
|
|
25111
|
+
* With this empty, `resolveSourceOwner` returns `undefined` and every
|
|
25112
|
+
* `selectRunnerFrameSource` emits `local-broker` — bit-identical to
|
|
25113
|
+
* pre-Phase-2 behavior.
|
|
25029
25114
|
*/
|
|
25030
25115
|
remoteSourcingNodes = [];
|
|
25031
25116
|
/**
|
|
25032
25117
|
* Hub-wide allow-list of node ids eligible to run audio-analyzer sessions.
|
|
25033
|
-
*
|
|
25118
|
+
* DERIVED from the per-node capability store (`agentSettings[node].audio`).
|
|
25034
25119
|
* Defaults to `['hub']`.
|
|
25035
25120
|
*/
|
|
25036
25121
|
enabledAudioNodes = ["hub"];
|
|
25037
25122
|
/**
|
|
25123
|
+
* Cluster-wide singleton role assignments (placement-model §6). Each role
|
|
25124
|
+
* is the ONE node that serves it for every camera. Driven by the
|
|
25125
|
+
* `ingestNode` / `recordingNode` / `audioNode` node-selects in the addon
|
|
25126
|
+
* global schema; defaults to the hub for all three.
|
|
25127
|
+
*/
|
|
25128
|
+
clusterRoles = {
|
|
25129
|
+
ingestNode: "hub",
|
|
25130
|
+
recordingNode: "hub",
|
|
25131
|
+
audioNode: "hub"
|
|
25132
|
+
};
|
|
25133
|
+
/**
|
|
25038
25134
|
* Full global settings snapshot kept in memory so synchronous cap methods
|
|
25039
25135
|
* (e.g. `getGlobalOrchestrationSettings`) can resolve without awaiting the
|
|
25040
25136
|
* backing store. Refreshed in `initialize` and `onConfigChange`.
|
|
@@ -25103,6 +25199,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25103
25199
|
loadShedState = /* @__PURE__ */ new Map();
|
|
25104
25200
|
/** Timer for the auto-resume sweep. */
|
|
25105
25201
|
loadShedResumeTimer = null;
|
|
25202
|
+
/** Per-camera guard so repeated low-fps snapshots don't stack relocate/pause. */
|
|
25203
|
+
shedInFlight = /* @__PURE__ */ new Set();
|
|
25106
25204
|
/** Pending `scheduleReconcile` debounce timer. */
|
|
25107
25205
|
reconcileTimer = null;
|
|
25108
25206
|
/** True while `reconcileDispatch` is awaiting the RPC round-trip. */
|
|
@@ -25127,6 +25225,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25127
25225
|
const stored = await this.resolveGlobalStore();
|
|
25128
25226
|
this.globalSettings = { ...stored };
|
|
25129
25227
|
this.applyRuntimeSettings(this.globalSettings);
|
|
25228
|
+
await this.refreshNodeCapabilities();
|
|
25130
25229
|
} catch (err) {
|
|
25131
25230
|
const msg = errMsg(err);
|
|
25132
25231
|
this.ctx.logger.warn("orchestrator settings load failed — using defaults", { meta: { error: msg } });
|
|
@@ -25542,6 +25641,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25542
25641
|
knownRunnerNodes: [...this.knownRunnerNodes].toSorted(),
|
|
25543
25642
|
cachedAgentLoadNodeIds: [...this.cachedAgentLoad.keys()].toSorted(),
|
|
25544
25643
|
enabledNodes: [...this.enabledNodes],
|
|
25644
|
+
enabledDecoderNodes: [...this.enabledDecoderNodes],
|
|
25645
|
+
enabledAudioNodes: [...this.enabledAudioNodes],
|
|
25646
|
+
clusterRoles: { ...this.clusterRoles },
|
|
25545
25647
|
assignedDeviceCount: this.assignments.size,
|
|
25546
25648
|
cameraConfigCount: this.cameraConfigs.size,
|
|
25547
25649
|
activeDetectionCount: this.activeDetections.size
|
|
@@ -25666,6 +25768,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25666
25768
|
nodes: await this.collectAgentLoad({ onlyEnabled: true }),
|
|
25667
25769
|
preferredAgent,
|
|
25668
25770
|
nodeCaps: await this.buildNodeCaps(),
|
|
25771
|
+
weights: await this.buildNodeWeights(),
|
|
25669
25772
|
eligibleNodes: this.detectionEligibleNodes(runnerConfig.deviceId)
|
|
25670
25773
|
});
|
|
25671
25774
|
if (!decision) throw new Error(`dispatchCamera: no runner available for ${runnerConfig.deviceId}`);
|
|
@@ -25782,6 +25885,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25782
25885
|
nodes: await this.collectAgentLoad({ onlyEnabled: true }),
|
|
25783
25886
|
preferredAgent: null,
|
|
25784
25887
|
nodeCaps: await this.buildNodeCaps(),
|
|
25888
|
+
weights: await this.buildNodeWeights(),
|
|
25785
25889
|
eligibleNodes: this.detectionEligibleNodes(input.deviceId)
|
|
25786
25890
|
});
|
|
25787
25891
|
if (!decision) this.ctx.logger.warn("unassignPipeline: no runner available, leaving unassigned", { tags: { deviceId: input.deviceId } });
|
|
@@ -25830,6 +25934,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25830
25934
|
if (!this.ctx) throw new Error("PipelineOrchestrator: rebalance called before initialize");
|
|
25831
25935
|
const loads = await this.collectAgentLoad({ onlyEnabled: true });
|
|
25832
25936
|
const nodeCaps = await this.buildNodeCaps();
|
|
25937
|
+
const nodeWeights = await this.buildNodeWeights();
|
|
25833
25938
|
const attachedDelta = /* @__PURE__ */ new Map();
|
|
25834
25939
|
const bumpAttached = (nodeId, by) => {
|
|
25835
25940
|
attachedDelta.set(nodeId, (attachedDelta.get(nodeId) ?? 0) + by);
|
|
@@ -25851,6 +25956,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25851
25956
|
nodes: talliedLoads(),
|
|
25852
25957
|
preferredAgent,
|
|
25853
25958
|
nodeCaps,
|
|
25959
|
+
weights: nodeWeights,
|
|
25854
25960
|
eligibleNodes: this.detectionEligibleNodes(deviceId)
|
|
25855
25961
|
});
|
|
25856
25962
|
if (!decision) continue;
|
|
@@ -26306,6 +26412,18 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26306
26412
|
for (const [nodeId, settings] of Object.entries(blob)) caps[nodeId] = settings.maxCameras ?? null;
|
|
26307
26413
|
return caps;
|
|
26308
26414
|
}
|
|
26415
|
+
/**
|
|
26416
|
+
* Build a per-node detection WEIGHT map from the persisted agent settings —
|
|
26417
|
+
* the sibling of {@link buildNodeCaps}. Nodes without a stored `detectWeight`
|
|
26418
|
+
* are simply absent (the balancer treats absent as weight 1). Passed to every
|
|
26419
|
+
* `balance()` call so unpinned cameras distribute proportionally to weight.
|
|
26420
|
+
*/
|
|
26421
|
+
async buildNodeWeights() {
|
|
26422
|
+
const blob = await this.agentSettingsState.get();
|
|
26423
|
+
const weights = {};
|
|
26424
|
+
for (const [nodeId, settings] of Object.entries(blob)) if (typeof settings.detectWeight === "number" && settings.detectWeight > 0) weights[nodeId] = settings.detectWeight;
|
|
26425
|
+
return weights;
|
|
26426
|
+
}
|
|
26309
26427
|
async readAudioNodePin(deviceId) {
|
|
26310
26428
|
if (!this.ctx?.settings) return null;
|
|
26311
26429
|
try {
|
|
@@ -26454,6 +26572,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26454
26572
|
nodes: loads,
|
|
26455
26573
|
preferredAgent: null,
|
|
26456
26574
|
nodeCaps: await this.buildNodeCaps(),
|
|
26575
|
+
weights: await this.buildNodeWeights(),
|
|
26457
26576
|
eligibleNodes: this.detectionEligibleNodes(deviceId)
|
|
26458
26577
|
});
|
|
26459
26578
|
if (!decision) {
|
|
@@ -27181,6 +27300,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27181
27300
|
};
|
|
27182
27301
|
const { [input.agentNodeId]: _drop, ...rest } = all;
|
|
27183
27302
|
await this.agentSettingsState.set(rest);
|
|
27303
|
+
await this.refreshNodeCapabilities();
|
|
27184
27304
|
this.ctx.logger.info("agentSettings entry removed", { tags: { nodeId: input.agentNodeId } });
|
|
27185
27305
|
return {
|
|
27186
27306
|
success: true,
|
|
@@ -27204,6 +27324,68 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27204
27324
|
this.schedulePendingRetry();
|
|
27205
27325
|
return { success: true };
|
|
27206
27326
|
}
|
|
27327
|
+
async setAgentDetectWeight(input) {
|
|
27328
|
+
const existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
27329
|
+
const weight = input.detectWeight ?? 1;
|
|
27330
|
+
const next = existing ? {
|
|
27331
|
+
...existing,
|
|
27332
|
+
detectWeight: weight
|
|
27333
|
+
} : {
|
|
27334
|
+
addonDefaults: {},
|
|
27335
|
+
maxCameras: null,
|
|
27336
|
+
detectWeight: weight
|
|
27337
|
+
};
|
|
27338
|
+
await this.writeAgentSettings(input.agentNodeId, next);
|
|
27339
|
+
this.ctx.logger.info("agentSettings.detectWeight updated", {
|
|
27340
|
+
tags: { nodeId: input.agentNodeId },
|
|
27341
|
+
meta: { detectWeight: weight ?? null }
|
|
27342
|
+
});
|
|
27343
|
+
this.rebalance().catch((err) => {
|
|
27344
|
+
this.ctx.logger.warn("setAgentDetectWeight: rebalance failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
27345
|
+
});
|
|
27346
|
+
return { success: true };
|
|
27347
|
+
}
|
|
27348
|
+
/**
|
|
27349
|
+
* Set one node's placement capabilities — the per-node record that replaced
|
|
27350
|
+
* the four flat orchestrator lists. Each flag is a tri-state patch: omit to
|
|
27351
|
+
* leave unchanged, `null` to reset to the node default (persisted as an
|
|
27352
|
+
* absent flag), `true`/`false` to force. After persisting, re-derive the
|
|
27353
|
+
* enabled-node sets and reconcile dispatch so the change takes effect
|
|
27354
|
+
* immediately (reuses the existing balancer / retry, no custom logic).
|
|
27355
|
+
*/
|
|
27356
|
+
async setAgentCapabilities(input) {
|
|
27357
|
+
const existing = (await this.readAgentSettingsMap())[input.agentNodeId] ?? {
|
|
27358
|
+
addonDefaults: {},
|
|
27359
|
+
maxCameras: null
|
|
27360
|
+
};
|
|
27361
|
+
const apply = (key) => {
|
|
27362
|
+
const patch = input[key];
|
|
27363
|
+
if (patch === void 0) return existing[key];
|
|
27364
|
+
if (patch === null) return void 0;
|
|
27365
|
+
return patch;
|
|
27366
|
+
};
|
|
27367
|
+
const next = {
|
|
27368
|
+
...existing,
|
|
27369
|
+
detect: apply("detect"),
|
|
27370
|
+
decode: apply("decode"),
|
|
27371
|
+
audio: apply("audio"),
|
|
27372
|
+
ingest: apply("ingest")
|
|
27373
|
+
};
|
|
27374
|
+
await this.writeAgentSettings(input.agentNodeId, next);
|
|
27375
|
+
this.ctx.logger.info("agentSettings.capabilities updated", {
|
|
27376
|
+
tags: { nodeId: input.agentNodeId },
|
|
27377
|
+
meta: {
|
|
27378
|
+
detect: next.detect,
|
|
27379
|
+
decode: next.decode,
|
|
27380
|
+
audio: next.audio,
|
|
27381
|
+
ingest: next.ingest
|
|
27382
|
+
}
|
|
27383
|
+
});
|
|
27384
|
+
await this.refreshNodeCapabilities();
|
|
27385
|
+
this.knownRunnerNodes.add(input.agentNodeId);
|
|
27386
|
+
this.schedulePendingRetry();
|
|
27387
|
+
return { success: true };
|
|
27388
|
+
}
|
|
27207
27389
|
async getCameraSettings(input) {
|
|
27208
27390
|
return (await this.readCameraSettingsMap())[String(input.deviceId)] ?? null;
|
|
27209
27391
|
}
|
|
@@ -27569,7 +27751,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27569
27751
|
const all = await this.readAgentSettingsMap();
|
|
27570
27752
|
const existing = all[nodeId];
|
|
27571
27753
|
all[nodeId] = {
|
|
27572
|
-
|
|
27754
|
+
...existing,
|
|
27755
|
+
...settings,
|
|
27573
27756
|
maxCameras: settings.maxCameras !== void 0 ? settings.maxCameras ?? null : existing?.maxCameras ?? null
|
|
27574
27757
|
};
|
|
27575
27758
|
await this.agentSettingsState.set(all);
|
|
@@ -27852,45 +28035,37 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27852
28035
|
const rand = Math.random().toString(36).slice(2, 8);
|
|
27853
28036
|
return `tpl_${Date.now().toString(36).slice(-6)}${rand}`;
|
|
27854
28037
|
}
|
|
27855
|
-
/** Build the addon-level schema (cluster
|
|
28038
|
+
/** Build the addon-level schema (cluster roles + balancer + failover). */
|
|
27856
28039
|
globalSettingsSchema() {
|
|
27857
28040
|
return this.schema({ sections: [
|
|
27858
28041
|
{
|
|
27859
28042
|
id: "cluster",
|
|
27860
|
-
title: "Cluster",
|
|
28043
|
+
title: "Cluster Roles",
|
|
27861
28044
|
tab: "pipeline",
|
|
27862
|
-
description: "
|
|
28045
|
+
description: "Cluster-wide singleton roles: the single node that serves each role for every camera. Per-node detection/decode/audio CAPABILITIES + weights + camera caps are edited in the per-node capability table (agent settings), not here.",
|
|
27863
28046
|
fields: [
|
|
27864
28047
|
{
|
|
27865
|
-
key: "
|
|
27866
|
-
type: "node-
|
|
27867
|
-
label: "
|
|
27868
|
-
description: "
|
|
27869
|
-
default:
|
|
28048
|
+
key: "ingestNode",
|
|
28049
|
+
type: "node-select",
|
|
28050
|
+
label: "Ingest Node",
|
|
28051
|
+
description: "The node that connects every camera and serves the compressed restream. Defaults to the hub. (Arbitrary ingest ≠ hub is a later phase; kept here so the role is modeled.)",
|
|
28052
|
+
default: "hub",
|
|
27870
28053
|
showOffline: true
|
|
27871
28054
|
},
|
|
27872
28055
|
{
|
|
27873
|
-
key: "
|
|
27874
|
-
type: "node-
|
|
27875
|
-
label: "
|
|
27876
|
-
description: "
|
|
27877
|
-
default:
|
|
28056
|
+
key: "recordingNode",
|
|
28057
|
+
type: "node-select",
|
|
28058
|
+
label: "Recording Node",
|
|
28059
|
+
description: "The node that records every camera (passthrough copy to its storage location). Defaults to the hub.",
|
|
28060
|
+
default: "hub",
|
|
27878
28061
|
showOffline: true
|
|
27879
28062
|
},
|
|
27880
28063
|
{
|
|
27881
|
-
key: "
|
|
27882
|
-
type: "node-
|
|
27883
|
-
label: "
|
|
27884
|
-
description: "
|
|
27885
|
-
default:
|
|
27886
|
-
showOffline: true
|
|
27887
|
-
},
|
|
27888
|
-
{
|
|
27889
|
-
key: "enabledAudioNodes",
|
|
27890
|
-
type: "node-multiselect",
|
|
27891
|
-
label: "Enabled Audio Nodes",
|
|
27892
|
-
description: "Nodes eligible to run audio-analyzer sessions. Disabled nodes remain connected for other capabilities but will never receive audio assignments.",
|
|
27893
|
-
default: ["hub"],
|
|
28064
|
+
key: "audioNode",
|
|
28065
|
+
type: "node-select",
|
|
28066
|
+
label: "Audio Node",
|
|
28067
|
+
description: "The node that runs all audio analysis. Defaults to the hub. Must be an audio-capable node.",
|
|
28068
|
+
default: "hub",
|
|
27894
28069
|
showOffline: true
|
|
27895
28070
|
}
|
|
27896
28071
|
]
|
|
@@ -28393,16 +28568,58 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28393
28568
|
pinnedOnDisconnect: config["pinnedOnDisconnect"] === "unpin-and-migrate" ? "unpin-and-migrate" : "leave-pinned",
|
|
28394
28569
|
onReconnect: config["onReconnect"] === "rebalance" ? "rebalance" : "restore"
|
|
28395
28570
|
};
|
|
28396
|
-
const
|
|
28397
|
-
|
|
28398
|
-
|
|
28399
|
-
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
28403
|
-
|
|
28571
|
+
const readRole = (key) => {
|
|
28572
|
+
const raw = config[key];
|
|
28573
|
+
return typeof raw === "string" && raw.length > 0 && !raw.includes("/") ? raw : "hub";
|
|
28574
|
+
};
|
|
28575
|
+
this.clusterRoles = {
|
|
28576
|
+
ingestNode: readRole("ingestNode"),
|
|
28577
|
+
recordingNode: readRole("recordingNode"),
|
|
28578
|
+
audioNode: readRole("audioNode")
|
|
28579
|
+
};
|
|
28404
28580
|
this.schedulePendingRetry();
|
|
28405
28581
|
}
|
|
28582
|
+
/**
|
|
28583
|
+
* Default placement capability for a node when its stored flag is unset:
|
|
28584
|
+
* the hub is capable of every role out of the box; every other node must be
|
|
28585
|
+
* explicitly enabled. This reproduces the classic `['hub']` defaults of the
|
|
28586
|
+
* four removed flat lists against an empty capability store.
|
|
28587
|
+
*/
|
|
28588
|
+
static nodeCapabilityDefault(nodeId) {
|
|
28589
|
+
return nodeId === "hub";
|
|
28590
|
+
}
|
|
28591
|
+
/**
|
|
28592
|
+
* Recompute the derived enabled-node sets (`enabledNodes` /
|
|
28593
|
+
* `enabledDecoderNodes` / `enabledAudioNodes`) from the per-node capability
|
|
28594
|
+
* store. A node is eligible for a concern iff its stored flag is `true`, or
|
|
28595
|
+
* the flag is unset AND the node defaults capable ({@link nodeCapabilityDefault}).
|
|
28596
|
+
* Forked child nodeIds (`hub/classifier`) are never dispatchable and are
|
|
28597
|
+
* excluded. `hub` is always considered even when absent from the store, so a
|
|
28598
|
+
* fresh install keeps the hub-only cluster working.
|
|
28599
|
+
*/
|
|
28600
|
+
async refreshNodeCapabilities() {
|
|
28601
|
+
let blob = {};
|
|
28602
|
+
try {
|
|
28603
|
+
blob = await this.agentSettingsState.get();
|
|
28604
|
+
} catch (err) {
|
|
28605
|
+
this.ctx.logger.warn("refreshNodeCapabilities: agent settings read failed", { meta: { error: errMsg(err) } });
|
|
28606
|
+
}
|
|
28607
|
+
const nodeIds = new Set(["hub"]);
|
|
28608
|
+
for (const nodeId of Object.keys(blob)) if (typeof nodeId === "string" && nodeId.length > 0 && !nodeId.includes("/")) nodeIds.add(nodeId);
|
|
28609
|
+
const detect = [];
|
|
28610
|
+
const decode = [];
|
|
28611
|
+
const audio = [];
|
|
28612
|
+
const capable = (flag, nodeId) => typeof flag === "boolean" ? flag : PipelineOrchestratorAddon.nodeCapabilityDefault(nodeId);
|
|
28613
|
+
for (const nodeId of nodeIds) {
|
|
28614
|
+
const settings = blob[nodeId];
|
|
28615
|
+
if (capable(settings?.detect, nodeId)) detect.push(nodeId);
|
|
28616
|
+
if (capable(settings?.decode, nodeId)) decode.push(nodeId);
|
|
28617
|
+
if (capable(settings?.audio, nodeId)) audio.push(nodeId);
|
|
28618
|
+
}
|
|
28619
|
+
this.enabledNodes = detect.toSorted();
|
|
28620
|
+
this.enabledDecoderNodes = decode.toSorted();
|
|
28621
|
+
this.enabledAudioNodes = audio.toSorted();
|
|
28622
|
+
}
|
|
28406
28623
|
get api() {
|
|
28407
28624
|
return this.ctx.api ?? null;
|
|
28408
28625
|
}
|
|
@@ -28999,7 +29216,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28999
29216
|
});
|
|
29000
29217
|
return;
|
|
29001
29218
|
}
|
|
29002
|
-
this.
|
|
29219
|
+
if (this.shedInFlight.has(deviceId)) return;
|
|
29220
|
+
this.shedInFlight.add(deviceId);
|
|
29221
|
+
this.ctx.logger.warn("Load management: sustained low fps — relocating or pausing", {
|
|
29003
29222
|
tags: { deviceId },
|
|
29004
29223
|
meta: {
|
|
29005
29224
|
actualFps: metrics.actualFps,
|
|
@@ -29008,7 +29227,28 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29008
29227
|
windowMs
|
|
29009
29228
|
}
|
|
29010
29229
|
});
|
|
29011
|
-
state.
|
|
29230
|
+
this.shedOrRelocate(deviceId, state).finally(() => this.shedInFlight.delete(deviceId));
|
|
29231
|
+
}
|
|
29232
|
+
/**
|
|
29233
|
+
* Cluster-wide load-shed action: relocate the sustained-slow camera to a
|
|
29234
|
+
* less-loaded node if the balancer (ceiling + weight aware) finds one,
|
|
29235
|
+
* otherwise pause it. Reuses `balance()` / `attachOn` / `detachOn` — no custom
|
|
29236
|
+
* placement logic.
|
|
29237
|
+
*/
|
|
29238
|
+
async shedOrRelocate(deviceId, state) {
|
|
29239
|
+
if (state.pausedAt !== null) return;
|
|
29240
|
+
if (await this.tryClusterRelocate(deviceId).catch((err) => {
|
|
29241
|
+
this.ctx.logger.warn("Load management: cluster relocate failed", {
|
|
29242
|
+
tags: { deviceId },
|
|
29243
|
+
meta: { error: errMsg(err) }
|
|
29244
|
+
});
|
|
29245
|
+
return false;
|
|
29246
|
+
})) {
|
|
29247
|
+
state.lowSinceTs = null;
|
|
29248
|
+
return;
|
|
29249
|
+
}
|
|
29250
|
+
this.ctx.logger.warn("Load management: pausing camera detection (no spare cluster capacity)", { tags: { deviceId } });
|
|
29251
|
+
state.pausedAt = Date.now();
|
|
29012
29252
|
state.lowSinceTs = null;
|
|
29013
29253
|
this.pipelineWatchdog?.unregister(deviceId);
|
|
29014
29254
|
this.ctx.api?.pipelineRunner.detachCamera.mutate({ deviceId }).catch((err) => {
|
|
@@ -29019,6 +29259,51 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29019
29259
|
});
|
|
29020
29260
|
this.ensureLoadShedResumeTimer();
|
|
29021
29261
|
}
|
|
29262
|
+
/**
|
|
29263
|
+
* Try to move a sustained-slow camera to a less-loaded node CLUSTER-WIDE.
|
|
29264
|
+
* Runs the same `balance()` the dispatcher uses (per-node ceiling + weight +
|
|
29265
|
+
* frame-source eligibility); the camera's current node counts it in its own
|
|
29266
|
+
* load, so a genuinely less-loaded node with spare capacity wins. Returns
|
|
29267
|
+
* `true` iff the camera was moved to a DIFFERENT node. A manually pinned
|
|
29268
|
+
* camera never relocates. Reuses `detachOn`/`attachOn`/`recordAssignment`.
|
|
29269
|
+
*/
|
|
29270
|
+
async tryClusterRelocate(deviceId) {
|
|
29271
|
+
const current = this.assignments.get(deviceId);
|
|
29272
|
+
const cached = this.cameraConfigs.get(deviceId);
|
|
29273
|
+
if (!current || !cached) return false;
|
|
29274
|
+
const currentNode = current.agentNodeId;
|
|
29275
|
+
if (await this.readPipelinePin(deviceId)) return false;
|
|
29276
|
+
const decision = balance({
|
|
29277
|
+
nodes: await this.collectAgentLoad({ onlyEnabled: true }),
|
|
29278
|
+
preferredAgent: null,
|
|
29279
|
+
nodeCaps: await this.buildNodeCaps(),
|
|
29280
|
+
weights: await this.buildNodeWeights(),
|
|
29281
|
+
eligibleNodes: this.detectionEligibleNodes(deviceId)
|
|
29282
|
+
});
|
|
29283
|
+
if (!decision || decision.kind !== "assigned") return false;
|
|
29284
|
+
const target = decision.agentNodeId;
|
|
29285
|
+
if (target === currentNode) return false;
|
|
29286
|
+
await this.detachOn(currentNode, deviceId).catch((err) => {
|
|
29287
|
+
this.ctx.logger.debug("Load management: relocate detach-old failed", {
|
|
29288
|
+
tags: { deviceId },
|
|
29289
|
+
meta: {
|
|
29290
|
+
from: currentNode,
|
|
29291
|
+
error: errMsg(err)
|
|
29292
|
+
}
|
|
29293
|
+
});
|
|
29294
|
+
});
|
|
29295
|
+
await this.attachOn(target, cached);
|
|
29296
|
+
this.recordAssignment(deviceId, target, "rebalance", false);
|
|
29297
|
+
this.ctx.logger.info("Load management: relocated camera to spare-capacity node (cluster-wide)", {
|
|
29298
|
+
tags: { deviceId },
|
|
29299
|
+
meta: {
|
|
29300
|
+
from: currentNode,
|
|
29301
|
+
to: target,
|
|
29302
|
+
score: decision.score
|
|
29303
|
+
}
|
|
29304
|
+
});
|
|
29305
|
+
return true;
|
|
29306
|
+
}
|
|
29022
29307
|
/** Base cooldown before first auto-resume attempt. */
|
|
29023
29308
|
static LOAD_SHED_BASE_COOLDOWN_MS = 3e4;
|
|
29024
29309
|
/** Maximum backoff cap. */
|
package/dist/remoteEntry.js
CHANGED
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CQmCy3VK.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline-orchestrator",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.22",
|
|
4
4
|
"description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|