@camstack/addon-pipeline 1.2.302 → 1.2.304
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/pipeline-runner/index.js +211 -13
- package/dist/pipeline-runner/index.mjs +211 -13
- package/dist/recorder/index.js +1349 -70
- package/dist/recorder/index.mjs +1423 -145
- package/dist/session-decode/decode-worker-main.js +250 -57
- package/dist/session-decode/decode-worker-main.mjs +250 -57
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DFaPx_Ot.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CF7C68rV.mjs} +1 -1
- package/dist/stream-broker/{hostInit-CePd6XbV.mjs → hostInit-dac4-ofq.mjs} +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -41,14 +41,18 @@ var KILL_SIGNALS = {
|
|
|
41
41
|
* parameter rather than a second `child.on('exit')` in the caller because
|
|
42
42
|
* every listener added out here is one somebody can forget to remove.
|
|
43
43
|
*/
|
|
44
|
-
function wrapChild(child, onExit) {
|
|
44
|
+
function wrapChild(child, onExit, killedByRunner = () => false) {
|
|
45
45
|
let spawned = false;
|
|
46
46
|
let exitDelivered = false;
|
|
47
47
|
const exitListeners = [];
|
|
48
|
-
const deliverExit = (
|
|
48
|
+
const deliverExit = (exit) => {
|
|
49
49
|
if (exitDelivered) return;
|
|
50
50
|
exitDelivered = true;
|
|
51
51
|
onExit();
|
|
52
|
+
const info = killedByRunner() ? {
|
|
53
|
+
...exit,
|
|
54
|
+
killedByRunner: true
|
|
55
|
+
} : exit;
|
|
52
56
|
for (const listener of exitListeners) listener(info);
|
|
53
57
|
};
|
|
54
58
|
child.on("spawn", () => {
|
|
@@ -102,19 +106,179 @@ function wrapChild(child, onExit) {
|
|
|
102
106
|
* node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
|
|
103
107
|
* agents, so a replaced env would silently break decoding off the hub.
|
|
104
108
|
*/
|
|
105
|
-
function forkDecodeWorker(source,
|
|
106
|
-
|
|
109
|
+
function forkDecodeWorker(source, deps) {
|
|
110
|
+
return forkDecodeWorkerAt(CHILD_ENTRY_PATH, source, deps ?? {});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* {@link forkDecodeWorker} with the entry named — the one seam a process-level
|
|
114
|
+
* test needs to fork a stub through the REAL fork, claim and live-worker path.
|
|
115
|
+
* Production forks only {@link CHILD_ENTRY_PATH}.
|
|
116
|
+
*/
|
|
117
|
+
function forkDecodeWorkerAt(entryPath, source, deps) {
|
|
118
|
+
const child = (0, node_child_process.fork)(entryPath, [], {
|
|
107
119
|
serialization: "advanced",
|
|
108
120
|
env: { ...process.env }
|
|
109
121
|
});
|
|
110
|
-
const costClaim = costRegistry?.claim({
|
|
122
|
+
const costClaim = deps.costRegistry?.claim({
|
|
111
123
|
role: "decode",
|
|
112
124
|
deviceId: source.deviceId,
|
|
113
125
|
unit: `${source.deviceId}/decode`,
|
|
114
126
|
pid: child.pid
|
|
115
127
|
}) ?? require_node.NO_COST_CLAIM;
|
|
116
|
-
|
|
128
|
+
let killedByRunner = false;
|
|
129
|
+
const untrack = deps.liveWorkers?.add({
|
|
130
|
+
deviceId: source.deviceId,
|
|
131
|
+
pid: child.pid,
|
|
132
|
+
killNow: () => {
|
|
133
|
+
killedByRunner = true;
|
|
134
|
+
child.kill("SIGKILL");
|
|
135
|
+
}
|
|
136
|
+
}) ?? (() => void 0);
|
|
137
|
+
return wrapChild(child, () => {
|
|
138
|
+
costClaim.release();
|
|
139
|
+
untrack();
|
|
140
|
+
}, () => killedByRunner);
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/session-decode/live-decode-workers.ts
|
|
144
|
+
var KILL_MESSAGE = {
|
|
145
|
+
shutdown: "session-decode: worker killed at shutdown",
|
|
146
|
+
"forked-after-shutdown": "session-decode: worker forked after shutdown began — killed at once",
|
|
147
|
+
"process-exit": "session-decode: worker killed at process exit"
|
|
148
|
+
};
|
|
149
|
+
/** Send the signal. Never throws: a failure is the outcome. */
|
|
150
|
+
function signal(worker) {
|
|
151
|
+
try {
|
|
152
|
+
worker.killNow();
|
|
153
|
+
return {
|
|
154
|
+
worker,
|
|
155
|
+
error: null
|
|
156
|
+
};
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return {
|
|
159
|
+
worker,
|
|
160
|
+
error: require_dist.errMsg(err)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* One line per outcome, each in its own swallowing try. The logger may throw:
|
|
166
|
+
* the `'exit'` hook runs while the transport goes down, and the runner's
|
|
167
|
+
* write path has no guard. A line that fails must not cost another line.
|
|
168
|
+
*/
|
|
169
|
+
function logOutcomes(outcomes, reason, logger) {
|
|
170
|
+
for (const { worker, error } of outcomes) try {
|
|
171
|
+
if (error === null) logger.info(KILL_MESSAGE[reason], {
|
|
172
|
+
tags: { deviceId: worker.deviceId },
|
|
173
|
+
meta: {
|
|
174
|
+
pid: worker.pid ?? null,
|
|
175
|
+
signal: "SIGKILL",
|
|
176
|
+
reason
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
else logger.warn("session-decode: worker kill failed — it may outlive the runner", {
|
|
180
|
+
tags: { deviceId: worker.deviceId },
|
|
181
|
+
meta: {
|
|
182
|
+
pid: worker.pid ?? null,
|
|
183
|
+
reason,
|
|
184
|
+
error
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} catch {}
|
|
117
188
|
}
|
|
189
|
+
var LiveDecodeWorkers = class {
|
|
190
|
+
live = /* @__PURE__ */ new Set();
|
|
191
|
+
/** Set by {@link close}: from then on a worker is killed as it is added. */
|
|
192
|
+
closedLogger = null;
|
|
193
|
+
/** Track a forked worker; call the returned function from its real exit. */
|
|
194
|
+
add(worker) {
|
|
195
|
+
if (this.closedLogger !== null) {
|
|
196
|
+
const outcome = signal(worker);
|
|
197
|
+
logOutcomes([outcome], "forked-after-shutdown", this.closedLogger);
|
|
198
|
+
if (outcome.error === null) return () => void 0;
|
|
199
|
+
}
|
|
200
|
+
this.live.add(worker);
|
|
201
|
+
return () => {
|
|
202
|
+
this.live.delete(worker);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
get size() {
|
|
206
|
+
return this.live.size;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* SIGKILL every tracked worker now, then one log line per worker. SIGKILL
|
|
210
|
+
* rather than SIGTERM: the runner is about to exit and cannot wait for a
|
|
211
|
+
* fallback, and a worker blocked in a native call never runs a SIGTERM
|
|
212
|
+
* handler (D652).
|
|
213
|
+
*
|
|
214
|
+
* EVERY worker is signalled before ANY line is written: a logger that throws
|
|
215
|
+
* must not stop the kills. A signalled worker is forgotten (a second pass
|
|
216
|
+
* logs nothing twice); one whose kill threw stays tracked, so the exit hook
|
|
217
|
+
* or a later pass tries it again. Returns how many it signalled.
|
|
218
|
+
*/
|
|
219
|
+
killAll(reason, logger) {
|
|
220
|
+
const outcomes = [...this.live].map(signal);
|
|
221
|
+
for (const outcome of outcomes) if (outcome.error === null) this.live.delete(outcome.worker);
|
|
222
|
+
logOutcomes(outcomes, reason, logger);
|
|
223
|
+
return outcomes.filter((outcome) => outcome.error === null).length;
|
|
224
|
+
}
|
|
225
|
+
/** Kill everything tracked, and kill every later `add` at once. */
|
|
226
|
+
close(reason, logger) {
|
|
227
|
+
this.closedLogger = logger;
|
|
228
|
+
return this.killAll(reason, logger);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* The runner's decode workers: every fork is registered in the set, and
|
|
233
|
+
* {@link shutdown} and any process exit kill whatever is still alive.
|
|
234
|
+
*/
|
|
235
|
+
var RunnerDecodeWorkers = class {
|
|
236
|
+
deps;
|
|
237
|
+
live = new LiveDecodeWorkers();
|
|
238
|
+
exitHookArmed = false;
|
|
239
|
+
shutDown = false;
|
|
240
|
+
constructor(deps) {
|
|
241
|
+
this.deps = deps;
|
|
242
|
+
}
|
|
243
|
+
/** Fork one worker, registered and cost-claimed. */
|
|
244
|
+
fork = (source) => {
|
|
245
|
+
this.armExitHook();
|
|
246
|
+
return this.deps.fork(source, {
|
|
247
|
+
costRegistry: this.deps.costRegistry,
|
|
248
|
+
liveWorkers: this.live
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* The addon is shutting down: kill every worker, and every later fork at
|
|
253
|
+
* once. Synchronous, so it is done before the runner's `process.exit`.
|
|
254
|
+
*/
|
|
255
|
+
shutdown() {
|
|
256
|
+
this.shutDown = true;
|
|
257
|
+
const killed = this.live.close("shutdown", this.deps.logger);
|
|
258
|
+
if (this.live.size === 0) this.disarmExitHook();
|
|
259
|
+
return killed;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The process is exiting without having shut this addon down (an uncaught
|
|
263
|
+
* error, or an `onShutdown` that never reached {@link shutdown}). The log
|
|
264
|
+
* line is attempted but will probably not land: the transport goes down
|
|
265
|
+
* with the process.
|
|
266
|
+
*/
|
|
267
|
+
onProcessExit = () => {
|
|
268
|
+
this.exitHookArmed = false;
|
|
269
|
+
this.live.close("process-exit", this.deps.logger);
|
|
270
|
+
};
|
|
271
|
+
armExitHook() {
|
|
272
|
+
if (this.exitHookArmed || this.shutDown) return;
|
|
273
|
+
this.exitHookArmed = true;
|
|
274
|
+
this.deps.process.once("exit", this.onProcessExit);
|
|
275
|
+
}
|
|
276
|
+
disarmExitHook() {
|
|
277
|
+
if (!this.exitHookArmed) return;
|
|
278
|
+
this.exitHookArmed = false;
|
|
279
|
+
this.deps.process.removeListener("exit", this.onProcessExit);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
118
282
|
//#endregion
|
|
119
283
|
//#region src/session-decode/native-lease-config.ts
|
|
120
284
|
/**
|
|
@@ -1722,6 +1886,11 @@ var SessionDecodeSession = class {
|
|
|
1722
1886
|
/** Set when `kill()` is sent, so handleExit can tell an intentional stop from a death. */
|
|
1723
1887
|
killSent = false;
|
|
1724
1888
|
/**
|
|
1889
|
+
* A SIGKILL already went out — an `'immediate'` teardown from the pump
|
|
1890
|
+
* (D666) and the generator's later `finally` both escalate; one kill is enough.
|
|
1891
|
+
*/
|
|
1892
|
+
sigkillSent = false;
|
|
1893
|
+
/**
|
|
1725
1894
|
* Images yielded and not yet closed that a CONSUMER still holds — queued,
|
|
1726
1895
|
* in flight, or being read. Each dies with the worker and somebody was going
|
|
1727
1896
|
* to read it first. This is what a drain waits on.
|
|
@@ -1933,7 +2102,7 @@ var SessionDecodeSession = class {
|
|
|
1933
2102
|
}
|
|
1934
2103
|
/** The drain (or a SIGTERM still waiting on its fallback) gives way to SIGKILL now. */
|
|
1935
2104
|
escalateToImmediate() {
|
|
1936
|
-
if (this.exited) return;
|
|
2105
|
+
if (this.exited || this.sigkillSent) return;
|
|
1937
2106
|
if (this.drainTimer !== null) {
|
|
1938
2107
|
clearTimeout(this.drainTimer);
|
|
1939
2108
|
this.drainTimer = null;
|
|
@@ -2120,7 +2289,14 @@ var SessionDecodeSession = class {
|
|
|
2120
2289
|
}
|
|
2121
2290
|
handleExit(info) {
|
|
2122
2291
|
if (!this.exited) this.hooks.onWorkerExit?.();
|
|
2123
|
-
if (!this.killSent && !this.exited) this.logger.
|
|
2292
|
+
if (!this.killSent && !this.exited && info?.killedByRunner === true) this.logger.debug("session-decode worker ended by the runner shutdown kill", {
|
|
2293
|
+
tags: { deviceId: this.deviceId },
|
|
2294
|
+
meta: {
|
|
2295
|
+
code: info.code,
|
|
2296
|
+
signal: info.signal
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
else if (!this.killSent && !this.exited) this.logger.warn(this.stopRequested ? "session-decode worker exited unexpectedly while draining" : "session-decode worker exited unexpectedly", {
|
|
2124
2300
|
tags: { deviceId: this.deviceId },
|
|
2125
2301
|
meta: {
|
|
2126
2302
|
code: info?.code ?? null,
|
|
@@ -2312,11 +2488,15 @@ var SessionDecodeSession = class {
|
|
|
2312
2488
|
tryKill(signal) {
|
|
2313
2489
|
try {
|
|
2314
2490
|
this.child.kill(signal);
|
|
2491
|
+
if (signal === "SIGKILL") this.sigkillSent = true;
|
|
2315
2492
|
} catch (err) {
|
|
2316
|
-
this.logger.warn("session-decode-coordinator: kill() on child failed", {
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2493
|
+
this.logger.warn("session-decode-coordinator: kill() on child failed", {
|
|
2494
|
+
tags: { deviceId: this.deviceId },
|
|
2495
|
+
meta: {
|
|
2496
|
+
signal,
|
|
2497
|
+
err: toError(err).message
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2320
2500
|
}
|
|
2321
2501
|
}
|
|
2322
2502
|
};
|
|
@@ -2586,6 +2766,7 @@ function startSessionDecodePump(deps) {
|
|
|
2586
2766
|
stopped = true;
|
|
2587
2767
|
teardownMode = mode;
|
|
2588
2768
|
releaseSpawnSlotOnce();
|
|
2769
|
+
if (mode === "immediate") escalateSession?.("immediate");
|
|
2589
2770
|
iterator.return().catch(() => {});
|
|
2590
2771
|
};
|
|
2591
2772
|
}
|
|
@@ -8009,6 +8190,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8009
8190
|
*/
|
|
8010
8191
|
costRegistry = new require_node.ChildCostRegistry();
|
|
8011
8192
|
/**
|
|
8193
|
+
* Every decode worker this process forked, from ANY spawn site (live
|
|
8194
|
+
* sessions, occupancy bursts), until its exit. The first thing `onShutdown`
|
|
8195
|
+
* does is kill them, a fork after that is killed at once, and any process
|
|
8196
|
+
* exit kills what is left (D666). The logger is only unready before
|
|
8197
|
+
* `initialize()`, when no worker can have been forked.
|
|
8198
|
+
*/
|
|
8199
|
+
decodeWorkers = new RunnerDecodeWorkers({
|
|
8200
|
+
fork: forkDecodeWorker,
|
|
8201
|
+
costRegistry: this.costRegistry,
|
|
8202
|
+
logger: {
|
|
8203
|
+
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
8204
|
+
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
8205
|
+
},
|
|
8206
|
+
process
|
|
8207
|
+
});
|
|
8208
|
+
/**
|
|
8012
8209
|
* Per-camera inference-deadline counters reported through
|
|
8013
8210
|
* `failure-contribution`. Bounded, cumulative, and read (never drained) by
|
|
8014
8211
|
* the provider. Constructed eagerly: created on the first timeout it would
|
|
@@ -8458,6 +8655,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8458
8655
|
};
|
|
8459
8656
|
}
|
|
8460
8657
|
async onShutdown() {
|
|
8658
|
+
this.decodeWorkers.shutdown();
|
|
8461
8659
|
if (this.runtimeInfoTimer !== null) {
|
|
8462
8660
|
clearInterval(this.runtimeInfoTimer);
|
|
8463
8661
|
this.runtimeInfoTimer = null;
|
|
@@ -10008,7 +10206,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
10008
10206
|
* environment. A worker is therefore forkable before anyone knows which
|
|
10009
10207
|
* camera it will serve.
|
|
10010
10208
|
*/
|
|
10011
|
-
forkDecodeWorkerWithLease = (source) =>
|
|
10209
|
+
forkDecodeWorkerWithLease = (source) => this.decodeWorkers.fork(source);
|
|
10012
10210
|
/**
|
|
10013
10211
|
* The lease knobs for the session that is about to start — `setting > env >
|
|
10014
10212
|
* default`, resolved HERE because this process is the one that carries the
|
|
@@ -34,14 +34,18 @@ var KILL_SIGNALS = {
|
|
|
34
34
|
* parameter rather than a second `child.on('exit')` in the caller because
|
|
35
35
|
* every listener added out here is one somebody can forget to remove.
|
|
36
36
|
*/
|
|
37
|
-
function wrapChild(child, onExit) {
|
|
37
|
+
function wrapChild(child, onExit, killedByRunner = () => false) {
|
|
38
38
|
let spawned = false;
|
|
39
39
|
let exitDelivered = false;
|
|
40
40
|
const exitListeners = [];
|
|
41
|
-
const deliverExit = (
|
|
41
|
+
const deliverExit = (exit) => {
|
|
42
42
|
if (exitDelivered) return;
|
|
43
43
|
exitDelivered = true;
|
|
44
44
|
onExit();
|
|
45
|
+
const info = killedByRunner() ? {
|
|
46
|
+
...exit,
|
|
47
|
+
killedByRunner: true
|
|
48
|
+
} : exit;
|
|
45
49
|
for (const listener of exitListeners) listener(info);
|
|
46
50
|
};
|
|
47
51
|
child.on("spawn", () => {
|
|
@@ -95,19 +99,179 @@ function wrapChild(child, onExit) {
|
|
|
95
99
|
* node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
|
|
96
100
|
* agents, so a replaced env would silently break decoding off the hub.
|
|
97
101
|
*/
|
|
98
|
-
function forkDecodeWorker(source,
|
|
99
|
-
|
|
102
|
+
function forkDecodeWorker(source, deps) {
|
|
103
|
+
return forkDecodeWorkerAt(CHILD_ENTRY_PATH, source, deps ?? {});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* {@link forkDecodeWorker} with the entry named — the one seam a process-level
|
|
107
|
+
* test needs to fork a stub through the REAL fork, claim and live-worker path.
|
|
108
|
+
* Production forks only {@link CHILD_ENTRY_PATH}.
|
|
109
|
+
*/
|
|
110
|
+
function forkDecodeWorkerAt(entryPath, source, deps) {
|
|
111
|
+
const child = fork(entryPath, [], {
|
|
100
112
|
serialization: "advanced",
|
|
101
113
|
env: { ...process.env }
|
|
102
114
|
});
|
|
103
|
-
const costClaim = costRegistry?.claim({
|
|
115
|
+
const costClaim = deps.costRegistry?.claim({
|
|
104
116
|
role: "decode",
|
|
105
117
|
deviceId: source.deviceId,
|
|
106
118
|
unit: `${source.deviceId}/decode`,
|
|
107
119
|
pid: child.pid
|
|
108
120
|
}) ?? NO_COST_CLAIM;
|
|
109
|
-
|
|
121
|
+
let killedByRunner = false;
|
|
122
|
+
const untrack = deps.liveWorkers?.add({
|
|
123
|
+
deviceId: source.deviceId,
|
|
124
|
+
pid: child.pid,
|
|
125
|
+
killNow: () => {
|
|
126
|
+
killedByRunner = true;
|
|
127
|
+
child.kill("SIGKILL");
|
|
128
|
+
}
|
|
129
|
+
}) ?? (() => void 0);
|
|
130
|
+
return wrapChild(child, () => {
|
|
131
|
+
costClaim.release();
|
|
132
|
+
untrack();
|
|
133
|
+
}, () => killedByRunner);
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/session-decode/live-decode-workers.ts
|
|
137
|
+
var KILL_MESSAGE = {
|
|
138
|
+
shutdown: "session-decode: worker killed at shutdown",
|
|
139
|
+
"forked-after-shutdown": "session-decode: worker forked after shutdown began — killed at once",
|
|
140
|
+
"process-exit": "session-decode: worker killed at process exit"
|
|
141
|
+
};
|
|
142
|
+
/** Send the signal. Never throws: a failure is the outcome. */
|
|
143
|
+
function signal(worker) {
|
|
144
|
+
try {
|
|
145
|
+
worker.killNow();
|
|
146
|
+
return {
|
|
147
|
+
worker,
|
|
148
|
+
error: null
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return {
|
|
152
|
+
worker,
|
|
153
|
+
error: errMsg(err)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* One line per outcome, each in its own swallowing try. The logger may throw:
|
|
159
|
+
* the `'exit'` hook runs while the transport goes down, and the runner's
|
|
160
|
+
* write path has no guard. A line that fails must not cost another line.
|
|
161
|
+
*/
|
|
162
|
+
function logOutcomes(outcomes, reason, logger) {
|
|
163
|
+
for (const { worker, error } of outcomes) try {
|
|
164
|
+
if (error === null) logger.info(KILL_MESSAGE[reason], {
|
|
165
|
+
tags: { deviceId: worker.deviceId },
|
|
166
|
+
meta: {
|
|
167
|
+
pid: worker.pid ?? null,
|
|
168
|
+
signal: "SIGKILL",
|
|
169
|
+
reason
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
else logger.warn("session-decode: worker kill failed — it may outlive the runner", {
|
|
173
|
+
tags: { deviceId: worker.deviceId },
|
|
174
|
+
meta: {
|
|
175
|
+
pid: worker.pid ?? null,
|
|
176
|
+
reason,
|
|
177
|
+
error
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} catch {}
|
|
110
181
|
}
|
|
182
|
+
var LiveDecodeWorkers = class {
|
|
183
|
+
live = /* @__PURE__ */ new Set();
|
|
184
|
+
/** Set by {@link close}: from then on a worker is killed as it is added. */
|
|
185
|
+
closedLogger = null;
|
|
186
|
+
/** Track a forked worker; call the returned function from its real exit. */
|
|
187
|
+
add(worker) {
|
|
188
|
+
if (this.closedLogger !== null) {
|
|
189
|
+
const outcome = signal(worker);
|
|
190
|
+
logOutcomes([outcome], "forked-after-shutdown", this.closedLogger);
|
|
191
|
+
if (outcome.error === null) return () => void 0;
|
|
192
|
+
}
|
|
193
|
+
this.live.add(worker);
|
|
194
|
+
return () => {
|
|
195
|
+
this.live.delete(worker);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
get size() {
|
|
199
|
+
return this.live.size;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* SIGKILL every tracked worker now, then one log line per worker. SIGKILL
|
|
203
|
+
* rather than SIGTERM: the runner is about to exit and cannot wait for a
|
|
204
|
+
* fallback, and a worker blocked in a native call never runs a SIGTERM
|
|
205
|
+
* handler (D652).
|
|
206
|
+
*
|
|
207
|
+
* EVERY worker is signalled before ANY line is written: a logger that throws
|
|
208
|
+
* must not stop the kills. A signalled worker is forgotten (a second pass
|
|
209
|
+
* logs nothing twice); one whose kill threw stays tracked, so the exit hook
|
|
210
|
+
* or a later pass tries it again. Returns how many it signalled.
|
|
211
|
+
*/
|
|
212
|
+
killAll(reason, logger) {
|
|
213
|
+
const outcomes = [...this.live].map(signal);
|
|
214
|
+
for (const outcome of outcomes) if (outcome.error === null) this.live.delete(outcome.worker);
|
|
215
|
+
logOutcomes(outcomes, reason, logger);
|
|
216
|
+
return outcomes.filter((outcome) => outcome.error === null).length;
|
|
217
|
+
}
|
|
218
|
+
/** Kill everything tracked, and kill every later `add` at once. */
|
|
219
|
+
close(reason, logger) {
|
|
220
|
+
this.closedLogger = logger;
|
|
221
|
+
return this.killAll(reason, logger);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* The runner's decode workers: every fork is registered in the set, and
|
|
226
|
+
* {@link shutdown} and any process exit kill whatever is still alive.
|
|
227
|
+
*/
|
|
228
|
+
var RunnerDecodeWorkers = class {
|
|
229
|
+
deps;
|
|
230
|
+
live = new LiveDecodeWorkers();
|
|
231
|
+
exitHookArmed = false;
|
|
232
|
+
shutDown = false;
|
|
233
|
+
constructor(deps) {
|
|
234
|
+
this.deps = deps;
|
|
235
|
+
}
|
|
236
|
+
/** Fork one worker, registered and cost-claimed. */
|
|
237
|
+
fork = (source) => {
|
|
238
|
+
this.armExitHook();
|
|
239
|
+
return this.deps.fork(source, {
|
|
240
|
+
costRegistry: this.deps.costRegistry,
|
|
241
|
+
liveWorkers: this.live
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* The addon is shutting down: kill every worker, and every later fork at
|
|
246
|
+
* once. Synchronous, so it is done before the runner's `process.exit`.
|
|
247
|
+
*/
|
|
248
|
+
shutdown() {
|
|
249
|
+
this.shutDown = true;
|
|
250
|
+
const killed = this.live.close("shutdown", this.deps.logger);
|
|
251
|
+
if (this.live.size === 0) this.disarmExitHook();
|
|
252
|
+
return killed;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The process is exiting without having shut this addon down (an uncaught
|
|
256
|
+
* error, or an `onShutdown` that never reached {@link shutdown}). The log
|
|
257
|
+
* line is attempted but will probably not land: the transport goes down
|
|
258
|
+
* with the process.
|
|
259
|
+
*/
|
|
260
|
+
onProcessExit = () => {
|
|
261
|
+
this.exitHookArmed = false;
|
|
262
|
+
this.live.close("process-exit", this.deps.logger);
|
|
263
|
+
};
|
|
264
|
+
armExitHook() {
|
|
265
|
+
if (this.exitHookArmed || this.shutDown) return;
|
|
266
|
+
this.exitHookArmed = true;
|
|
267
|
+
this.deps.process.once("exit", this.onProcessExit);
|
|
268
|
+
}
|
|
269
|
+
disarmExitHook() {
|
|
270
|
+
if (!this.exitHookArmed) return;
|
|
271
|
+
this.exitHookArmed = false;
|
|
272
|
+
this.deps.process.removeListener("exit", this.onProcessExit);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
111
275
|
//#endregion
|
|
112
276
|
//#region src/session-decode/native-lease-config.ts
|
|
113
277
|
/**
|
|
@@ -1715,6 +1879,11 @@ var SessionDecodeSession = class {
|
|
|
1715
1879
|
/** Set when `kill()` is sent, so handleExit can tell an intentional stop from a death. */
|
|
1716
1880
|
killSent = false;
|
|
1717
1881
|
/**
|
|
1882
|
+
* A SIGKILL already went out — an `'immediate'` teardown from the pump
|
|
1883
|
+
* (D666) and the generator's later `finally` both escalate; one kill is enough.
|
|
1884
|
+
*/
|
|
1885
|
+
sigkillSent = false;
|
|
1886
|
+
/**
|
|
1718
1887
|
* Images yielded and not yet closed that a CONSUMER still holds — queued,
|
|
1719
1888
|
* in flight, or being read. Each dies with the worker and somebody was going
|
|
1720
1889
|
* to read it first. This is what a drain waits on.
|
|
@@ -1926,7 +2095,7 @@ var SessionDecodeSession = class {
|
|
|
1926
2095
|
}
|
|
1927
2096
|
/** The drain (or a SIGTERM still waiting on its fallback) gives way to SIGKILL now. */
|
|
1928
2097
|
escalateToImmediate() {
|
|
1929
|
-
if (this.exited) return;
|
|
2098
|
+
if (this.exited || this.sigkillSent) return;
|
|
1930
2099
|
if (this.drainTimer !== null) {
|
|
1931
2100
|
clearTimeout(this.drainTimer);
|
|
1932
2101
|
this.drainTimer = null;
|
|
@@ -2113,7 +2282,14 @@ var SessionDecodeSession = class {
|
|
|
2113
2282
|
}
|
|
2114
2283
|
handleExit(info) {
|
|
2115
2284
|
if (!this.exited) this.hooks.onWorkerExit?.();
|
|
2116
|
-
if (!this.killSent && !this.exited) this.logger.
|
|
2285
|
+
if (!this.killSent && !this.exited && info?.killedByRunner === true) this.logger.debug("session-decode worker ended by the runner shutdown kill", {
|
|
2286
|
+
tags: { deviceId: this.deviceId },
|
|
2287
|
+
meta: {
|
|
2288
|
+
code: info.code,
|
|
2289
|
+
signal: info.signal
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
else if (!this.killSent && !this.exited) this.logger.warn(this.stopRequested ? "session-decode worker exited unexpectedly while draining" : "session-decode worker exited unexpectedly", {
|
|
2117
2293
|
tags: { deviceId: this.deviceId },
|
|
2118
2294
|
meta: {
|
|
2119
2295
|
code: info?.code ?? null,
|
|
@@ -2305,11 +2481,15 @@ var SessionDecodeSession = class {
|
|
|
2305
2481
|
tryKill(signal) {
|
|
2306
2482
|
try {
|
|
2307
2483
|
this.child.kill(signal);
|
|
2484
|
+
if (signal === "SIGKILL") this.sigkillSent = true;
|
|
2308
2485
|
} catch (err) {
|
|
2309
|
-
this.logger.warn("session-decode-coordinator: kill() on child failed", {
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2486
|
+
this.logger.warn("session-decode-coordinator: kill() on child failed", {
|
|
2487
|
+
tags: { deviceId: this.deviceId },
|
|
2488
|
+
meta: {
|
|
2489
|
+
signal,
|
|
2490
|
+
err: toError(err).message
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
2313
2493
|
}
|
|
2314
2494
|
}
|
|
2315
2495
|
};
|
|
@@ -2579,6 +2759,7 @@ function startSessionDecodePump(deps) {
|
|
|
2579
2759
|
stopped = true;
|
|
2580
2760
|
teardownMode = mode;
|
|
2581
2761
|
releaseSpawnSlotOnce();
|
|
2762
|
+
if (mode === "immediate") escalateSession?.("immediate");
|
|
2582
2763
|
iterator.return().catch(() => {});
|
|
2583
2764
|
};
|
|
2584
2765
|
}
|
|
@@ -8001,6 +8182,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
8001
8182
|
*/
|
|
8002
8183
|
costRegistry = new ChildCostRegistry();
|
|
8003
8184
|
/**
|
|
8185
|
+
* Every decode worker this process forked, from ANY spawn site (live
|
|
8186
|
+
* sessions, occupancy bursts), until its exit. The first thing `onShutdown`
|
|
8187
|
+
* does is kill them, a fork after that is killed at once, and any process
|
|
8188
|
+
* exit kills what is left (D666). The logger is only unready before
|
|
8189
|
+
* `initialize()`, when no worker can have been forked.
|
|
8190
|
+
*/
|
|
8191
|
+
decodeWorkers = new RunnerDecodeWorkers({
|
|
8192
|
+
fork: forkDecodeWorker,
|
|
8193
|
+
costRegistry: this.costRegistry,
|
|
8194
|
+
logger: {
|
|
8195
|
+
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
8196
|
+
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
8197
|
+
},
|
|
8198
|
+
process
|
|
8199
|
+
});
|
|
8200
|
+
/**
|
|
8004
8201
|
* Per-camera inference-deadline counters reported through
|
|
8005
8202
|
* `failure-contribution`. Bounded, cumulative, and read (never drained) by
|
|
8006
8203
|
* the provider. Constructed eagerly: created on the first timeout it would
|
|
@@ -8450,6 +8647,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
8450
8647
|
};
|
|
8451
8648
|
}
|
|
8452
8649
|
async onShutdown() {
|
|
8650
|
+
this.decodeWorkers.shutdown();
|
|
8453
8651
|
if (this.runtimeInfoTimer !== null) {
|
|
8454
8652
|
clearInterval(this.runtimeInfoTimer);
|
|
8455
8653
|
this.runtimeInfoTimer = null;
|
|
@@ -10000,7 +10198,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
10000
10198
|
* environment. A worker is therefore forkable before anyone knows which
|
|
10001
10199
|
* camera it will serve.
|
|
10002
10200
|
*/
|
|
10003
|
-
forkDecodeWorkerWithLease = (source) =>
|
|
10201
|
+
forkDecodeWorkerWithLease = (source) => this.decodeWorkers.fork(source);
|
|
10004
10202
|
/**
|
|
10005
10203
|
* The lease knobs for the session that is about to start — `setting > env >
|
|
10006
10204
|
* default`, resolved HERE because this process is the one that carries the
|