@camstack/addon-pipeline-orchestrator 1.2.636 → 1.2.638
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/CHANGELOG.md +12 -0
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DwR2APoE.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Hya6JCSL.mjs} +1 -1
- package/dist/{hostInit-2ejULZGw.mjs → hostInit-BWrAZ8UU.mjs} +1 -1
- package/dist/index.js +308 -2
- package/dist/index.mjs +308 -2
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.638] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Node alerts name the node by its display name and id
|
|
15
|
+
|
|
16
|
+
## [1.2.637] - 2026-10-28
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Raises an alert when a node joining or leaving leaves cameras that a manual rebalance would move, with a one-click Rebalance now
|
|
21
|
+
|
|
10
22
|
## [1.2.636] - 2026-10-28
|
|
11
23
|
|
|
12
24
|
### Changed
|
package/dist/index.js
CHANGED
|
@@ -17661,9 +17661,21 @@ var AlertSeveritySchema = _enum([
|
|
|
17661
17661
|
"error",
|
|
17662
17662
|
"critical"
|
|
17663
17663
|
]);
|
|
17664
|
+
/**
|
|
17665
|
+
* An action that RUNS something instead of going somewhere: the admin UI calls
|
|
17666
|
+
* the addon's custom action (`addons.custom`) with exactly these fields. The
|
|
17667
|
+
* producer owns the meaning; the UI knows nothing about the addon or the kind.
|
|
17668
|
+
*/
|
|
17669
|
+
var AlertActionInvokeSchema = object({
|
|
17670
|
+
addonId: string().min(1),
|
|
17671
|
+
action: string().min(1),
|
|
17672
|
+
input: record(string(), unknown()).optional()
|
|
17673
|
+
});
|
|
17674
|
+
/** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
|
|
17664
17675
|
var AlertActionSchema = object({
|
|
17665
17676
|
label: string(),
|
|
17666
17677
|
href: string().optional(),
|
|
17678
|
+
invoke: AlertActionInvokeSchema.optional(),
|
|
17667
17679
|
/**
|
|
17668
17680
|
* How `href` is followed. Absent / `app`: an admin-ui route (client-side).
|
|
17669
17681
|
* `server`: a route the HUB serves (an addon HTTP route such as an OAuth
|
|
@@ -96457,8 +96469,17 @@ var NodeStressStatsOutputSchema = object({
|
|
|
96457
96469
|
at: number()
|
|
96458
96470
|
})).readonly()
|
|
96459
96471
|
});
|
|
96472
|
+
var RebalanceNowOutputSchema = object({
|
|
96473
|
+
migrated: number(),
|
|
96474
|
+
skipped: number()
|
|
96475
|
+
});
|
|
96460
96476
|
var pipelineOrchestratorActions = defineCustomActions({
|
|
96461
96477
|
dumpState: customAction(_void(), OrchestratorDiagnosticsSchema),
|
|
96478
|
+
/**
|
|
96479
|
+
* A manual rebalance (margin 0) — the action behind the "Camera placement can
|
|
96480
|
+
* be rebalanced" alert. Closes that alert as completed, or marks it failed.
|
|
96481
|
+
*/
|
|
96482
|
+
rebalanceNow: customAction(_void(), RebalanceNowOutputSchema, { auth: "admin" }),
|
|
96462
96483
|
nodeStressStats: customAction(NodeStressStatsInputSchema, NodeStressStatsOutputSchema, { auth: "admin" })
|
|
96463
96484
|
});
|
|
96464
96485
|
/**
|
|
@@ -99150,6 +99171,8 @@ var PlacementService = class {
|
|
|
99150
99171
|
const talliedLoads = () => loads.map((load) => tallyLoad(load, attachedDelta.get(load.nodeId) ?? 0, deviceDelta.get(load.nodeId)));
|
|
99151
99172
|
let migrated = 0;
|
|
99152
99173
|
const skipped = [];
|
|
99174
|
+
const moves = [];
|
|
99175
|
+
const dryRun = opts?.dryRun === true;
|
|
99153
99176
|
for (const [deviceId, config] of this.deps.ledger.configEntries()) {
|
|
99154
99177
|
if (opts?.onlyDeviceIds !== void 0 && !opts.onlyDeviceIds.has(deviceId)) continue;
|
|
99155
99178
|
if (this.deps.isIdleSessionCamera(deviceId)) continue;
|
|
@@ -99168,7 +99191,7 @@ var PlacementService = class {
|
|
|
99168
99191
|
});
|
|
99169
99192
|
if (!decision) continue;
|
|
99170
99193
|
if (decision.kind === "pending") {
|
|
99171
|
-
this.handlePendingDecision(deviceId, decision.reason);
|
|
99194
|
+
if (!dryRun) this.handlePendingDecision(deviceId, decision.reason);
|
|
99172
99195
|
continue;
|
|
99173
99196
|
}
|
|
99174
99197
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
@@ -99178,6 +99201,16 @@ var PlacementService = class {
|
|
|
99178
99201
|
const targetLoad = tallied.find((l) => l.nodeId === decision.agentNodeId);
|
|
99179
99202
|
if (currentLoad && targetLoad && capacityImprovement(currentLoad, targetLoad, nodeWeights) < minImprovement) continue;
|
|
99180
99203
|
}
|
|
99204
|
+
if (dryRun) {
|
|
99205
|
+
if (current) bumpAttached(current.agentNodeId, -1, config.deviceKey);
|
|
99206
|
+
bumpAttached(decision.agentNodeId, 1, config.deviceKey);
|
|
99207
|
+
moves.push({
|
|
99208
|
+
deviceId,
|
|
99209
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99210
|
+
toNodeId: decision.agentNodeId
|
|
99211
|
+
});
|
|
99212
|
+
continue;
|
|
99213
|
+
}
|
|
99181
99214
|
const placement = await this.resolvePlacement({
|
|
99182
99215
|
deviceId,
|
|
99183
99216
|
nodeId: decision.agentNodeId,
|
|
@@ -99220,11 +99253,28 @@ var PlacementService = class {
|
|
|
99220
99253
|
dispatchGuard.dispose();
|
|
99221
99254
|
}
|
|
99222
99255
|
migrated++;
|
|
99256
|
+
moves.push({
|
|
99257
|
+
deviceId,
|
|
99258
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99259
|
+
toNodeId: decision.agentNodeId
|
|
99260
|
+
});
|
|
99261
|
+
}
|
|
99262
|
+
if (dryRun) {
|
|
99263
|
+
ctx.logger.debug("rebalance: plan", { meta: {
|
|
99264
|
+
minImprovement,
|
|
99265
|
+
wouldMove: moves.length
|
|
99266
|
+
} });
|
|
99267
|
+
return {
|
|
99268
|
+
migrated: 0,
|
|
99269
|
+
skipped,
|
|
99270
|
+
moves
|
|
99271
|
+
};
|
|
99223
99272
|
}
|
|
99224
99273
|
ctx.logger.info("rebalance: migrated cameras", { meta: { migrated } });
|
|
99225
99274
|
return {
|
|
99226
99275
|
migrated,
|
|
99227
|
-
skipped
|
|
99276
|
+
skipped,
|
|
99277
|
+
moves
|
|
99228
99278
|
};
|
|
99229
99279
|
}
|
|
99230
99280
|
getPipelineAssignments() {
|
|
@@ -106915,6 +106965,157 @@ async function buildOrchestratorControllers(deps) {
|
|
|
106915
106965
|
};
|
|
106916
106966
|
}
|
|
106917
106967
|
//#endregion
|
|
106968
|
+
//#region src/rebalance-suggestion.ts
|
|
106969
|
+
var REBALANCE_SUGGESTED_ALERT_ID = "pipeline-orchestrator:rebalance-suggested";
|
|
106970
|
+
var NODE_NOT_USED_ALERT_PREFIX = "pipeline-orchestrator:node-not-used:";
|
|
106971
|
+
/** The alert category the Alert Center files both alerts under. */
|
|
106972
|
+
var ALERT_CATEGORY = "system";
|
|
106973
|
+
var DEFAULT_SUGGESTION_DEBOUNCE_MS = 9e4;
|
|
106974
|
+
var VERB = {
|
|
106975
|
+
joined: "joined",
|
|
106976
|
+
left: "left",
|
|
106977
|
+
capabilities: "changed its pipeline roles"
|
|
106978
|
+
};
|
|
106979
|
+
var RebalanceSuggestion = class {
|
|
106980
|
+
deps;
|
|
106981
|
+
/** `name (id)` when a name distinct from the id is known, else the id. */
|
|
106982
|
+
async label(nodeId) {
|
|
106983
|
+
const name = await this.deps.nodeName(nodeId);
|
|
106984
|
+
return name === null || name.length === 0 || name === nodeId ? nodeId : `${name} (${nodeId})`;
|
|
106985
|
+
}
|
|
106986
|
+
timer = null;
|
|
106987
|
+
lastNote = null;
|
|
106988
|
+
notUsedRaised = /* @__PURE__ */ new Set();
|
|
106989
|
+
disposed = false;
|
|
106990
|
+
constructor(deps) {
|
|
106991
|
+
this.deps = deps;
|
|
106992
|
+
}
|
|
106993
|
+
/** A node event: (re)arm the single evaluation. Forked-process ids are not nodes. */
|
|
106994
|
+
noteNodeEvent(note) {
|
|
106995
|
+
if (this.disposed || note.nodeId.includes("/")) return;
|
|
106996
|
+
this.lastNote = note;
|
|
106997
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
106998
|
+
this.timer = setTimeout(() => {
|
|
106999
|
+
this.timer = null;
|
|
107000
|
+
this.evaluate();
|
|
107001
|
+
}, this.deps.debounceMs);
|
|
107002
|
+
}
|
|
107003
|
+
dispose() {
|
|
107004
|
+
this.disposed = true;
|
|
107005
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
107006
|
+
this.timer = null;
|
|
107007
|
+
}
|
|
107008
|
+
async evaluate() {
|
|
107009
|
+
await this.evaluateSuggestion();
|
|
107010
|
+
await this.evaluateUnusedNodes();
|
|
107011
|
+
}
|
|
107012
|
+
async evaluateSuggestion() {
|
|
107013
|
+
const { logger } = this.deps;
|
|
107014
|
+
let manual;
|
|
107015
|
+
let sweep;
|
|
107016
|
+
try {
|
|
107017
|
+
manual = await this.deps.plan(0);
|
|
107018
|
+
sweep = await this.deps.plan(AUTO_REBALANCE_MIN_IMPROVEMENT);
|
|
107019
|
+
} catch (err) {
|
|
107020
|
+
logger.warn("rebalance suggestion: plan failed — alert left as it is", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
107021
|
+
return;
|
|
107022
|
+
}
|
|
107023
|
+
const sweepIds = new Set(sweep.moves.map((m) => m.deviceId));
|
|
107024
|
+
const onlyManual = manual.moves.filter((m) => !sweepIds.has(m.deviceId));
|
|
107025
|
+
if (onlyManual.length === 0) {
|
|
107026
|
+
logger.debug("rebalance suggestion: nothing beyond the auto sweep — closing the alert", { meta: {
|
|
107027
|
+
manualMoves: manual.moves.length,
|
|
107028
|
+
sweepMoves: sweep.moves.length
|
|
107029
|
+
} });
|
|
107030
|
+
await this.complete(REBALANCE_SUGGESTED_ALERT_ID);
|
|
107031
|
+
return;
|
|
107032
|
+
}
|
|
107033
|
+
const note = this.lastNote;
|
|
107034
|
+
const cause = note === null ? "The cluster changed" : `Node ${await this.label(note.nodeId)} ${VERB[note.kind]}`;
|
|
107035
|
+
const count = manual.moves.length;
|
|
107036
|
+
const now = this.deps.now();
|
|
107037
|
+
logger.info("rebalance suggestion: raising the alert", { meta: {
|
|
107038
|
+
manualMoves: count,
|
|
107039
|
+
beyondSweep: onlyManual.length
|
|
107040
|
+
} });
|
|
107041
|
+
await this.emit({
|
|
107042
|
+
id: REBALANCE_SUGGESTED_ALERT_ID,
|
|
107043
|
+
category: ALERT_CATEGORY,
|
|
107044
|
+
severity: "warning",
|
|
107045
|
+
title: "Camera placement can be rebalanced",
|
|
107046
|
+
message: `${cause}. ${String(count)} ${count === 1 ? "camera" : "cameras"} would move to even out the load; the automatic balancing will not do it on its own.`,
|
|
107047
|
+
action: {
|
|
107048
|
+
label: "Rebalance now",
|
|
107049
|
+
invoke: {
|
|
107050
|
+
addonId: this.deps.addonId,
|
|
107051
|
+
action: "rebalanceNow"
|
|
107052
|
+
}
|
|
107053
|
+
},
|
|
107054
|
+
status: "active",
|
|
107055
|
+
read: false,
|
|
107056
|
+
createdAt: now,
|
|
107057
|
+
updatedAt: now,
|
|
107058
|
+
source: {
|
|
107059
|
+
type: "addon",
|
|
107060
|
+
id: this.deps.addonId
|
|
107061
|
+
}
|
|
107062
|
+
});
|
|
107063
|
+
}
|
|
107064
|
+
async evaluateUnusedNodes() {
|
|
107065
|
+
const enabled = new Set(this.deps.enabledNodes());
|
|
107066
|
+
const unused = new Set(this.deps.runnerNodes().filter((id) => !enabled.has(id)));
|
|
107067
|
+
for (const nodeId of unused) {
|
|
107068
|
+
const id = `${NODE_NOT_USED_ALERT_PREFIX}${nodeId}`;
|
|
107069
|
+
if (this.notUsedRaised.has(id)) continue;
|
|
107070
|
+
this.notUsedRaised.add(id);
|
|
107071
|
+
this.deps.logger.info("rebalance suggestion: node online but not used for detection", { tags: { nodeId } });
|
|
107072
|
+
const now = this.deps.now();
|
|
107073
|
+
await this.emit({
|
|
107074
|
+
id,
|
|
107075
|
+
category: ALERT_CATEGORY,
|
|
107076
|
+
severity: "info",
|
|
107077
|
+
title: "Node is not used for detection",
|
|
107078
|
+
message: `Node ${await this.label(nodeId)} is online but detection is not enabled on it, so no camera will be placed there.`,
|
|
107079
|
+
action: {
|
|
107080
|
+
label: "Open node",
|
|
107081
|
+
href: `/system/pipeline?view=nodes&node=${encodeURIComponent(nodeId)}`,
|
|
107082
|
+
navigation: "app"
|
|
107083
|
+
},
|
|
107084
|
+
status: "active",
|
|
107085
|
+
read: false,
|
|
107086
|
+
createdAt: now,
|
|
107087
|
+
updatedAt: now,
|
|
107088
|
+
source: {
|
|
107089
|
+
type: "node",
|
|
107090
|
+
id: nodeId
|
|
107091
|
+
}
|
|
107092
|
+
});
|
|
107093
|
+
}
|
|
107094
|
+
for (const id of [...this.notUsedRaised]) {
|
|
107095
|
+
const nodeId = id.slice(36);
|
|
107096
|
+
if (unused.has(nodeId)) continue;
|
|
107097
|
+
this.notUsedRaised.delete(id);
|
|
107098
|
+
await this.complete(id);
|
|
107099
|
+
}
|
|
107100
|
+
}
|
|
107101
|
+
async emit(alert) {
|
|
107102
|
+
await this.deps.alerts.emit(alert).catch((err) => {
|
|
107103
|
+
this.deps.logger.warn("rebalance suggestion: could not raise an alert", { meta: {
|
|
107104
|
+
alertId: alert.id,
|
|
107105
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107106
|
+
} });
|
|
107107
|
+
});
|
|
107108
|
+
}
|
|
107109
|
+
async complete(alertId) {
|
|
107110
|
+
await this.deps.alerts.complete(alertId).catch((err) => {
|
|
107111
|
+
this.deps.logger.debug("rebalance suggestion: close failed (alert may not exist)", { meta: {
|
|
107112
|
+
alertId,
|
|
107113
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107114
|
+
} });
|
|
107115
|
+
});
|
|
107116
|
+
}
|
|
107117
|
+
};
|
|
107118
|
+
//#endregion
|
|
106918
107119
|
//#region src/settings-ui-schemas.ts
|
|
106919
107120
|
/** Build the addon-level schema sections (cluster roles + crop + balancer + failover). */
|
|
106920
107121
|
function buildGlobalSettingsSections(options) {
|
|
@@ -108711,6 +108912,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108711
108912
|
* Constructed right after `loadShed`. Nulled at the end of `onShutdown`.
|
|
108712
108913
|
*/
|
|
108713
108914
|
placement = null;
|
|
108915
|
+
/** The debounced "placement can be rebalanced" evaluation; see `rebalance-suggestion.ts`. */
|
|
108916
|
+
rebalanceSuggestion = null;
|
|
108917
|
+
unsubSuggestionEvents = [];
|
|
108714
108918
|
/** EventBus unsubscribes for agent online/offline events. */
|
|
108715
108919
|
unsubAgentOnline = null;
|
|
108716
108920
|
unsubAgentOffline = null;
|
|
@@ -108984,6 +109188,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108984
109188
|
this.localNodeId = controllers.localNodeId;
|
|
108985
109189
|
this.unsubAgentOnline = controllers.unsubAgentOnline;
|
|
108986
109190
|
this.unsubAgentOffline = controllers.unsubAgentOffline;
|
|
109191
|
+
this.startRebalanceSuggestion();
|
|
108987
109192
|
this.unsubOrchestratorSubscriptions = controllers.unsubOrchestratorSubscriptions;
|
|
108988
109193
|
this.pendingRetryTimer = controllers.pendingRetryTimer;
|
|
108989
109194
|
this.autoRebalanceTimer = controllers.autoRebalanceTimer;
|
|
@@ -109027,6 +109232,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109027
109232
|
customActions: pipelineOrchestratorActions,
|
|
109028
109233
|
actionHandlers: {
|
|
109029
109234
|
dumpState: async () => this.dumpDiagnostics(),
|
|
109235
|
+
rebalanceNow: async () => this.rebalanceNow(),
|
|
109030
109236
|
nodeStressStats: async (input) => {
|
|
109031
109237
|
const lts = this.nodeStressLts;
|
|
109032
109238
|
const moves = this.nodeStress?.historyView() ?? [];
|
|
@@ -109109,6 +109315,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109109
109315
|
this.detectionWiring = null;
|
|
109110
109316
|
this.unsubAgentOffline?.();
|
|
109111
109317
|
this.unsubAgentOffline = null;
|
|
109318
|
+
this.stopRebalanceSuggestion();
|
|
109112
109319
|
this.effectiveSharesService?.stop();
|
|
109113
109320
|
this.unsubAgentOnline?.();
|
|
109114
109321
|
this.unsubAgentOnline = null;
|
|
@@ -109175,6 +109382,101 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109175
109382
|
async rebalance(opts) {
|
|
109176
109383
|
return this.placement.rebalance(opts);
|
|
109177
109384
|
}
|
|
109385
|
+
/**
|
|
109386
|
+
* The `rebalanceNow` custom action: a manual rebalance, and the verdict on
|
|
109387
|
+
* the suggestion alert that offered it. Every branch is logged.
|
|
109388
|
+
*/
|
|
109389
|
+
async rebalanceNow() {
|
|
109390
|
+
const alertId = REBALANCE_SUGGESTED_ALERT_ID;
|
|
109391
|
+
try {
|
|
109392
|
+
const result = await this.rebalance({ minImprovement: 0 });
|
|
109393
|
+
const message = `${String(result.migrated)} ${result.migrated === 1 ? "camera" : "cameras"} moved`;
|
|
109394
|
+
this.ctx.logger.info("rebalanceNow: done", { meta: {
|
|
109395
|
+
migrated: result.migrated,
|
|
109396
|
+
skipped: result.skipped.length
|
|
109397
|
+
} });
|
|
109398
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109399
|
+
alertId,
|
|
109400
|
+
patch: {
|
|
109401
|
+
status: "completed",
|
|
109402
|
+
message,
|
|
109403
|
+
updatedAt: Date.now()
|
|
109404
|
+
}
|
|
109405
|
+
}).catch((err) => this.ctx.logger.warn("rebalanceNow: could not close the suggestion alert", { meta: { error: errMsg$2(err) } }));
|
|
109406
|
+
return {
|
|
109407
|
+
migrated: result.migrated,
|
|
109408
|
+
skipped: result.skipped.length
|
|
109409
|
+
};
|
|
109410
|
+
} catch (err) {
|
|
109411
|
+
const reason = errMsg$2(err);
|
|
109412
|
+
this.ctx.logger.error("rebalanceNow: failed", { meta: { error: reason } });
|
|
109413
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109414
|
+
alertId,
|
|
109415
|
+
patch: {
|
|
109416
|
+
status: "failed",
|
|
109417
|
+
message: `Rebalance failed: ${reason}`,
|
|
109418
|
+
updatedAt: Date.now()
|
|
109419
|
+
}
|
|
109420
|
+
}).catch((e) => this.ctx.logger.warn("rebalanceNow: could not mark the suggestion alert failed", { meta: { error: errMsg$2(e) } }));
|
|
109421
|
+
throw err;
|
|
109422
|
+
}
|
|
109423
|
+
}
|
|
109424
|
+
startRebalanceSuggestion() {
|
|
109425
|
+
const ctx = this.ctx;
|
|
109426
|
+
const suggestion = new RebalanceSuggestion({
|
|
109427
|
+
addonId: "pipeline-orchestrator",
|
|
109428
|
+
logger: ctx.logger,
|
|
109429
|
+
debounceMs: DEFAULT_SUGGESTION_DEBOUNCE_MS,
|
|
109430
|
+
now: () => Date.now(),
|
|
109431
|
+
plan: (minImprovement) => this.rebalance({
|
|
109432
|
+
minImprovement,
|
|
109433
|
+
dryRun: true
|
|
109434
|
+
}),
|
|
109435
|
+
enabledNodes: () => this.topology?.enabledNodes ?? [],
|
|
109436
|
+
runnerNodes: () => this.topology?.collectRunnerNodeIds() ?? [],
|
|
109437
|
+
nodeName: async (nodeId) => {
|
|
109438
|
+
try {
|
|
109439
|
+
return (await ctx.api.nodes.directory.query()).nodes.find((n) => n.id === nodeId)?.name ?? null;
|
|
109440
|
+
} catch {
|
|
109441
|
+
return null;
|
|
109442
|
+
}
|
|
109443
|
+
},
|
|
109444
|
+
alerts: {
|
|
109445
|
+
emit: (alert) => ctx.api.alerts.emit.mutate(alert),
|
|
109446
|
+
complete: (alertId) => ctx.api.alerts.update.mutate({
|
|
109447
|
+
alertId,
|
|
109448
|
+
patch: {
|
|
109449
|
+
status: "completed",
|
|
109450
|
+
updatedAt: Date.now()
|
|
109451
|
+
}
|
|
109452
|
+
})
|
|
109453
|
+
}
|
|
109454
|
+
});
|
|
109455
|
+
this.rebalanceSuggestion = suggestion;
|
|
109456
|
+
this.unsubSuggestionEvents = [
|
|
109457
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOnline }, (event) => suggestion.noteNodeEvent({
|
|
109458
|
+
kind: "joined",
|
|
109459
|
+
nodeId: event.data.agentId
|
|
109460
|
+
})),
|
|
109461
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOffline }, (event) => suggestion.noteNodeEvent({
|
|
109462
|
+
kind: "left",
|
|
109463
|
+
nodeId: event.data.agentId
|
|
109464
|
+
})),
|
|
109465
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentUnregistered }, (event) => {
|
|
109466
|
+
const agentId = event.data["agentId"];
|
|
109467
|
+
if (typeof agentId === "string") suggestion.noteNodeEvent({
|
|
109468
|
+
kind: "left",
|
|
109469
|
+
nodeId: agentId
|
|
109470
|
+
});
|
|
109471
|
+
})
|
|
109472
|
+
];
|
|
109473
|
+
}
|
|
109474
|
+
stopRebalanceSuggestion() {
|
|
109475
|
+
for (const unsub of this.unsubSuggestionEvents) unsub();
|
|
109476
|
+
this.unsubSuggestionEvents = [];
|
|
109477
|
+
this.rebalanceSuggestion?.dispose();
|
|
109478
|
+
this.rebalanceSuggestion = null;
|
|
109479
|
+
}
|
|
109178
109480
|
getPipelineAssignments() {
|
|
109179
109481
|
return this.placement.getPipelineAssignments();
|
|
109180
109482
|
}
|
|
@@ -109530,6 +109832,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109530
109832
|
this.topology.addKnownRunnerNode(input.agentNodeId);
|
|
109531
109833
|
this.reconcile.schedulePendingRetry();
|
|
109532
109834
|
if (input.detect !== void 0) this.noteShareChange(`node-detect:${input.agentNodeId}`);
|
|
109835
|
+
this.rebalanceSuggestion?.noteNodeEvent({
|
|
109836
|
+
kind: "capabilities",
|
|
109837
|
+
nodeId: input.agentNodeId
|
|
109838
|
+
});
|
|
109533
109839
|
return { success: true };
|
|
109534
109840
|
}
|
|
109535
109841
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -17633,9 +17633,21 @@ var AlertSeveritySchema = _enum([
|
|
|
17633
17633
|
"error",
|
|
17634
17634
|
"critical"
|
|
17635
17635
|
]);
|
|
17636
|
+
/**
|
|
17637
|
+
* An action that RUNS something instead of going somewhere: the admin UI calls
|
|
17638
|
+
* the addon's custom action (`addons.custom`) with exactly these fields. The
|
|
17639
|
+
* producer owns the meaning; the UI knows nothing about the addon or the kind.
|
|
17640
|
+
*/
|
|
17641
|
+
var AlertActionInvokeSchema = object({
|
|
17642
|
+
addonId: string().min(1),
|
|
17643
|
+
action: string().min(1),
|
|
17644
|
+
input: record(string(), unknown()).optional()
|
|
17645
|
+
});
|
|
17646
|
+
/** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
|
|
17636
17647
|
var AlertActionSchema = object({
|
|
17637
17648
|
label: string(),
|
|
17638
17649
|
href: string().optional(),
|
|
17650
|
+
invoke: AlertActionInvokeSchema.optional(),
|
|
17639
17651
|
/**
|
|
17640
17652
|
* How `href` is followed. Absent / `app`: an admin-ui route (client-side).
|
|
17641
17653
|
* `server`: a route the HUB serves (an addon HTTP route such as an OAuth
|
|
@@ -96429,8 +96441,17 @@ var NodeStressStatsOutputSchema = object({
|
|
|
96429
96441
|
at: number()
|
|
96430
96442
|
})).readonly()
|
|
96431
96443
|
});
|
|
96444
|
+
var RebalanceNowOutputSchema = object({
|
|
96445
|
+
migrated: number(),
|
|
96446
|
+
skipped: number()
|
|
96447
|
+
});
|
|
96432
96448
|
var pipelineOrchestratorActions = defineCustomActions({
|
|
96433
96449
|
dumpState: customAction(_void(), OrchestratorDiagnosticsSchema),
|
|
96450
|
+
/**
|
|
96451
|
+
* A manual rebalance (margin 0) — the action behind the "Camera placement can
|
|
96452
|
+
* be rebalanced" alert. Closes that alert as completed, or marks it failed.
|
|
96453
|
+
*/
|
|
96454
|
+
rebalanceNow: customAction(_void(), RebalanceNowOutputSchema, { auth: "admin" }),
|
|
96434
96455
|
nodeStressStats: customAction(NodeStressStatsInputSchema, NodeStressStatsOutputSchema, { auth: "admin" })
|
|
96435
96456
|
});
|
|
96436
96457
|
/**
|
|
@@ -99122,6 +99143,8 @@ var PlacementService = class {
|
|
|
99122
99143
|
const talliedLoads = () => loads.map((load) => tallyLoad(load, attachedDelta.get(load.nodeId) ?? 0, deviceDelta.get(load.nodeId)));
|
|
99123
99144
|
let migrated = 0;
|
|
99124
99145
|
const skipped = [];
|
|
99146
|
+
const moves = [];
|
|
99147
|
+
const dryRun = opts?.dryRun === true;
|
|
99125
99148
|
for (const [deviceId, config] of this.deps.ledger.configEntries()) {
|
|
99126
99149
|
if (opts?.onlyDeviceIds !== void 0 && !opts.onlyDeviceIds.has(deviceId)) continue;
|
|
99127
99150
|
if (this.deps.isIdleSessionCamera(deviceId)) continue;
|
|
@@ -99140,7 +99163,7 @@ var PlacementService = class {
|
|
|
99140
99163
|
});
|
|
99141
99164
|
if (!decision) continue;
|
|
99142
99165
|
if (decision.kind === "pending") {
|
|
99143
|
-
this.handlePendingDecision(deviceId, decision.reason);
|
|
99166
|
+
if (!dryRun) this.handlePendingDecision(deviceId, decision.reason);
|
|
99144
99167
|
continue;
|
|
99145
99168
|
}
|
|
99146
99169
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
@@ -99150,6 +99173,16 @@ var PlacementService = class {
|
|
|
99150
99173
|
const targetLoad = tallied.find((l) => l.nodeId === decision.agentNodeId);
|
|
99151
99174
|
if (currentLoad && targetLoad && capacityImprovement(currentLoad, targetLoad, nodeWeights) < minImprovement) continue;
|
|
99152
99175
|
}
|
|
99176
|
+
if (dryRun) {
|
|
99177
|
+
if (current) bumpAttached(current.agentNodeId, -1, config.deviceKey);
|
|
99178
|
+
bumpAttached(decision.agentNodeId, 1, config.deviceKey);
|
|
99179
|
+
moves.push({
|
|
99180
|
+
deviceId,
|
|
99181
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99182
|
+
toNodeId: decision.agentNodeId
|
|
99183
|
+
});
|
|
99184
|
+
continue;
|
|
99185
|
+
}
|
|
99153
99186
|
const placement = await this.resolvePlacement({
|
|
99154
99187
|
deviceId,
|
|
99155
99188
|
nodeId: decision.agentNodeId,
|
|
@@ -99192,11 +99225,28 @@ var PlacementService = class {
|
|
|
99192
99225
|
dispatchGuard.dispose();
|
|
99193
99226
|
}
|
|
99194
99227
|
migrated++;
|
|
99228
|
+
moves.push({
|
|
99229
|
+
deviceId,
|
|
99230
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99231
|
+
toNodeId: decision.agentNodeId
|
|
99232
|
+
});
|
|
99233
|
+
}
|
|
99234
|
+
if (dryRun) {
|
|
99235
|
+
ctx.logger.debug("rebalance: plan", { meta: {
|
|
99236
|
+
minImprovement,
|
|
99237
|
+
wouldMove: moves.length
|
|
99238
|
+
} });
|
|
99239
|
+
return {
|
|
99240
|
+
migrated: 0,
|
|
99241
|
+
skipped,
|
|
99242
|
+
moves
|
|
99243
|
+
};
|
|
99195
99244
|
}
|
|
99196
99245
|
ctx.logger.info("rebalance: migrated cameras", { meta: { migrated } });
|
|
99197
99246
|
return {
|
|
99198
99247
|
migrated,
|
|
99199
|
-
skipped
|
|
99248
|
+
skipped,
|
|
99249
|
+
moves
|
|
99200
99250
|
};
|
|
99201
99251
|
}
|
|
99202
99252
|
getPipelineAssignments() {
|
|
@@ -106887,6 +106937,157 @@ async function buildOrchestratorControllers(deps) {
|
|
|
106887
106937
|
};
|
|
106888
106938
|
}
|
|
106889
106939
|
//#endregion
|
|
106940
|
+
//#region src/rebalance-suggestion.ts
|
|
106941
|
+
var REBALANCE_SUGGESTED_ALERT_ID = "pipeline-orchestrator:rebalance-suggested";
|
|
106942
|
+
var NODE_NOT_USED_ALERT_PREFIX = "pipeline-orchestrator:node-not-used:";
|
|
106943
|
+
/** The alert category the Alert Center files both alerts under. */
|
|
106944
|
+
var ALERT_CATEGORY = "system";
|
|
106945
|
+
var DEFAULT_SUGGESTION_DEBOUNCE_MS = 9e4;
|
|
106946
|
+
var VERB = {
|
|
106947
|
+
joined: "joined",
|
|
106948
|
+
left: "left",
|
|
106949
|
+
capabilities: "changed its pipeline roles"
|
|
106950
|
+
};
|
|
106951
|
+
var RebalanceSuggestion = class {
|
|
106952
|
+
deps;
|
|
106953
|
+
/** `name (id)` when a name distinct from the id is known, else the id. */
|
|
106954
|
+
async label(nodeId) {
|
|
106955
|
+
const name = await this.deps.nodeName(nodeId);
|
|
106956
|
+
return name === null || name.length === 0 || name === nodeId ? nodeId : `${name} (${nodeId})`;
|
|
106957
|
+
}
|
|
106958
|
+
timer = null;
|
|
106959
|
+
lastNote = null;
|
|
106960
|
+
notUsedRaised = /* @__PURE__ */ new Set();
|
|
106961
|
+
disposed = false;
|
|
106962
|
+
constructor(deps) {
|
|
106963
|
+
this.deps = deps;
|
|
106964
|
+
}
|
|
106965
|
+
/** A node event: (re)arm the single evaluation. Forked-process ids are not nodes. */
|
|
106966
|
+
noteNodeEvent(note) {
|
|
106967
|
+
if (this.disposed || note.nodeId.includes("/")) return;
|
|
106968
|
+
this.lastNote = note;
|
|
106969
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
106970
|
+
this.timer = setTimeout(() => {
|
|
106971
|
+
this.timer = null;
|
|
106972
|
+
this.evaluate();
|
|
106973
|
+
}, this.deps.debounceMs);
|
|
106974
|
+
}
|
|
106975
|
+
dispose() {
|
|
106976
|
+
this.disposed = true;
|
|
106977
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
106978
|
+
this.timer = null;
|
|
106979
|
+
}
|
|
106980
|
+
async evaluate() {
|
|
106981
|
+
await this.evaluateSuggestion();
|
|
106982
|
+
await this.evaluateUnusedNodes();
|
|
106983
|
+
}
|
|
106984
|
+
async evaluateSuggestion() {
|
|
106985
|
+
const { logger } = this.deps;
|
|
106986
|
+
let manual;
|
|
106987
|
+
let sweep;
|
|
106988
|
+
try {
|
|
106989
|
+
manual = await this.deps.plan(0);
|
|
106990
|
+
sweep = await this.deps.plan(AUTO_REBALANCE_MIN_IMPROVEMENT);
|
|
106991
|
+
} catch (err) {
|
|
106992
|
+
logger.warn("rebalance suggestion: plan failed — alert left as it is", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
106993
|
+
return;
|
|
106994
|
+
}
|
|
106995
|
+
const sweepIds = new Set(sweep.moves.map((m) => m.deviceId));
|
|
106996
|
+
const onlyManual = manual.moves.filter((m) => !sweepIds.has(m.deviceId));
|
|
106997
|
+
if (onlyManual.length === 0) {
|
|
106998
|
+
logger.debug("rebalance suggestion: nothing beyond the auto sweep — closing the alert", { meta: {
|
|
106999
|
+
manualMoves: manual.moves.length,
|
|
107000
|
+
sweepMoves: sweep.moves.length
|
|
107001
|
+
} });
|
|
107002
|
+
await this.complete(REBALANCE_SUGGESTED_ALERT_ID);
|
|
107003
|
+
return;
|
|
107004
|
+
}
|
|
107005
|
+
const note = this.lastNote;
|
|
107006
|
+
const cause = note === null ? "The cluster changed" : `Node ${await this.label(note.nodeId)} ${VERB[note.kind]}`;
|
|
107007
|
+
const count = manual.moves.length;
|
|
107008
|
+
const now = this.deps.now();
|
|
107009
|
+
logger.info("rebalance suggestion: raising the alert", { meta: {
|
|
107010
|
+
manualMoves: count,
|
|
107011
|
+
beyondSweep: onlyManual.length
|
|
107012
|
+
} });
|
|
107013
|
+
await this.emit({
|
|
107014
|
+
id: REBALANCE_SUGGESTED_ALERT_ID,
|
|
107015
|
+
category: ALERT_CATEGORY,
|
|
107016
|
+
severity: "warning",
|
|
107017
|
+
title: "Camera placement can be rebalanced",
|
|
107018
|
+
message: `${cause}. ${String(count)} ${count === 1 ? "camera" : "cameras"} would move to even out the load; the automatic balancing will not do it on its own.`,
|
|
107019
|
+
action: {
|
|
107020
|
+
label: "Rebalance now",
|
|
107021
|
+
invoke: {
|
|
107022
|
+
addonId: this.deps.addonId,
|
|
107023
|
+
action: "rebalanceNow"
|
|
107024
|
+
}
|
|
107025
|
+
},
|
|
107026
|
+
status: "active",
|
|
107027
|
+
read: false,
|
|
107028
|
+
createdAt: now,
|
|
107029
|
+
updatedAt: now,
|
|
107030
|
+
source: {
|
|
107031
|
+
type: "addon",
|
|
107032
|
+
id: this.deps.addonId
|
|
107033
|
+
}
|
|
107034
|
+
});
|
|
107035
|
+
}
|
|
107036
|
+
async evaluateUnusedNodes() {
|
|
107037
|
+
const enabled = new Set(this.deps.enabledNodes());
|
|
107038
|
+
const unused = new Set(this.deps.runnerNodes().filter((id) => !enabled.has(id)));
|
|
107039
|
+
for (const nodeId of unused) {
|
|
107040
|
+
const id = `${NODE_NOT_USED_ALERT_PREFIX}${nodeId}`;
|
|
107041
|
+
if (this.notUsedRaised.has(id)) continue;
|
|
107042
|
+
this.notUsedRaised.add(id);
|
|
107043
|
+
this.deps.logger.info("rebalance suggestion: node online but not used for detection", { tags: { nodeId } });
|
|
107044
|
+
const now = this.deps.now();
|
|
107045
|
+
await this.emit({
|
|
107046
|
+
id,
|
|
107047
|
+
category: ALERT_CATEGORY,
|
|
107048
|
+
severity: "info",
|
|
107049
|
+
title: "Node is not used for detection",
|
|
107050
|
+
message: `Node ${await this.label(nodeId)} is online but detection is not enabled on it, so no camera will be placed there.`,
|
|
107051
|
+
action: {
|
|
107052
|
+
label: "Open node",
|
|
107053
|
+
href: `/system/pipeline?view=nodes&node=${encodeURIComponent(nodeId)}`,
|
|
107054
|
+
navigation: "app"
|
|
107055
|
+
},
|
|
107056
|
+
status: "active",
|
|
107057
|
+
read: false,
|
|
107058
|
+
createdAt: now,
|
|
107059
|
+
updatedAt: now,
|
|
107060
|
+
source: {
|
|
107061
|
+
type: "node",
|
|
107062
|
+
id: nodeId
|
|
107063
|
+
}
|
|
107064
|
+
});
|
|
107065
|
+
}
|
|
107066
|
+
for (const id of [...this.notUsedRaised]) {
|
|
107067
|
+
const nodeId = id.slice(36);
|
|
107068
|
+
if (unused.has(nodeId)) continue;
|
|
107069
|
+
this.notUsedRaised.delete(id);
|
|
107070
|
+
await this.complete(id);
|
|
107071
|
+
}
|
|
107072
|
+
}
|
|
107073
|
+
async emit(alert) {
|
|
107074
|
+
await this.deps.alerts.emit(alert).catch((err) => {
|
|
107075
|
+
this.deps.logger.warn("rebalance suggestion: could not raise an alert", { meta: {
|
|
107076
|
+
alertId: alert.id,
|
|
107077
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107078
|
+
} });
|
|
107079
|
+
});
|
|
107080
|
+
}
|
|
107081
|
+
async complete(alertId) {
|
|
107082
|
+
await this.deps.alerts.complete(alertId).catch((err) => {
|
|
107083
|
+
this.deps.logger.debug("rebalance suggestion: close failed (alert may not exist)", { meta: {
|
|
107084
|
+
alertId,
|
|
107085
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107086
|
+
} });
|
|
107087
|
+
});
|
|
107088
|
+
}
|
|
107089
|
+
};
|
|
107090
|
+
//#endregion
|
|
106890
107091
|
//#region src/settings-ui-schemas.ts
|
|
106891
107092
|
/** Build the addon-level schema sections (cluster roles + crop + balancer + failover). */
|
|
106892
107093
|
function buildGlobalSettingsSections(options) {
|
|
@@ -108683,6 +108884,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108683
108884
|
* Constructed right after `loadShed`. Nulled at the end of `onShutdown`.
|
|
108684
108885
|
*/
|
|
108685
108886
|
placement = null;
|
|
108887
|
+
/** The debounced "placement can be rebalanced" evaluation; see `rebalance-suggestion.ts`. */
|
|
108888
|
+
rebalanceSuggestion = null;
|
|
108889
|
+
unsubSuggestionEvents = [];
|
|
108686
108890
|
/** EventBus unsubscribes for agent online/offline events. */
|
|
108687
108891
|
unsubAgentOnline = null;
|
|
108688
108892
|
unsubAgentOffline = null;
|
|
@@ -108956,6 +109160,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108956
109160
|
this.localNodeId = controllers.localNodeId;
|
|
108957
109161
|
this.unsubAgentOnline = controllers.unsubAgentOnline;
|
|
108958
109162
|
this.unsubAgentOffline = controllers.unsubAgentOffline;
|
|
109163
|
+
this.startRebalanceSuggestion();
|
|
108959
109164
|
this.unsubOrchestratorSubscriptions = controllers.unsubOrchestratorSubscriptions;
|
|
108960
109165
|
this.pendingRetryTimer = controllers.pendingRetryTimer;
|
|
108961
109166
|
this.autoRebalanceTimer = controllers.autoRebalanceTimer;
|
|
@@ -108999,6 +109204,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108999
109204
|
customActions: pipelineOrchestratorActions,
|
|
109000
109205
|
actionHandlers: {
|
|
109001
109206
|
dumpState: async () => this.dumpDiagnostics(),
|
|
109207
|
+
rebalanceNow: async () => this.rebalanceNow(),
|
|
109002
109208
|
nodeStressStats: async (input) => {
|
|
109003
109209
|
const lts = this.nodeStressLts;
|
|
109004
109210
|
const moves = this.nodeStress?.historyView() ?? [];
|
|
@@ -109081,6 +109287,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109081
109287
|
this.detectionWiring = null;
|
|
109082
109288
|
this.unsubAgentOffline?.();
|
|
109083
109289
|
this.unsubAgentOffline = null;
|
|
109290
|
+
this.stopRebalanceSuggestion();
|
|
109084
109291
|
this.effectiveSharesService?.stop();
|
|
109085
109292
|
this.unsubAgentOnline?.();
|
|
109086
109293
|
this.unsubAgentOnline = null;
|
|
@@ -109147,6 +109354,101 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109147
109354
|
async rebalance(opts) {
|
|
109148
109355
|
return this.placement.rebalance(opts);
|
|
109149
109356
|
}
|
|
109357
|
+
/**
|
|
109358
|
+
* The `rebalanceNow` custom action: a manual rebalance, and the verdict on
|
|
109359
|
+
* the suggestion alert that offered it. Every branch is logged.
|
|
109360
|
+
*/
|
|
109361
|
+
async rebalanceNow() {
|
|
109362
|
+
const alertId = REBALANCE_SUGGESTED_ALERT_ID;
|
|
109363
|
+
try {
|
|
109364
|
+
const result = await this.rebalance({ minImprovement: 0 });
|
|
109365
|
+
const message = `${String(result.migrated)} ${result.migrated === 1 ? "camera" : "cameras"} moved`;
|
|
109366
|
+
this.ctx.logger.info("rebalanceNow: done", { meta: {
|
|
109367
|
+
migrated: result.migrated,
|
|
109368
|
+
skipped: result.skipped.length
|
|
109369
|
+
} });
|
|
109370
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109371
|
+
alertId,
|
|
109372
|
+
patch: {
|
|
109373
|
+
status: "completed",
|
|
109374
|
+
message,
|
|
109375
|
+
updatedAt: Date.now()
|
|
109376
|
+
}
|
|
109377
|
+
}).catch((err) => this.ctx.logger.warn("rebalanceNow: could not close the suggestion alert", { meta: { error: errMsg$2(err) } }));
|
|
109378
|
+
return {
|
|
109379
|
+
migrated: result.migrated,
|
|
109380
|
+
skipped: result.skipped.length
|
|
109381
|
+
};
|
|
109382
|
+
} catch (err) {
|
|
109383
|
+
const reason = errMsg$2(err);
|
|
109384
|
+
this.ctx.logger.error("rebalanceNow: failed", { meta: { error: reason } });
|
|
109385
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109386
|
+
alertId,
|
|
109387
|
+
patch: {
|
|
109388
|
+
status: "failed",
|
|
109389
|
+
message: `Rebalance failed: ${reason}`,
|
|
109390
|
+
updatedAt: Date.now()
|
|
109391
|
+
}
|
|
109392
|
+
}).catch((e) => this.ctx.logger.warn("rebalanceNow: could not mark the suggestion alert failed", { meta: { error: errMsg$2(e) } }));
|
|
109393
|
+
throw err;
|
|
109394
|
+
}
|
|
109395
|
+
}
|
|
109396
|
+
startRebalanceSuggestion() {
|
|
109397
|
+
const ctx = this.ctx;
|
|
109398
|
+
const suggestion = new RebalanceSuggestion({
|
|
109399
|
+
addonId: "pipeline-orchestrator",
|
|
109400
|
+
logger: ctx.logger,
|
|
109401
|
+
debounceMs: DEFAULT_SUGGESTION_DEBOUNCE_MS,
|
|
109402
|
+
now: () => Date.now(),
|
|
109403
|
+
plan: (minImprovement) => this.rebalance({
|
|
109404
|
+
minImprovement,
|
|
109405
|
+
dryRun: true
|
|
109406
|
+
}),
|
|
109407
|
+
enabledNodes: () => this.topology?.enabledNodes ?? [],
|
|
109408
|
+
runnerNodes: () => this.topology?.collectRunnerNodeIds() ?? [],
|
|
109409
|
+
nodeName: async (nodeId) => {
|
|
109410
|
+
try {
|
|
109411
|
+
return (await ctx.api.nodes.directory.query()).nodes.find((n) => n.id === nodeId)?.name ?? null;
|
|
109412
|
+
} catch {
|
|
109413
|
+
return null;
|
|
109414
|
+
}
|
|
109415
|
+
},
|
|
109416
|
+
alerts: {
|
|
109417
|
+
emit: (alert) => ctx.api.alerts.emit.mutate(alert),
|
|
109418
|
+
complete: (alertId) => ctx.api.alerts.update.mutate({
|
|
109419
|
+
alertId,
|
|
109420
|
+
patch: {
|
|
109421
|
+
status: "completed",
|
|
109422
|
+
updatedAt: Date.now()
|
|
109423
|
+
}
|
|
109424
|
+
})
|
|
109425
|
+
}
|
|
109426
|
+
});
|
|
109427
|
+
this.rebalanceSuggestion = suggestion;
|
|
109428
|
+
this.unsubSuggestionEvents = [
|
|
109429
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOnline }, (event) => suggestion.noteNodeEvent({
|
|
109430
|
+
kind: "joined",
|
|
109431
|
+
nodeId: event.data.agentId
|
|
109432
|
+
})),
|
|
109433
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOffline }, (event) => suggestion.noteNodeEvent({
|
|
109434
|
+
kind: "left",
|
|
109435
|
+
nodeId: event.data.agentId
|
|
109436
|
+
})),
|
|
109437
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentUnregistered }, (event) => {
|
|
109438
|
+
const agentId = event.data["agentId"];
|
|
109439
|
+
if (typeof agentId === "string") suggestion.noteNodeEvent({
|
|
109440
|
+
kind: "left",
|
|
109441
|
+
nodeId: agentId
|
|
109442
|
+
});
|
|
109443
|
+
})
|
|
109444
|
+
];
|
|
109445
|
+
}
|
|
109446
|
+
stopRebalanceSuggestion() {
|
|
109447
|
+
for (const unsub of this.unsubSuggestionEvents) unsub();
|
|
109448
|
+
this.unsubSuggestionEvents = [];
|
|
109449
|
+
this.rebalanceSuggestion?.dispose();
|
|
109450
|
+
this.rebalanceSuggestion = null;
|
|
109451
|
+
}
|
|
109150
109452
|
getPipelineAssignments() {
|
|
109151
109453
|
return this.placement.getPipelineAssignments();
|
|
109152
109454
|
}
|
|
@@ -109502,6 +109804,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109502
109804
|
this.topology.addKnownRunnerNode(input.agentNodeId);
|
|
109503
109805
|
this.reconcile.schedulePendingRetry();
|
|
109504
109806
|
if (input.detect !== void 0) this.noteShareChange(`node-detect:${input.agentNodeId}`);
|
|
109807
|
+
this.rebalanceSuggestion?.noteNodeEvent({
|
|
109808
|
+
kind: "capabilities",
|
|
109809
|
+
nodeId: input.agentNodeId
|
|
109810
|
+
});
|
|
109505
109811
|
return { success: true };
|
|
109506
109812
|
}
|
|
109507
109813
|
/**
|
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-Hya6JCSL.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.2.
|
|
3
|
+
"version": "1.2.638",
|
|
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",
|