@camstack/addon-pipeline-orchestrator 1.2.181 → 1.2.182
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/assets/viewer/.viewer-current-files.json +5 -5
- package/assets/viewer/.viewer-version +1 -1
- package/assets/viewer/_expo/static/js/web/Clipboard-fffea46d75b5210ec63206a6e71109c9.js +8 -0
- package/assets/viewer/_expo/static/js/web/Haptics-3d0b88cef97052d03a162bf09cceb2f2.js +3 -0
- package/assets/viewer/_expo/static/js/web/{index-e332b95855a7450dc918a234e13f19ea.js → index-0a664153411e8ea701f301321758dcde.js} +1555 -1551
- package/assets/viewer/_expo/static/js/web/node-fs-01fcfc0be4013fb16134783c5c55066c.js +1 -0
- package/assets/viewer/_expo/static/js/web/node-path-cbe5ac061679ae3273222ef45ab6ff8e.js +1 -0
- package/assets/viewer/index.html +1 -1
- package/dist/_stub.js +1 -1
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CUGRt6hQ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-B944RKiT.mjs} +3 -3
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-bQkIh01P.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-H_CMYslN.mjs} +1 -1
- package/dist/{hostInit-DdMcjaFM.mjs → hostInit-Dp6V7dau.mjs} +3 -3
- package/dist/index.js +295 -57
- package/dist/index.mjs +295 -57
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17097,6 +17097,7 @@ var NcSystemEventKindSchema = _enum([
|
|
|
17097
17097
|
"addon-crash-loop",
|
|
17098
17098
|
"addon-update-available",
|
|
17099
17099
|
"server-update-available",
|
|
17100
|
+
"wrapper-update-available",
|
|
17100
17101
|
"alarm-triggered",
|
|
17101
17102
|
"alarm-armed",
|
|
17102
17103
|
"alarm-disarmed",
|
|
@@ -23355,6 +23356,53 @@ var ImageContractSchema = object({
|
|
|
23355
23356
|
/** One operator-grade sentence: this node runs image X; the contract says Y. */
|
|
23356
23357
|
message: string()
|
|
23357
23358
|
});
|
|
23359
|
+
/**
|
|
23360
|
+
* The shell verdict (see {@link WrapperKindSchema}) with the proofs that
|
|
23361
|
+
* produced it.
|
|
23362
|
+
*
|
|
23363
|
+
* There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
|
|
23364
|
+
* update is an ANNOUNCEMENT. The notification body carries the exact command
|
|
23365
|
+
* for a `docker` shell and says the app restarts into the new build for
|
|
23366
|
+
* `electron`, and that sentence is chosen from `kind` alone — a boolean
|
|
23367
|
+
* nothing reads is the defect this repo keeps paying for.
|
|
23368
|
+
*/
|
|
23369
|
+
var WrapperIdentitySchema = object({
|
|
23370
|
+
kind: _enum([
|
|
23371
|
+
"docker",
|
|
23372
|
+
"electron",
|
|
23373
|
+
"native",
|
|
23374
|
+
"contradictory",
|
|
23375
|
+
"unknown"
|
|
23376
|
+
]),
|
|
23377
|
+
/** Every probe that ran, in a stable order. Never empty. */
|
|
23378
|
+
evidence: array(object({
|
|
23379
|
+
/** Which probe this row reports. */
|
|
23380
|
+
fact: _enum([
|
|
23381
|
+
"dockerenv-file",
|
|
23382
|
+
"proc-1-cgroup",
|
|
23383
|
+
"platform",
|
|
23384
|
+
"electron-app-version-env"
|
|
23385
|
+
]),
|
|
23386
|
+
/**
|
|
23387
|
+
* `observed` — this process read it itself. `declared` — the shell told the
|
|
23388
|
+
* process and nothing here can check it. Only the Electron app version is
|
|
23389
|
+
* ever `declared`, and only because a child cannot see its parent.
|
|
23390
|
+
*/
|
|
23391
|
+
mode: _enum(["observed", "declared"]),
|
|
23392
|
+
/** Did the probe support the fact it names? A `false` row is still evidence. */
|
|
23393
|
+
holds: boolean(),
|
|
23394
|
+
/** What was actually read, verbatim enough for an operator to argue with. */
|
|
23395
|
+
detail: string()
|
|
23396
|
+
})),
|
|
23397
|
+
/**
|
|
23398
|
+
* The version of the SHELL, when the shell has one it can state: the
|
|
23399
|
+
* Electron app version it declared, or the baked seed closure that
|
|
23400
|
+
* fingerprints a container / app bundle. `null` for `contradictory` and
|
|
23401
|
+
* `unknown` — which version belongs to the shell is precisely what is not
|
|
23402
|
+
* known there.
|
|
23403
|
+
*/
|
|
23404
|
+
currentVersion: string().nullable()
|
|
23405
|
+
});
|
|
23358
23406
|
var ServerRollbackInfoSchema = object({
|
|
23359
23407
|
/** The version that failed (or was manually rolled back). */
|
|
23360
23408
|
fromVersion: string(),
|
|
@@ -23396,7 +23444,13 @@ var ServerPackageStatusSchema = object({
|
|
|
23396
23444
|
* Seed-vs-contract verdict (see {@link ImageContractSchema}). Optional for
|
|
23397
23445
|
* version skew: an older provider's payload simply omits it.
|
|
23398
23446
|
*/
|
|
23399
|
-
imageContract: ImageContractSchema.optional()
|
|
23447
|
+
imageContract: ImageContractSchema.optional(),
|
|
23448
|
+
/**
|
|
23449
|
+
* What this node runs INSIDE (see {@link WrapperIdentitySchema}). Optional
|
|
23450
|
+
* for version skew: an older provider's payload simply omits it, and an
|
|
23451
|
+
* absent wrapper is unknown — never `native`.
|
|
23452
|
+
*/
|
|
23453
|
+
wrapper: WrapperIdentitySchema.optional()
|
|
23400
23454
|
});
|
|
23401
23455
|
var ServerUpdateCheckResultSchema = object({
|
|
23402
23456
|
packageName: string(),
|
|
@@ -42925,6 +42979,52 @@ var LoadShedController = class LoadShedController {
|
|
|
42925
42979
|
}
|
|
42926
42980
|
};
|
|
42927
42981
|
//#endregion
|
|
42982
|
+
//#region src/dispatch-timing.ts
|
|
42983
|
+
/**
|
|
42984
|
+
* How long the placement census waits for ONE runner's `getLocalCameras`
|
|
42985
|
+
* before counting that node as not having reported. A node that did not
|
|
42986
|
+
* report never has its assignments dropped (`reconcilePlacement` says so), so
|
|
42987
|
+
* skipping a slow node costs one pass of orphan cleanup on that node, not
|
|
42988
|
+
* correctness. The census fans out in parallel, so this bounds the whole
|
|
42989
|
+
* census at one budget rather than one budget per node.
|
|
42990
|
+
*/
|
|
42991
|
+
var RECONCILE_NODE_BUDGET_MS = 1500;
|
|
42992
|
+
/**
|
|
42993
|
+
* Wait for `promise`, but never longer than `budgetMs`.
|
|
42994
|
+
*
|
|
42995
|
+
* The promise is NOT cancelled — it keeps running, and its eventual rejection
|
|
42996
|
+
* is observed here so an overrun never becomes an unhandled rejection. The
|
|
42997
|
+
* caller decides what an overrun means; this only says that it happened.
|
|
42998
|
+
*/
|
|
42999
|
+
function raceBudget(promise, budgetMs) {
|
|
43000
|
+
return new Promise((resolve) => {
|
|
43001
|
+
let done = false;
|
|
43002
|
+
const timer = setTimeout(() => {
|
|
43003
|
+
if (done) return;
|
|
43004
|
+
done = true;
|
|
43005
|
+
resolve({ kind: "budget-exceeded" });
|
|
43006
|
+
}, budgetMs);
|
|
43007
|
+
timer.unref?.();
|
|
43008
|
+
promise.then((value) => {
|
|
43009
|
+
if (done) return;
|
|
43010
|
+
done = true;
|
|
43011
|
+
clearTimeout(timer);
|
|
43012
|
+
resolve({
|
|
43013
|
+
kind: "settled",
|
|
43014
|
+
value
|
|
43015
|
+
});
|
|
43016
|
+
}, (error) => {
|
|
43017
|
+
if (done) return;
|
|
43018
|
+
done = true;
|
|
43019
|
+
clearTimeout(timer);
|
|
43020
|
+
resolve({
|
|
43021
|
+
kind: "rejected",
|
|
43022
|
+
error
|
|
43023
|
+
});
|
|
43024
|
+
});
|
|
43025
|
+
});
|
|
43026
|
+
}
|
|
43027
|
+
//#endregion
|
|
42928
43028
|
//#region src/agent-load-service.ts
|
|
42929
43029
|
var AgentLoadService = class AgentLoadService {
|
|
42930
43030
|
deps;
|
|
@@ -42962,28 +43062,69 @@ var AgentLoadService = class AgentLoadService {
|
|
|
42962
43062
|
* collects every known runner, which is what the UI wants.
|
|
42963
43063
|
*/
|
|
42964
43064
|
async collectAgentLoad(options) {
|
|
42965
|
-
const loads = [];
|
|
42966
43065
|
const onlyEnabled = options?.onlyEnabled ?? false;
|
|
42967
|
-
|
|
42968
|
-
if (onlyEnabled
|
|
42969
|
-
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
|
|
42975
|
-
|
|
42976
|
-
|
|
42977
|
-
|
|
42978
|
-
|
|
42979
|
-
|
|
42980
|
-
|
|
42981
|
-
|
|
42982
|
-
|
|
43066
|
+
const nodeIds = this.deps.topology.knownRunnerNodeIds().filter((nodeId) => {
|
|
43067
|
+
if (!onlyEnabled || this.deps.topology.isNodeEnabled(nodeId)) return true;
|
|
43068
|
+
this.deps.logger.debug("runner excluded by enabledNodes whitelist", { tags: { nodeId } });
|
|
43069
|
+
return false;
|
|
43070
|
+
});
|
|
43071
|
+
const budgetMs = this.agentLoadBudgetMs();
|
|
43072
|
+
const entries = await Promise.all(nodeIds.map((nodeId) => this.censusNode(nodeId, budgetMs)));
|
|
43073
|
+
const loads = entries.flatMap((e) => e.load === null ? [] : [e.load]);
|
|
43074
|
+
const skipped = entries.filter((e) => e.load === null).map((e) => e.nodeId);
|
|
43075
|
+
if (skipped.length > 0) {
|
|
43076
|
+
const nodes = Object.fromEntries(entries.map((e) => [e.nodeId, {
|
|
43077
|
+
ms: e.ms,
|
|
43078
|
+
outcome: e.outcome,
|
|
43079
|
+
...e.error === void 0 ? {} : { error: e.error }
|
|
43080
|
+
}]));
|
|
43081
|
+
this.deps.logger.warn("agent load census skipped a runner", { meta: {
|
|
43082
|
+
budgetMs,
|
|
43083
|
+
skipped,
|
|
43084
|
+
nodes
|
|
43085
|
+
} });
|
|
42983
43086
|
}
|
|
42984
43087
|
this.refreshAgentLoadCache(loads);
|
|
42985
43088
|
return loads;
|
|
42986
43089
|
}
|
|
43090
|
+
/**
|
|
43091
|
+
* One runner's load under `budgetMs`. A wedged runner (transport up enough
|
|
43092
|
+
* to stay in the registry, but whose call neither resolves nor rejects) is
|
|
43093
|
+
* counted as `budget-exceeded` and skipped exactly like an offline one; the
|
|
43094
|
+
* in-flight call is left to settle on its own, as before.
|
|
43095
|
+
*/
|
|
43096
|
+
async censusNode(nodeId, budgetMs) {
|
|
43097
|
+
const api = this.deps.api();
|
|
43098
|
+
const startedAt = Date.now();
|
|
43099
|
+
if (!api) return {
|
|
43100
|
+
nodeId,
|
|
43101
|
+
ms: 0,
|
|
43102
|
+
outcome: "failed",
|
|
43103
|
+
load: null,
|
|
43104
|
+
error: "addon not initialized"
|
|
43105
|
+
};
|
|
43106
|
+
const outcome = await raceBudget(api.pipelineRunner.getLocalLoad.query({ nodeId }), budgetMs);
|
|
43107
|
+
const ms = Date.now() - startedAt;
|
|
43108
|
+
if (outcome.kind === "settled") return {
|
|
43109
|
+
nodeId,
|
|
43110
|
+
ms,
|
|
43111
|
+
outcome: "reported",
|
|
43112
|
+
load: outcome.value
|
|
43113
|
+
};
|
|
43114
|
+
if (outcome.kind === "rejected") return {
|
|
43115
|
+
nodeId,
|
|
43116
|
+
ms,
|
|
43117
|
+
outcome: "failed",
|
|
43118
|
+
load: null,
|
|
43119
|
+
error: errMsg(outcome.error)
|
|
43120
|
+
};
|
|
43121
|
+
return {
|
|
43122
|
+
nodeId,
|
|
43123
|
+
ms,
|
|
43124
|
+
outcome: "budget-exceeded",
|
|
43125
|
+
load: null
|
|
43126
|
+
};
|
|
43127
|
+
}
|
|
42987
43128
|
refreshAgentLoadCache(loads) {
|
|
42988
43129
|
const next = /* @__PURE__ */ new Map();
|
|
42989
43130
|
for (const load of loads) next.set(load.nodeId, {
|
|
@@ -43030,25 +43171,6 @@ var AgentLoadService = class AgentLoadService {
|
|
|
43030
43171
|
const value = (this.deps.readGlobalSettings() ?? {}).agentLoadTimeoutMs;
|
|
43031
43172
|
return typeof value === "number" && value > 0 ? value : AgentLoadService.DEFAULT_AGENT_LOAD_TIMEOUT_MS;
|
|
43032
43173
|
}
|
|
43033
|
-
/**
|
|
43034
|
-
* `getLocalLoad` for one runner node, bounded by `agentLoadBudgetMs`. On
|
|
43035
|
-
* timeout it rejects so `collectAgentLoad`'s catch treats the node exactly
|
|
43036
|
-
* like an offline one (logged at debug, skipped).
|
|
43037
|
-
*/
|
|
43038
|
-
async queryLocalLoadBounded(nodeId) {
|
|
43039
|
-
const api = this.deps.api();
|
|
43040
|
-
if (!api) throw new Error("queryLocalLoadBounded: addon not initialized");
|
|
43041
|
-
const budgetMs = this.agentLoadBudgetMs();
|
|
43042
|
-
let timer;
|
|
43043
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
43044
|
-
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`getLocalLoad exceeded ${budgetMs}ms budget for ${nodeId}`)), budgetMs);
|
|
43045
|
-
});
|
|
43046
|
-
try {
|
|
43047
|
-
return await Promise.race([api.pipelineRunner.getLocalLoad.query({ nodeId }), timeout]);
|
|
43048
|
-
} finally {
|
|
43049
|
-
if (timer) clearTimeout(timer);
|
|
43050
|
-
}
|
|
43051
|
-
}
|
|
43052
43174
|
/** Mirrors the former `onShutdown`'s `this.cachedAgentLoad.clear()`. */
|
|
43053
43175
|
dispose() {
|
|
43054
43176
|
this.cachedAgentLoad.clear();
|
|
@@ -50619,19 +50741,44 @@ var ReconcileController = class ReconcileController {
|
|
|
50619
50741
|
async runPlacementReconcile() {
|
|
50620
50742
|
const api = this.deps.api();
|
|
50621
50743
|
if (!api) return;
|
|
50744
|
+
const censusStartedAt = Date.now();
|
|
50622
50745
|
const runners = [];
|
|
50623
|
-
|
|
50624
|
-
|
|
50625
|
-
|
|
50746
|
+
const census = [];
|
|
50747
|
+
await Promise.all(this.deps.topology.collectRunnerNodeIds().map(async (nodeId) => {
|
|
50748
|
+
const startedAt = Date.now();
|
|
50749
|
+
const outcome = await raceBudget(api.pipelineRunner.getLocalCameras.query(void 0, nodePin(nodeId)), RECONCILE_NODE_BUDGET_MS);
|
|
50750
|
+
const ms = Date.now() - startedAt;
|
|
50751
|
+
if (outcome.kind === "settled") {
|
|
50752
|
+
runners.push({
|
|
50753
|
+
nodeId,
|
|
50754
|
+
attached: outcome.value
|
|
50755
|
+
});
|
|
50756
|
+
census.push({
|
|
50757
|
+
nodeId,
|
|
50758
|
+
ms,
|
|
50759
|
+
outcome: "reported"
|
|
50760
|
+
});
|
|
50761
|
+
return;
|
|
50762
|
+
}
|
|
50763
|
+
census.push({
|
|
50626
50764
|
nodeId,
|
|
50627
|
-
|
|
50765
|
+
ms,
|
|
50766
|
+
outcome: outcome.kind === "rejected" ? "failed" : "budget-exceeded"
|
|
50628
50767
|
});
|
|
50629
|
-
} catch (err) {
|
|
50630
50768
|
this.deps.logger.debug("placement reconcile skipped node — getLocalCameras failed", {
|
|
50631
50769
|
tags: { nodeId },
|
|
50632
|
-
meta: {
|
|
50770
|
+
meta: {
|
|
50771
|
+
error: outcome.kind === "rejected" ? errMsg(outcome.error) : "budget exceeded",
|
|
50772
|
+
ms
|
|
50773
|
+
}
|
|
50633
50774
|
});
|
|
50634
|
-
}
|
|
50775
|
+
}));
|
|
50776
|
+
const censusMs = Date.now() - censusStartedAt;
|
|
50777
|
+
if (censusMs >= 1e3 || census.some((entry) => entry.outcome !== "reported")) this.deps.logger.info("placement reconcile: runner census was slow", { meta: {
|
|
50778
|
+
censusMs,
|
|
50779
|
+
nodes: census,
|
|
50780
|
+
budgetMs: RECONCILE_NODE_BUDGET_MS
|
|
50781
|
+
} });
|
|
50635
50782
|
const { assignedNodeById, dispatching } = this.deps.ledger.snapshotForReconcile();
|
|
50636
50783
|
const idleSessionDeviceIds = new Set([...this.deps.activeDeviceIds()].filter((deviceId) => this.deps.isIdleSessionCamera(deviceId)));
|
|
50637
50784
|
const motionWatchAttachments = /* @__PURE__ */ new Set();
|
|
@@ -50645,7 +50792,7 @@ var ReconcileController = class ReconcileController {
|
|
|
50645
50792
|
});
|
|
50646
50793
|
if (detach.length > 0) {
|
|
50647
50794
|
this.deps.logger.info("placement reconcile: detaching orphans/duplicates", { meta: { detach } });
|
|
50648
|
-
|
|
50795
|
+
await Promise.all(detach.map(({ nodeId, deviceId }) => this.deps.detachOn(nodeId, deviceId).catch((err) => {
|
|
50649
50796
|
this.deps.logger.warn("placement reconcile: detach failed", {
|
|
50650
50797
|
tags: {
|
|
50651
50798
|
nodeId,
|
|
@@ -50653,7 +50800,7 @@ var ReconcileController = class ReconcileController {
|
|
|
50653
50800
|
},
|
|
50654
50801
|
meta: { error: errMsg(err) }
|
|
50655
50802
|
});
|
|
50656
|
-
});
|
|
50803
|
+
})));
|
|
50657
50804
|
}
|
|
50658
50805
|
if (dropStaleAssignment.length > 0) {
|
|
50659
50806
|
this.deps.logger.info("placement reconcile: dropping stale assignments", { meta: { deviceIds: dropStaleAssignment } });
|
|
@@ -51043,6 +51190,13 @@ var TtlMemo = class {
|
|
|
51043
51190
|
* declared wrong.
|
|
51044
51191
|
*/
|
|
51045
51192
|
generations = /* @__PURE__ */ new Map();
|
|
51193
|
+
/**
|
|
51194
|
+
* The last value ANY read produced for a key, kept across
|
|
51195
|
+
* {@link invalidate}. Never served by {@link get}; it is the bounded fallback
|
|
51196
|
+
* of {@link getWithin} alone (D441) — a caller that would rather balance on a
|
|
51197
|
+
* snapshot a few seconds old than wait seconds for the re-read.
|
|
51198
|
+
*/
|
|
51199
|
+
lastKnown = /* @__PURE__ */ new Map();
|
|
51046
51200
|
ttlMs;
|
|
51047
51201
|
now;
|
|
51048
51202
|
constructor(opts) {
|
|
@@ -51076,6 +51230,50 @@ var TtlMemo = class {
|
|
|
51076
51230
|
}
|
|
51077
51231
|
}
|
|
51078
51232
|
/**
|
|
51233
|
+
* The value for `key` within `budgetMs`: from memory when fresh, from `load`
|
|
51234
|
+
* when the read lands in time, and otherwise the LAST KNOWN value —
|
|
51235
|
+
* invalidated or not — with its age (D441).
|
|
51236
|
+
*
|
|
51237
|
+
* Three properties, each pinned by a test:
|
|
51238
|
+
* - a read that overruns the budget is NOT abandoned: it keeps running and
|
|
51239
|
+
* stores its result for the next caller, so a stale answer is served at
|
|
51240
|
+
* most once per read;
|
|
51241
|
+
* - a COLD key waits for the read past the budget, because a cold cache must
|
|
51242
|
+
* never be the reason a camera fails to dispatch (the `get` contract);
|
|
51243
|
+
* - a read that FAILS with a last-known value in hand serves it as stale.
|
|
51244
|
+
*/
|
|
51245
|
+
async getWithin(key, budgetMs) {
|
|
51246
|
+
const at = this.now();
|
|
51247
|
+
const entry = this.entries.get(key);
|
|
51248
|
+
if (entry) this.entries.set(key, {
|
|
51249
|
+
...entry,
|
|
51250
|
+
usedAt: at
|
|
51251
|
+
});
|
|
51252
|
+
if (entry && at - entry.readAt < this.ttlMs) return {
|
|
51253
|
+
value: entry.value,
|
|
51254
|
+
stale: false,
|
|
51255
|
+
ageMs: at - entry.readAt
|
|
51256
|
+
};
|
|
51257
|
+
const fallback = this.lastKnown.get(key);
|
|
51258
|
+
const pending = this.read(key);
|
|
51259
|
+
if (fallback === void 0) return {
|
|
51260
|
+
value: await pending,
|
|
51261
|
+
stale: false,
|
|
51262
|
+
ageMs: 0
|
|
51263
|
+
};
|
|
51264
|
+
const outcome = await raceBudget(pending, budgetMs);
|
|
51265
|
+
if (outcome.kind === "settled") return {
|
|
51266
|
+
value: outcome.value,
|
|
51267
|
+
stale: false,
|
|
51268
|
+
ageMs: 0
|
|
51269
|
+
};
|
|
51270
|
+
return {
|
|
51271
|
+
value: fallback.value,
|
|
51272
|
+
stale: true,
|
|
51273
|
+
ageMs: this.now() - fallback.readAt
|
|
51274
|
+
};
|
|
51275
|
+
}
|
|
51276
|
+
/**
|
|
51079
51277
|
* Declare the cached answer for `key` WRONG — not merely old.
|
|
51080
51278
|
*
|
|
51081
51279
|
* The caller has just changed the thing being cached (an attach or a detach
|
|
@@ -51120,6 +51318,7 @@ var TtlMemo = class {
|
|
|
51120
51318
|
this.entries.clear();
|
|
51121
51319
|
this.inFlight.clear();
|
|
51122
51320
|
this.generations.clear();
|
|
51321
|
+
this.lastKnown.clear();
|
|
51123
51322
|
}
|
|
51124
51323
|
/** One shared in-flight read per key; stores the result on success. */
|
|
51125
51324
|
read(key) {
|
|
@@ -51127,14 +51326,15 @@ var TtlMemo = class {
|
|
|
51127
51326
|
if (existing) return existing;
|
|
51128
51327
|
const generation = this.generations.get(key) ?? 0;
|
|
51129
51328
|
const pending = this.opts.load(key).then((value) => {
|
|
51130
|
-
if ((this.generations.get(key) ?? 0) !== generation) return value;
|
|
51131
51329
|
const at = this.now();
|
|
51132
|
-
const
|
|
51133
|
-
this.entries.set(key, {
|
|
51330
|
+
const stored = {
|
|
51134
51331
|
value,
|
|
51135
51332
|
readAt: at,
|
|
51136
|
-
usedAt:
|
|
51137
|
-
}
|
|
51333
|
+
usedAt: this.entries.get(key)?.usedAt ?? at
|
|
51334
|
+
};
|
|
51335
|
+
this.lastKnown.set(key, stored);
|
|
51336
|
+
if ((this.generations.get(key) ?? 0) !== generation) return value;
|
|
51337
|
+
this.entries.set(key, stored);
|
|
51138
51338
|
return value;
|
|
51139
51339
|
}).finally(() => {
|
|
51140
51340
|
if (this.inFlight.get(key) === pending) this.inFlight.delete(key);
|
|
@@ -51602,7 +51802,7 @@ var SessionDispatchController = class {
|
|
|
51602
51802
|
return;
|
|
51603
51803
|
}
|
|
51604
51804
|
this.activeRefireCountByDevice.delete(deviceId);
|
|
51605
|
-
await this.dispatchDetectionSession(deviceId, cur, trigger);
|
|
51805
|
+
const timings = await this.dispatchDetectionSession(deviceId, cur, trigger);
|
|
51606
51806
|
if (this.sessionRegistry.has(deviceId)) this.scheduleSessionTeardown(deviceId, cooldownMs);
|
|
51607
51807
|
const doneAt = Date.now();
|
|
51608
51808
|
this.deps.logger.info("session motion → attach latency", {
|
|
@@ -51611,6 +51811,7 @@ var SessionDispatchController = class {
|
|
|
51611
51811
|
...busLagMs !== void 0 ? { busLagMs } : {},
|
|
51612
51812
|
lockWaitMs: lockAcquiredAt - lockRequestedAt,
|
|
51613
51813
|
dispatchMs: doneAt - lockAcquiredAt,
|
|
51814
|
+
...timings,
|
|
51614
51815
|
totalMs: doneAt - receivedAt,
|
|
51615
51816
|
...emittedAt !== void 0 ? { sinceMotionMs: doneAt - emittedAt } : {}
|
|
51616
51817
|
}
|
|
@@ -51643,9 +51844,32 @@ var SessionDispatchController = class {
|
|
|
51643
51844
|
*/
|
|
51644
51845
|
async dispatchDetectionSession(deviceId, config, trigger) {
|
|
51645
51846
|
const log = this.deps.logger.withTags({ deviceId });
|
|
51646
|
-
|
|
51847
|
+
const reconcileStartedAt = Date.now();
|
|
51848
|
+
const census = this.deps.reconcilePlacementFromRunners();
|
|
51849
|
+
const censusOutcome = await raceBudget(census, 500);
|
|
51850
|
+
const reconcileMs = Date.now() - reconcileStartedAt;
|
|
51851
|
+
const reconcileBudgetExceeded = censusOutcome.kind === "budget-exceeded";
|
|
51852
|
+
const dispatchGuard = this.deps.ledger.markDispatching(deviceId);
|
|
51853
|
+
const releaseGuard = () => {
|
|
51854
|
+
if (!reconcileBudgetExceeded) {
|
|
51855
|
+
dispatchGuard.dispose();
|
|
51856
|
+
return;
|
|
51857
|
+
}
|
|
51858
|
+
census.finally(() => dispatchGuard.dispose());
|
|
51859
|
+
};
|
|
51860
|
+
if (reconcileBudgetExceeded) log.warn("detection session: placement census overran its budget — placing without it", { meta: { budgetMs: 500 } });
|
|
51861
|
+
else if (censusOutcome.kind === "rejected") log.warn("detection session: placement census failed — placing on the ledger as it is", { meta: {
|
|
51862
|
+
error: errMsg(censusOutcome.error),
|
|
51863
|
+
reconcileMs
|
|
51864
|
+
} });
|
|
51865
|
+
const balanceStartedAt = Date.now();
|
|
51647
51866
|
const preferredAgent = await this.deps.readPipelinePin(deviceId);
|
|
51648
|
-
const
|
|
51867
|
+
const loadAnswer = await this.agentLoads.getWithin(CLUSTER_KEY, 500);
|
|
51868
|
+
const loads = loadAnswer.value;
|
|
51869
|
+
if (loadAnswer.stale) log.warn("detection session: agent load read overran its budget — balancing on the last known load", { meta: {
|
|
51870
|
+
budgetMs: 500,
|
|
51871
|
+
loadAgeMs: loadAnswer.ageMs
|
|
51872
|
+
} });
|
|
51649
51873
|
const allEligible = this.deps.topology.detectionEligibleNodes(deviceId);
|
|
51650
51874
|
const usableEligible = allEligible.filter((n) => this.deps.isNodeInferenceUsable(n));
|
|
51651
51875
|
const eligible = usableEligible.length > 0 ? usableEligible : allEligible;
|
|
@@ -51659,9 +51883,11 @@ var SessionDispatchController = class {
|
|
|
51659
51883
|
});
|
|
51660
51884
|
if (!decision || decision.kind !== "assigned") {
|
|
51661
51885
|
log.warn("detection session: no eligible node — will retry on next motion", { meta: { reason: decision?.kind === "pending" ? decision.reason : "no-runners-online" } });
|
|
51662
|
-
|
|
51886
|
+
releaseGuard();
|
|
51887
|
+
return null;
|
|
51663
51888
|
}
|
|
51664
51889
|
const targetNodeId = decision.agentNodeId;
|
|
51890
|
+
const balancedAt = Date.now();
|
|
51665
51891
|
const enabledDevices = await this.eligibleInferenceDevices.get(targetNodeId);
|
|
51666
51892
|
const targetLoad = loads.find((n) => n.nodeId === targetNodeId);
|
|
51667
51893
|
const deviceCaps = await this.inferenceDeviceCaps.get(targetNodeId);
|
|
@@ -51691,6 +51917,7 @@ var SessionDispatchController = class {
|
|
|
51691
51917
|
...pinnedDeviceKey ? { pin: pinnedDeviceKey } : {}
|
|
51692
51918
|
}
|
|
51693
51919
|
});
|
|
51920
|
+
const deviceSelectedAt = Date.now();
|
|
51694
51921
|
const pipelineConfig = await this.deps.settingsStore.resolvePipelineForDevice(deviceId, targetNodeId, deviceKey);
|
|
51695
51922
|
const deviceBase = await this.deps.settingsStore.buildNodeInferenceDeviceSteps(targetNodeId, deviceKey ?? "");
|
|
51696
51923
|
const deviceOverride = deviceKey ? await this.deps.settingsStore.readCameraDeviceOverride(deviceId, targetNodeId, deviceKey) : {};
|
|
@@ -51726,9 +51953,11 @@ var SessionDispatchController = class {
|
|
|
51726
51953
|
preRollInferenceEnabled: config.preRollInferenceEnabled,
|
|
51727
51954
|
preRollInferenceFrames: config.preRollInferenceFrames
|
|
51728
51955
|
};
|
|
51729
|
-
const
|
|
51956
|
+
const configuredAt = Date.now();
|
|
51957
|
+
let attachedAt = configuredAt;
|
|
51730
51958
|
try {
|
|
51731
51959
|
await this.deps.attachOn(targetNodeId, sessionConfig);
|
|
51960
|
+
attachedAt = Date.now();
|
|
51732
51961
|
this.deps.recordAssignment(deviceId, targetNodeId, decision.reason, decision.reason === "manual");
|
|
51733
51962
|
this.sessionRegistry.register(deviceId, targetNodeId, Date.now(), deviceKey);
|
|
51734
51963
|
log.info("detection session: dispatched on motion", {
|
|
@@ -51744,9 +51973,18 @@ var SessionDispatchController = class {
|
|
|
51744
51973
|
}
|
|
51745
51974
|
});
|
|
51746
51975
|
} finally {
|
|
51747
|
-
|
|
51976
|
+
releaseGuard();
|
|
51748
51977
|
this.notePlacementChanged();
|
|
51749
51978
|
}
|
|
51979
|
+
return {
|
|
51980
|
+
reconcileMs,
|
|
51981
|
+
reconcileBudgetExceeded,
|
|
51982
|
+
balanceMs: balancedAt - balanceStartedAt,
|
|
51983
|
+
balanceLoadStale: loadAnswer.stale,
|
|
51984
|
+
deviceSelectMs: deviceSelectedAt - balancedAt,
|
|
51985
|
+
configMs: configuredAt - deviceSelectedAt,
|
|
51986
|
+
attachMs: attachedAt - configuredAt
|
|
51987
|
+
};
|
|
51750
51988
|
}
|
|
51751
51989
|
/**
|
|
51752
51990
|
* Schedule a session teardown after `cooldownMs` of no rising-edge motion.
|