@camstack/addon-pipeline 1.2.130 → 1.2.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{addon-utils-DH78p79i.js → addon-utils-DCQ5AP_v.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +332 -20
- package/dist/detection-pipeline/index.mjs +330 -18
- package/dist/{dist-DOw-izoI.js → dist-CqoMNE1e.js} +1817 -1712
- package/dist/{dist-Wt81pN5Y.mjs → dist-DO0UVhOk.mjs} +1817 -1712
- package/dist/{event-loop-stall-monitor-CtQCOZqm.mjs → event-loop-stall-monitor-D0Juf7Dn.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-Bld7jNCb.js → event-loop-stall-monitor-eM7_yYfn.js} +1 -1
- package/dist/{lazy-sharp-BbNI2ZD-.js → lazy-sharp-BGrpS8yi.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-1_D3EZT9.mjs → process-memory-Bac3FuG6.mjs} +1 -1
- package/dist/{process-memory-D9H5qjDl.js → process-memory-xAPjvSw9.js} +1 -1
- package/dist/recorder/index.js +95 -4
- package/dist/recorder/index.mjs +94 -3
- package/dist/{segment-demux-js-C2jBBqGW.js → segment-demux-js-Cu0oDMUL.js} +1 -1
- package/dist/{segment-demux-js-BW-Q-j2A.mjs → segment-demux-js-skhsAZx6.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dw-iwP4y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DGQkDEYw.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D36MoVV1.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CQKwHIHI.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-DEVCEYgu.mjs → hostInit-D7FwwZgj.mjs} +3 -3
- package/dist/stream-broker/index.js +2 -2
- package/dist/stream-broker/index.mjs +2 -2
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-nAZl_Rsv.js → worker-protocol-G3HXTY8z.js} +1 -1
- package/dist/{worker-protocol-rpnr7Ldd.mjs → worker-protocol-_Ujvj1hl.mjs} +1 -1
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_process_memory = require("../process-memory-
|
|
7
|
-
const require_addon_utils = require("../addon-utils-
|
|
5
|
+
const require_dist = require("../dist-CqoMNE1e.js");
|
|
6
|
+
const require_process_memory = require("../process-memory-xAPjvSw9.js");
|
|
7
|
+
const require_addon_utils = require("../addon-utils-DCQ5AP_v.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_dist.__toESM(node_path);
|
|
10
10
|
let node_fs = require("node:fs");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { A as audioAnalyzerCapability, Dt as hydrateSchema, J as mapAudioLabelToMacro, St as BaseAddon, _ as HF_BASE_URL, gt as errMsg, k as audioAnalysisCapability, lt as resolvePoolMemoryPolicy, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, x as PoolMemoryWatchdog } from "../dist-
|
|
3
|
-
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-
|
|
2
|
+
import { A as audioAnalyzerCapability, Dt as hydrateSchema, J as mapAudioLabelToMacro, St as BaseAddon, _ as HF_BASE_URL, gt as errMsg, k as audioAnalysisCapability, lt as resolvePoolMemoryPolicy, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, x as PoolMemoryWatchdog } from "../dist-DO0UVhOk.mjs";
|
|
3
|
+
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-Bac3FuG6.mjs";
|
|
4
4
|
import { i as downloadFile } from "../addon-utils-CgBCF-mE.mjs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import * as fs from "node:fs";
|
|
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-
|
|
7
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
8
|
-
const require_process_memory = require("../process-memory-
|
|
9
|
-
const require_addon_utils = require("../addon-utils-
|
|
5
|
+
const require_dist = require("../dist-CqoMNE1e.js");
|
|
6
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-eM7_yYfn.js");
|
|
7
|
+
const require_lazy_sharp = require("../lazy-sharp-BGrpS8yi.js");
|
|
8
|
+
const require_process_memory = require("../process-memory-xAPjvSw9.js");
|
|
9
|
+
const require_addon_utils = require("../addon-utils-DCQ5AP_v.js");
|
|
10
10
|
let sharp = require("sharp");
|
|
11
11
|
sharp = require_dist.__toESM(sharp);
|
|
12
12
|
let node_child_process = require("node:child_process");
|
|
@@ -908,15 +908,20 @@ var PoolWorker = class {
|
|
|
908
908
|
} });
|
|
909
909
|
this.rejectAll(err);
|
|
910
910
|
});
|
|
911
|
-
this.process
|
|
912
|
-
|
|
913
|
-
this.log.error("Worker process exited", { meta: {
|
|
914
|
-
worker: this.opts.workerLabel,
|
|
915
|
-
code
|
|
916
|
-
} });
|
|
917
|
-
this.rejectAll(/* @__PURE__ */ new Error(`Worker process exited with code ${code}`));
|
|
918
|
-
}
|
|
911
|
+
const spawnedProcess = this.process;
|
|
912
|
+
spawnedProcess.on("exit", (code, signal) => {
|
|
919
913
|
this.ready = false;
|
|
914
|
+
if (this.process !== spawnedProcess) return;
|
|
915
|
+
this.log.error("Worker process exited", { meta: {
|
|
916
|
+
worker: this.opts.workerLabel,
|
|
917
|
+
pid: spawnedProcess.pid ?? null,
|
|
918
|
+
runtime: this.opts.poolRuntime,
|
|
919
|
+
device: this.opts.device ?? "default",
|
|
920
|
+
code,
|
|
921
|
+
signal,
|
|
922
|
+
inFlight: this.pending.size
|
|
923
|
+
} });
|
|
924
|
+
this.rejectAll(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: worker process exited (code=${code ?? "null"}, signal=${signal ?? "none"})`));
|
|
920
925
|
});
|
|
921
926
|
this.process.stdout.on("data", (chunk) => {
|
|
922
927
|
const t0 = Date.now();
|
|
@@ -1044,6 +1049,7 @@ var PoolWorker = class {
|
|
|
1044
1049
|
this.process = null;
|
|
1045
1050
|
this.ready = false;
|
|
1046
1051
|
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
1052
|
+
this.rejectAll(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: pool disposed while the request was in flight`));
|
|
1047
1053
|
}
|
|
1048
1054
|
/**
|
|
1049
1055
|
* Shed an inference request when the worker already has {@link
|
|
@@ -2356,6 +2362,23 @@ var EngineFactory = class {
|
|
|
2356
2362
|
active: l.active
|
|
2357
2363
|
}));
|
|
2358
2364
|
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Is this factory's pool alive and able to answer?
|
|
2367
|
+
*
|
|
2368
|
+
* `SharedInferencePool.isReady()` and `PoolWorker.isReady()` have always
|
|
2369
|
+
* existed; this factory surfaced `getPoolPids()` and `getPoolBacklog()` and
|
|
2370
|
+
* **not** readiness, so the one signal every caller needed sat one layer
|
|
2371
|
+
* below all of them and was never lifted. That is why a pool whose Python
|
|
2372
|
+
* worker was SIGABRT'd 1.3 s after `Pool ready` was handed back to 370 000
|
|
2373
|
+
* consecutive dispatches over 31 hours: `resolveDeviceFactory` had nothing
|
|
2374
|
+
* to ask.
|
|
2375
|
+
*
|
|
2376
|
+
* A factory that never initialised answers `false` — a pool that is not
|
|
2377
|
+
* there cannot serve a frame, and the caller's job is the same either way.
|
|
2378
|
+
*/
|
|
2379
|
+
isReady() {
|
|
2380
|
+
return this.pool?.isReady() ?? false;
|
|
2381
|
+
}
|
|
2359
2382
|
/** Native pid of the underlying Python pool, if any. */
|
|
2360
2383
|
getPoolPid() {
|
|
2361
2384
|
return this.pool?.getPid() ?? null;
|
|
@@ -2662,24 +2685,160 @@ var IdlePoolReaper = class {
|
|
|
2662
2685
|
}
|
|
2663
2686
|
};
|
|
2664
2687
|
//#endregion
|
|
2688
|
+
//#region src/detection-pipeline/engine/inference-device-liveness.ts
|
|
2689
|
+
/**
|
|
2690
|
+
* A dispatch refused because its inference device is not usable.
|
|
2691
|
+
*
|
|
2692
|
+
* Distinct from `PoolWorker[w0]: not initialized`, which said the same thing
|
|
2693
|
+
* for three different states and so distinguished none of them. This one names
|
|
2694
|
+
* the device, the state and the underlying death, so a single rejected frame
|
|
2695
|
+
* is decidable without correlating logs.
|
|
2696
|
+
*/
|
|
2697
|
+
var InferenceDeviceUnusableError = class extends Error {
|
|
2698
|
+
deviceKey;
|
|
2699
|
+
state;
|
|
2700
|
+
constructor(unhealthy) {
|
|
2701
|
+
super(`inference device "${unhealthy.deviceKey}" is ${unhealthy.state} after ${unhealthy.deaths} pool death(s): ${unhealthy.lastError}`);
|
|
2702
|
+
this.name = "InferenceDeviceUnusableError";
|
|
2703
|
+
this.deviceKey = unhealthy.deviceKey;
|
|
2704
|
+
this.state = unhealthy.state;
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
/**
|
|
2708
|
+
* Defaults. Three deaths inside ten minutes trips the breaker, so a
|
|
2709
|
+
* deterministic crash costs **three** Python spawns instead of one per frame,
|
|
2710
|
+
* and a flaky-once-an-hour pool still self-heals forever.
|
|
2711
|
+
*/
|
|
2712
|
+
var DEFAULT_INFERENCE_DEVICE_LIVENESS = {
|
|
2713
|
+
windowMs: 600 * 1e3,
|
|
2714
|
+
maxDeaths: 3,
|
|
2715
|
+
maxBackoffMs: 3e4
|
|
2716
|
+
};
|
|
2717
|
+
var InferenceDeviceLivenessSupervisor = class {
|
|
2718
|
+
opts;
|
|
2719
|
+
state = /* @__PURE__ */ new Map();
|
|
2720
|
+
constructor(opts = DEFAULT_INFERENCE_DEVICE_LIVENESS) {
|
|
2721
|
+
this.opts = opts;
|
|
2722
|
+
}
|
|
2723
|
+
/**
|
|
2724
|
+
* Record one pool death for `deviceKey` and decide what happens next.
|
|
2725
|
+
* `now` is injectable for deterministic tests. Never throws.
|
|
2726
|
+
*/
|
|
2727
|
+
recordDeath(deviceKey, error, now = Date.now()) {
|
|
2728
|
+
const cutoff = now - this.opts.windowMs;
|
|
2729
|
+
const recent = [...(this.state.get(deviceKey)?.deaths ?? []).filter((t) => t >= cutoff), now];
|
|
2730
|
+
if (recent.length >= this.opts.maxDeaths) {
|
|
2731
|
+
this.state.set(deviceKey, {
|
|
2732
|
+
deaths: recent,
|
|
2733
|
+
failed: true,
|
|
2734
|
+
retryAtMs: Number.POSITIVE_INFINITY,
|
|
2735
|
+
lastError: error,
|
|
2736
|
+
since: now
|
|
2737
|
+
});
|
|
2738
|
+
return {
|
|
2739
|
+
action: "failed",
|
|
2740
|
+
deathsInWindow: recent.length
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
const backoffMs = Math.min(this.opts.maxBackoffMs, 500 * 2 ** Math.min(6, Math.max(0, recent.length - 1)));
|
|
2744
|
+
this.state.set(deviceKey, {
|
|
2745
|
+
deaths: recent,
|
|
2746
|
+
failed: false,
|
|
2747
|
+
retryAtMs: now + backoffMs,
|
|
2748
|
+
lastError: error,
|
|
2749
|
+
since: now
|
|
2750
|
+
});
|
|
2751
|
+
return {
|
|
2752
|
+
action: "rebuild",
|
|
2753
|
+
backoffMs,
|
|
2754
|
+
deathsInWindow: recent.length
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
/** Has the breaker tripped for this device? */
|
|
2758
|
+
isFailed(deviceKey) {
|
|
2759
|
+
return this.state.get(deviceKey)?.failed ?? false;
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Why this device cannot be used right now, or `null` when it may be
|
|
2763
|
+
* (re)built. A device inside its backoff answers `'backoff'`; a device whose
|
|
2764
|
+
* budget is spent answers `'failed'` forever.
|
|
2765
|
+
*/
|
|
2766
|
+
unusableReason(deviceKey, now = Date.now()) {
|
|
2767
|
+
const s = this.state.get(deviceKey);
|
|
2768
|
+
if (!s) return null;
|
|
2769
|
+
if (s.failed) return {
|
|
2770
|
+
deviceKey,
|
|
2771
|
+
state: "failed",
|
|
2772
|
+
since: s.since,
|
|
2773
|
+
deaths: s.deaths.length,
|
|
2774
|
+
lastError: s.lastError
|
|
2775
|
+
};
|
|
2776
|
+
if (now < s.retryAtMs) return {
|
|
2777
|
+
deviceKey,
|
|
2778
|
+
state: "backoff",
|
|
2779
|
+
since: s.since,
|
|
2780
|
+
deaths: s.deaths.length,
|
|
2781
|
+
lastError: s.lastError
|
|
2782
|
+
};
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Every device the breaker currently refuses, sorted by key.
|
|
2787
|
+
*
|
|
2788
|
+
* This is what the health channel publishes. `'backoff'` entries are
|
|
2789
|
+
* included deliberately: a device mid-backoff cannot serve a frame either,
|
|
2790
|
+
* and the reader's own arm/apply reluctance (D49) is what keeps a single
|
|
2791
|
+
* transient observation from excluding it.
|
|
2792
|
+
*/
|
|
2793
|
+
unhealthy(now = Date.now()) {
|
|
2794
|
+
const out = [];
|
|
2795
|
+
for (const deviceKey of [...this.state.keys()].toSorted()) {
|
|
2796
|
+
const reason = this.unusableReason(deviceKey, now);
|
|
2797
|
+
if (reason) out.push(reason);
|
|
2798
|
+
}
|
|
2799
|
+
return out;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Operator re-arm: forget everything about `deviceKey` so the next dispatch
|
|
2803
|
+
* builds a fresh pool with a full budget. Returns whether there was anything
|
|
2804
|
+
* to forget — a terminal state nobody can clear is a silent fault, and this
|
|
2805
|
+
* is the clearing.
|
|
2806
|
+
*/
|
|
2807
|
+
rearm(deviceKey) {
|
|
2808
|
+
return this.state.delete(deviceKey);
|
|
2809
|
+
}
|
|
2810
|
+
/** Forget every device (shutdown / full engine re-spin). */
|
|
2811
|
+
reset() {
|
|
2812
|
+
this.state.clear();
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
//#endregion
|
|
2665
2816
|
//#region src/detection-pipeline/engine/pool-memory-guard.ts
|
|
2666
2817
|
function createDetectionPoolMemoryGuard(source, logger) {
|
|
2667
2818
|
const log = logger.child("pool-memory");
|
|
2819
|
+
/** Pools already reported dead — the `pool is DEAD` line fires on the EDGE.
|
|
2820
|
+
* A pool nobody dispatches to is never condemned, so without this gate one
|
|
2821
|
+
* idle corpse writes an ERROR every sweep forever, which is how a real
|
|
2822
|
+
* signal becomes noise. Cleared when the pool comes back or goes away. */
|
|
2823
|
+
const reportedDead = /* @__PURE__ */ new Set();
|
|
2668
2824
|
return new require_dist.PoolMemoryWatchdog({
|
|
2669
2825
|
policy: require_dist.resolvePoolMemoryPolicy(process.env),
|
|
2670
2826
|
log,
|
|
2671
2827
|
restart: (key) => source.restartPool(key),
|
|
2672
2828
|
sample: async () => {
|
|
2673
2829
|
const out = [];
|
|
2830
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2674
2831
|
for (const pool of source.listPools()) {
|
|
2675
|
-
|
|
2832
|
+
seen.add(pool.key);
|
|
2833
|
+
const telemetry = await samplePool(pool, log, reportedDead);
|
|
2676
2834
|
if (telemetry) out.push(telemetry);
|
|
2677
2835
|
}
|
|
2836
|
+
for (const key of [...reportedDead]) if (!seen.has(key)) reportedDead.delete(key);
|
|
2678
2837
|
return out;
|
|
2679
2838
|
}
|
|
2680
2839
|
});
|
|
2681
2840
|
}
|
|
2682
|
-
async function samplePool(pool, log) {
|
|
2841
|
+
async function samplePool(pool, log, reportedDead) {
|
|
2683
2842
|
const pids = pool.factory.getPoolPids();
|
|
2684
2843
|
if (pids.length === 0) return null;
|
|
2685
2844
|
let maxRssBytes = 0;
|
|
@@ -2703,7 +2862,18 @@ async function samplePool(pool, log) {
|
|
|
2703
2862
|
threads: mem.threads
|
|
2704
2863
|
});
|
|
2705
2864
|
}
|
|
2706
|
-
if (workers.length === 0)
|
|
2865
|
+
if (workers.length === 0) {
|
|
2866
|
+
if (!pool.factory.isReady()) {
|
|
2867
|
+
if (!reportedDead.has(pool.key)) {
|
|
2868
|
+
reportedDead.add(pool.key);
|
|
2869
|
+
log.error("inference pool is DEAD — every worker pid is gone and the pool is not ready", { meta: {
|
|
2870
|
+
poolKey: pool.key,
|
|
2871
|
+
pids
|
|
2872
|
+
} });
|
|
2873
|
+
}
|
|
2874
|
+
} else reportedDead.delete(pool.key);
|
|
2875
|
+
return null;
|
|
2876
|
+
}
|
|
2707
2877
|
const backlog = pool.factory.getPoolBacklog();
|
|
2708
2878
|
const memStats = await pool.factory.getPoolMemStats().catch((err) => err instanceof Error ? err.message : String(err));
|
|
2709
2879
|
return {
|
|
@@ -8408,7 +8578,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
8408
8578
|
* If global steps exist, initializes with them. Otherwise, creates an empty pool.
|
|
8409
8579
|
*/
|
|
8410
8580
|
async ensureEngineFactory() {
|
|
8411
|
-
|
|
8581
|
+
const defaultDeviceKey = deviceKeyOf(this.currentEngine);
|
|
8582
|
+
this.refuseIfDeviceUnusable(defaultDeviceKey);
|
|
8583
|
+
if (this.engineFactory) {
|
|
8584
|
+
if (this.engineFactory.isReady()) return;
|
|
8585
|
+
const dead = this.engineFactory;
|
|
8586
|
+
this.engineFactory = null;
|
|
8587
|
+
this.noteDeviceDeath(defaultDeviceKey, "pool worker is not ready");
|
|
8588
|
+
await dead.dispose().catch(() => void 0);
|
|
8589
|
+
this.refuseIfDeviceUnusable(defaultDeviceKey);
|
|
8590
|
+
}
|
|
8412
8591
|
const inflight = this.engineFactoryInflight;
|
|
8413
8592
|
if (inflight) {
|
|
8414
8593
|
await inflight;
|
|
@@ -8430,11 +8609,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
8430
8609
|
} catch (err) {
|
|
8431
8610
|
await factory.dispose().catch(() => void 0);
|
|
8432
8611
|
this.log.error("Node-default pool failed to initialize", { meta: {
|
|
8433
|
-
deviceKey:
|
|
8612
|
+
deviceKey: defaultDeviceKey,
|
|
8434
8613
|
backend: this.currentEngine.backend,
|
|
8435
8614
|
device: this.currentEngine.device ?? null,
|
|
8436
8615
|
error: require_dist.errMsg(err)
|
|
8437
8616
|
} });
|
|
8617
|
+
this.noteDeviceDeath(defaultDeviceKey, require_dist.errMsg(err));
|
|
8438
8618
|
throw err;
|
|
8439
8619
|
}
|
|
8440
8620
|
this.engineFactory = factory;
|
|
@@ -8483,16 +8663,147 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
8483
8663
|
* fail (the loser can't claim the already-held device).
|
|
8484
8664
|
*/
|
|
8485
8665
|
deviceFactoryInflight = /* @__PURE__ */ new Map();
|
|
8666
|
+
/**
|
|
8667
|
+
* Per-`deviceKey` restart budget for the Python pools (D6 discipline).
|
|
8668
|
+
*
|
|
8669
|
+
* In-memory and per-runner ON PURPOSE. The terminal state means "this
|
|
8670
|
+
* accelerator aborted N times in a row a few minutes ago"; a runner respawn
|
|
8671
|
+
* re-derives it in three dispatches if it is still true, and persisting it
|
|
8672
|
+
* would outlive the driver update that fixes it. The operator's explicit
|
|
8673
|
+
* re-arm is {@link rearmInferenceDevice}, and the state is never invisible:
|
|
8674
|
+
* it is published on {@link getInferenceDeviceHealth}.
|
|
8675
|
+
*/
|
|
8676
|
+
deviceLiveness = new InferenceDeviceLivenessSupervisor();
|
|
8677
|
+
/**
|
|
8678
|
+
* Throw if `deviceKey` is currently refused — before a single Python process
|
|
8679
|
+
* is spawned, a model index is allocated, or a frame is copied.
|
|
8680
|
+
*
|
|
8681
|
+
* The cost of NOT asking, measured: each frame of the 2026-08-25 storm ran
|
|
8682
|
+
* the full `models to load` → `allocateIndex` → `loadModel` → throw cycle at
|
|
8683
|
+
* 13 Hz for 31 hours, leaking one model index per frame — and past 254
|
|
8684
|
+
* (`encodeModelByte`) that pool could never have served inference again even
|
|
8685
|
+
* if its worker had come back.
|
|
8686
|
+
*/
|
|
8687
|
+
refuseIfDeviceUnusable(deviceKey) {
|
|
8688
|
+
const unhealthy = this.deviceLiveness.unusableReason(deviceKey);
|
|
8689
|
+
if (unhealthy) throw new InferenceDeviceUnusableError(unhealthy);
|
|
8690
|
+
}
|
|
8691
|
+
/**
|
|
8692
|
+
* Record one pool death against the budget and SAY what was decided.
|
|
8693
|
+
*
|
|
8694
|
+
* One line per state change, not one per dispatch: the refusals that follow
|
|
8695
|
+
* are already carried by the rejected frame's own error. The `failed`
|
|
8696
|
+
* transition is an ERROR because it is the terminal state — an operator who
|
|
8697
|
+
* never sees this line has a silently half-dead node, which is the failure
|
|
8698
|
+
* this whole path exists to end.
|
|
8699
|
+
*/
|
|
8700
|
+
noteDeviceDeath(deviceKey, reason) {
|
|
8701
|
+
const decision = this.deviceLiveness.recordDeath(deviceKey, reason);
|
|
8702
|
+
if (decision.action === "failed") {
|
|
8703
|
+
this.log.error("inference device FAILED — restart budget exhausted, no pool will be spawned", { meta: {
|
|
8704
|
+
deviceKey,
|
|
8705
|
+
deathsInWindow: decision.deathsInWindow,
|
|
8706
|
+
reason,
|
|
8707
|
+
rearm: "pipelineExecutor.rearmInferenceDevice"
|
|
8708
|
+
} });
|
|
8709
|
+
return;
|
|
8710
|
+
}
|
|
8711
|
+
this.log.warn("inference pool died — rebuilding under the restart budget", { meta: {
|
|
8712
|
+
deviceKey,
|
|
8713
|
+
deathsInWindow: decision.deathsInWindow,
|
|
8714
|
+
backoffMs: decision.backoffMs,
|
|
8715
|
+
reason
|
|
8716
|
+
} });
|
|
8717
|
+
}
|
|
8718
|
+
/**
|
|
8719
|
+
* Retire a dead per-device pool: drop it from the map FIRST (so a concurrent
|
|
8720
|
+
* dispatch rebuilds fresh through the single-flight guard rather than
|
|
8721
|
+
* grabbing the corpse), cancel its idle timer, charge the budget, then
|
|
8722
|
+
* dispose — which rejects whatever was still in flight on it with a reason
|
|
8723
|
+
* rather than letting those requests sit until their own deadlines.
|
|
8724
|
+
*/
|
|
8725
|
+
async condemnDeviceFactory(deviceKey, factory, reason) {
|
|
8726
|
+
if (this.factoriesByDevice.get(deviceKey) === factory) {
|
|
8727
|
+
this.factoriesByDevice.delete(deviceKey);
|
|
8728
|
+
this.deviceReaper.cancel(deviceKey);
|
|
8729
|
+
this.noteDeviceDeath(deviceKey, reason);
|
|
8730
|
+
}
|
|
8731
|
+
await factory.dispose().catch(() => void 0);
|
|
8732
|
+
}
|
|
8733
|
+
/**
|
|
8734
|
+
* Every inference device this node currently refuses, and why.
|
|
8735
|
+
*
|
|
8736
|
+
* The CHANNEL the 2026-08-26 analysis found missing. Pool health lived
|
|
8737
|
+
* entirely inside this addon and reached no routing decision at any level:
|
|
8738
|
+
* the capability gate is keyed on model FORMAT and so is permanently blind
|
|
8739
|
+
* between `openvino:gpu` and `openvino:npu`, and the orchestrator's live
|
|
8740
|
+
* probe answers about HARDWARE, which was present the whole time. So the
|
|
8741
|
+
* balancer kept handing this node cameras by rotation — 20 sessions onto a
|
|
8742
|
+
* dead pool in 20 minutes — and every one of them lost its frames.
|
|
8743
|
+
*
|
|
8744
|
+
* Two sources, deliberately merged into one answer:
|
|
8745
|
+
* - the breaker's own `failed` / `backoff` states, and
|
|
8746
|
+
* - any factory currently cached but NOT ready. A pool nobody has dispatched
|
|
8747
|
+
* to since it died has not yet been condemned by the dispatch path, and a
|
|
8748
|
+
* device with no traffic is exactly the one that must stop being chosen
|
|
8749
|
+
* BEFORE the traffic arrives. Charging the budget is the dispatch path's
|
|
8750
|
+
* job; reporting is free and costs no spawn.
|
|
8751
|
+
*
|
|
8752
|
+
* Read-only and synchronous over in-memory state: it never probes, never
|
|
8753
|
+
* spawns, and never throws — the reader's whole safety argument rests on a
|
|
8754
|
+
* failed read changing nothing, and a read that can fail for its own reasons
|
|
8755
|
+
* would poison that.
|
|
8756
|
+
*/
|
|
8757
|
+
async getInferenceDeviceHealth() {
|
|
8758
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
8759
|
+
for (const entry of this.deviceLiveness.unhealthy()) byKey.set(entry.deviceKey, entry);
|
|
8760
|
+
const live = [...this.factoriesByDevice, ...this.engineFactory ? [[deviceKeyOf(this.currentEngine), this.engineFactory]] : []];
|
|
8761
|
+
for (const [deviceKey, factory] of live) {
|
|
8762
|
+
if (factory.isReady() || byKey.has(deviceKey)) continue;
|
|
8763
|
+
byKey.set(deviceKey, {
|
|
8764
|
+
deviceKey,
|
|
8765
|
+
state: "backoff",
|
|
8766
|
+
since: Date.now(),
|
|
8767
|
+
deaths: 0,
|
|
8768
|
+
lastError: "pool worker is not ready"
|
|
8769
|
+
});
|
|
8770
|
+
}
|
|
8771
|
+
return { unhealthy: [...byKey.values()].toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey)) };
|
|
8772
|
+
}
|
|
8773
|
+
/**
|
|
8774
|
+
* Operator re-arm of a terminally failed inference device: forget the budget
|
|
8775
|
+
* so the next dispatch builds a fresh pool.
|
|
8776
|
+
*
|
|
8777
|
+
* A terminal state with no way out is a silent fault dressed as a safety
|
|
8778
|
+
* feature. This is the way out, and it is the ONLY automatic-looking one on
|
|
8779
|
+
* offer: there is deliberately no timed probation, because the crash this
|
|
8780
|
+
* bounds is deterministic and a probation would simply respawn Python
|
|
8781
|
+
* forever at a slower rate. Re-arming a device that was never failed is a
|
|
8782
|
+
* no-op, reported as such.
|
|
8783
|
+
*/
|
|
8784
|
+
async rearmInferenceDevice(input) {
|
|
8785
|
+
const rearmed = this.deviceLiveness.rearm(input.deviceKey);
|
|
8786
|
+
this.log.info("inference device re-armed by operator", { meta: {
|
|
8787
|
+
deviceKey: input.deviceKey,
|
|
8788
|
+
rearmed
|
|
8789
|
+
} });
|
|
8790
|
+
return { rearmed };
|
|
8791
|
+
}
|
|
8486
8792
|
async resolveDeviceFactory(deviceKey) {
|
|
8487
8793
|
const engine = resolveDeviceEngine(deviceKey);
|
|
8488
8794
|
if (enginesEqual(engine, this.currentEngine)) {
|
|
8489
8795
|
await this.ensureEngineFactory();
|
|
8490
8796
|
return this.engineFactory;
|
|
8491
8797
|
}
|
|
8798
|
+
this.refuseIfDeviceUnusable(deviceKey);
|
|
8492
8799
|
const existing = this.factoriesByDevice.get(deviceKey);
|
|
8493
8800
|
if (existing) {
|
|
8494
|
-
|
|
8495
|
-
|
|
8801
|
+
if (existing.isReady()) {
|
|
8802
|
+
this.deviceReaper.touch(deviceKey);
|
|
8803
|
+
return existing;
|
|
8804
|
+
}
|
|
8805
|
+
await this.condemnDeviceFactory(deviceKey, existing, "pool worker is not ready");
|
|
8806
|
+
this.refuseIfDeviceUnusable(deviceKey);
|
|
8496
8807
|
}
|
|
8497
8808
|
const inflight = this.deviceFactoryInflight.get(deviceKey);
|
|
8498
8809
|
if (inflight) return inflight;
|
|
@@ -8517,6 +8828,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
8517
8828
|
error: err instanceof Error ? err.message : String(err)
|
|
8518
8829
|
} });
|
|
8519
8830
|
await factory.dispose().catch(() => void 0);
|
|
8831
|
+
this.noteDeviceDeath(deviceKey, require_dist.errMsg(err));
|
|
8520
8832
|
throw err;
|
|
8521
8833
|
}
|
|
8522
8834
|
this.factoriesByDevice.set(deviceKey, factory);
|