@camstack/addon-pipeline-orchestrator 1.1.22 → 1.1.23

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/index.js CHANGED
@@ -24982,6 +24982,15 @@ var OrchestratorDiagnosticsSchema = object({
24982
24982
  activeDetectionCount: number().int().min(0)
24983
24983
  });
24984
24984
  var pipelineOrchestratorActions = defineCustomActions({ dumpState: customAction(_void(), OrchestratorDiagnosticsSchema) });
24985
+ /**
24986
+ * Sentinel returned by `buildDetectionConfig` when the profile-slot READ
24987
+ * itself failed transiently (e.g. `listAllProfileSlots` discovery timeout
24988
+ * while the stream-broker is (re)starting) — as opposed to `null`, which
24989
+ * means "genuinely no assigned slot / not configured". Callers MUST treat
24990
+ * this differently from `null`: never stop active detection on a transient
24991
+ * read failure (the slots almost certainly still exist), and schedule a retry.
24992
+ */
24993
+ var TRANSIENT_SLOT_READ = Symbol("transient-slot-read");
24985
24994
  var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddon {
24986
24995
  /** This node's Moleculer nodeId (from this.ctx.kernel.localNodeId). */
24987
24996
  localNodeId = "hub";
@@ -25205,6 +25214,15 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25205
25214
  loadShedResumeTimer = null;
25206
25215
  /** Per-camera guard so repeated low-fps snapshots don't stack relocate/pause. */
25207
25216
  shedInFlight = /* @__PURE__ */ new Set();
25217
+ /**
25218
+ * Per-device backoff for retrying {@link handleDeviceRegistered} after a
25219
+ * TRANSIENT profile-slot read failure ({@link TRANSIENT_SLOT_READ}). Timers
25220
+ * are `unref`'d and cleared on shutdown / device removal.
25221
+ */
25222
+ slotReadRetryTimers = /* @__PURE__ */ new Map();
25223
+ slotReadRetryAttempts = /* @__PURE__ */ new Map();
25224
+ static SLOT_READ_MAX_RETRIES = 6;
25225
+ static SLOT_READ_RETRY_BASE_MS = 500;
25208
25226
  /** Pending `scheduleReconcile` debounce timer. */
25209
25227
  reconcileTimer = null;
25210
25228
  /** True while `reconcileDispatch` is awaiting the RPC round-trip. */
@@ -25672,6 +25690,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25672
25690
  clearTimeout(this.pendingRetryDebounceTimer);
25673
25691
  this.pendingRetryDebounceTimer = null;
25674
25692
  }
25693
+ for (const t of this.slotReadRetryTimers.values()) clearTimeout(t);
25694
+ this.slotReadRetryTimers.clear();
25695
+ this.slotReadRetryAttempts.clear();
25675
25696
  this.unsubDeviceRegistered?.();
25676
25697
  this.unsubDeviceRegistered = null;
25677
25698
  this.unsubDeviceUnregistered?.();
@@ -26961,6 +26982,47 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
26961
26982
  }
26962
26983
  }
26963
26984
  /**
26985
+ * Bounded per-device retry of {@link handleDeviceRegistered} after a
26986
+ * TRANSIENT profile-slot read failure ({@link TRANSIENT_SLOT_READ}). The
26987
+ * read (`listAllProfileSlots`) can time out during a broker (re)start; we
26988
+ * must neither stop a running camera nor leave a not-yet-started camera
26989
+ * stranded — so re-run the registration handler with exponential backoff
26990
+ * until the read succeeds or the budget is exhausted. Reset on any
26991
+ * successful read ({@link clearSlotReadRetry}).
26992
+ */
26993
+ scheduleSlotReadRetry(deviceId) {
26994
+ const attempts = this.slotReadRetryAttempts.get(deviceId) ?? 0;
26995
+ if (attempts >= PipelineOrchestratorAddon.SLOT_READ_MAX_RETRIES) {
26996
+ this.ctx.logger.warn("slot-read retry budget exhausted — leaving detection as-is", { tags: { deviceId } });
26997
+ this.slotReadRetryAttempts.delete(deviceId);
26998
+ return;
26999
+ }
27000
+ const existing = this.slotReadRetryTimers.get(deviceId);
27001
+ if (existing) clearTimeout(existing);
27002
+ const delay = PipelineOrchestratorAddon.SLOT_READ_RETRY_BASE_MS * 2 ** attempts;
27003
+ this.slotReadRetryAttempts.set(deviceId, attempts + 1);
27004
+ const timer = setTimeout(() => {
27005
+ this.slotReadRetryTimers.delete(deviceId);
27006
+ this.handleDeviceRegistered(deviceId).catch((err) => {
27007
+ this.ctx.logger.debug("slot-read retry: handleDeviceRegistered failed", {
27008
+ tags: { deviceId },
27009
+ meta: { error: errMsg(err) }
27010
+ });
27011
+ });
27012
+ }, delay);
27013
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
27014
+ this.slotReadRetryTimers.set(deviceId, timer);
27015
+ }
27016
+ /** Cancel any pending slot-read retry + reset the budget for a device. */
27017
+ clearSlotReadRetry(deviceId) {
27018
+ const existing = this.slotReadRetryTimers.get(deviceId);
27019
+ if (existing) {
27020
+ clearTimeout(existing);
27021
+ this.slotReadRetryTimers.delete(deviceId);
27022
+ }
27023
+ this.slotReadRetryAttempts.delete(deviceId);
27024
+ }
27025
+ /**
26964
27026
  * Coalesce bursts of capacity/eligibility/readiness signals into a single
26965
27027
  * `retryPendingDispatches` pass. Mirrors `scheduleReconcile`, but on a
26966
27028
  * dedicated (longer) debounce so a raise-cap / node-connect flurry doesn't
@@ -28837,7 +28899,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28837
28899
  */
28838
28900
  async fetchAssignedProfiles(deviceId) {
28839
28901
  const api = this.api;
28840
- if (!api) return /* @__PURE__ */ new Map();
28902
+ if (!api) return null;
28841
28903
  try {
28842
28904
  const slots = await api.streamBroker.listAllProfileSlots.query();
28843
28905
  const out = /* @__PURE__ */ new Map();
@@ -28847,8 +28909,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28847
28909
  out.set(slot.profile, slot.sourceCamStreamId);
28848
28910
  }
28849
28911
  return out;
28850
- } catch {
28851
- return /* @__PURE__ */ new Map();
28912
+ } catch (err) {
28913
+ this.ctx.logger.debug("fetchAssignedProfiles: slot read failed (transient) — treating as unknown", {
28914
+ tags: { deviceId },
28915
+ meta: { error: errMsg(err) }
28916
+ });
28917
+ return null;
28852
28918
  }
28853
28919
  }
28854
28920
  /**
@@ -28867,6 +28933,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28867
28933
  */
28868
28934
  async buildDetectionConfig(deviceId) {
28869
28935
  const assigned = await this.fetchAssignedProfiles(deviceId);
28936
+ if (assigned === null) return TRANSIENT_SLOT_READ;
28870
28937
  if (assigned.size === 0) return null;
28871
28938
  const resolved = await this.resolveDeviceDetectionSettings(deviceId);
28872
28939
  if (!resolved) return null;
@@ -29010,6 +29077,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29010
29077
  }
29011
29078
  /** Stop detection and purge all persisted config for a removed device. */
29012
29079
  async handleDeviceUnregistered(deviceId) {
29080
+ this.clearSlotReadRetry(deviceId);
29013
29081
  await this.stopDetection(deviceId);
29014
29082
  this.zonesProvider?.forgetDevice(deviceId);
29015
29083
  this.zoneRulesProvider?.forgetDevice(deviceId);
@@ -29031,6 +29099,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29031
29099
  const log = this.ctx.logger.withTags({ deviceId });
29032
29100
  log.info("handleDeviceRegistered", { meta: { phase: "handleDeviceRegistered" } });
29033
29101
  const config = await this.buildDetectionConfig(deviceId);
29102
+ if (config === TRANSIENT_SLOT_READ) {
29103
+ log.warn("Profile-slot read failed transiently — keeping detection, scheduling retry");
29104
+ this.scheduleSlotReadRetry(deviceId);
29105
+ return;
29106
+ }
29107
+ this.clearSlotReadRetry(deviceId);
29034
29108
  log.info("[pipeline-orchestrator] buildDetectionConfig", config ? { meta: {
29035
29109
  enabled: config.enabled,
29036
29110
  motionStreamId: config.motionStreamId,
@@ -29096,6 +29170,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29096
29170
  const log = this.ctx.logger.withTags({ deviceId });
29097
29171
  if (this.activeDetections.has(deviceId)) await this.stopDetection(deviceId);
29098
29172
  const config = await this.buildDetectionConfig(deviceId);
29173
+ if (config === TRANSIENT_SLOT_READ) {
29174
+ log.warn("Settings changed — slot read failed transiently, scheduling retry");
29175
+ this.scheduleSlotReadRetry(deviceId);
29176
+ return;
29177
+ }
29178
+ this.clearSlotReadRetry(deviceId);
29099
29179
  if (!config) {
29100
29180
  log.info("Settings changed — no assigned slot, detection stopped");
29101
29181
  return;
package/dist/index.mjs CHANGED
@@ -24978,6 +24978,15 @@ var OrchestratorDiagnosticsSchema = object({
24978
24978
  activeDetectionCount: number().int().min(0)
24979
24979
  });
24980
24980
  var pipelineOrchestratorActions = defineCustomActions({ dumpState: customAction(_void(), OrchestratorDiagnosticsSchema) });
24981
+ /**
24982
+ * Sentinel returned by `buildDetectionConfig` when the profile-slot READ
24983
+ * itself failed transiently (e.g. `listAllProfileSlots` discovery timeout
24984
+ * while the stream-broker is (re)starting) — as opposed to `null`, which
24985
+ * means "genuinely no assigned slot / not configured". Callers MUST treat
24986
+ * this differently from `null`: never stop active detection on a transient
24987
+ * read failure (the slots almost certainly still exist), and schedule a retry.
24988
+ */
24989
+ var TRANSIENT_SLOT_READ = Symbol("transient-slot-read");
24981
24990
  var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddon {
24982
24991
  /** This node's Moleculer nodeId (from this.ctx.kernel.localNodeId). */
24983
24992
  localNodeId = "hub";
@@ -25201,6 +25210,15 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25201
25210
  loadShedResumeTimer = null;
25202
25211
  /** Per-camera guard so repeated low-fps snapshots don't stack relocate/pause. */
25203
25212
  shedInFlight = /* @__PURE__ */ new Set();
25213
+ /**
25214
+ * Per-device backoff for retrying {@link handleDeviceRegistered} after a
25215
+ * TRANSIENT profile-slot read failure ({@link TRANSIENT_SLOT_READ}). Timers
25216
+ * are `unref`'d and cleared on shutdown / device removal.
25217
+ */
25218
+ slotReadRetryTimers = /* @__PURE__ */ new Map();
25219
+ slotReadRetryAttempts = /* @__PURE__ */ new Map();
25220
+ static SLOT_READ_MAX_RETRIES = 6;
25221
+ static SLOT_READ_RETRY_BASE_MS = 500;
25204
25222
  /** Pending `scheduleReconcile` debounce timer. */
25205
25223
  reconcileTimer = null;
25206
25224
  /** True while `reconcileDispatch` is awaiting the RPC round-trip. */
@@ -25668,6 +25686,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25668
25686
  clearTimeout(this.pendingRetryDebounceTimer);
25669
25687
  this.pendingRetryDebounceTimer = null;
25670
25688
  }
25689
+ for (const t of this.slotReadRetryTimers.values()) clearTimeout(t);
25690
+ this.slotReadRetryTimers.clear();
25691
+ this.slotReadRetryAttempts.clear();
25671
25692
  this.unsubDeviceRegistered?.();
25672
25693
  this.unsubDeviceRegistered = null;
25673
25694
  this.unsubDeviceUnregistered?.();
@@ -26957,6 +26978,47 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
26957
26978
  }
26958
26979
  }
26959
26980
  /**
26981
+ * Bounded per-device retry of {@link handleDeviceRegistered} after a
26982
+ * TRANSIENT profile-slot read failure ({@link TRANSIENT_SLOT_READ}). The
26983
+ * read (`listAllProfileSlots`) can time out during a broker (re)start; we
26984
+ * must neither stop a running camera nor leave a not-yet-started camera
26985
+ * stranded — so re-run the registration handler with exponential backoff
26986
+ * until the read succeeds or the budget is exhausted. Reset on any
26987
+ * successful read ({@link clearSlotReadRetry}).
26988
+ */
26989
+ scheduleSlotReadRetry(deviceId) {
26990
+ const attempts = this.slotReadRetryAttempts.get(deviceId) ?? 0;
26991
+ if (attempts >= PipelineOrchestratorAddon.SLOT_READ_MAX_RETRIES) {
26992
+ this.ctx.logger.warn("slot-read retry budget exhausted — leaving detection as-is", { tags: { deviceId } });
26993
+ this.slotReadRetryAttempts.delete(deviceId);
26994
+ return;
26995
+ }
26996
+ const existing = this.slotReadRetryTimers.get(deviceId);
26997
+ if (existing) clearTimeout(existing);
26998
+ const delay = PipelineOrchestratorAddon.SLOT_READ_RETRY_BASE_MS * 2 ** attempts;
26999
+ this.slotReadRetryAttempts.set(deviceId, attempts + 1);
27000
+ const timer = setTimeout(() => {
27001
+ this.slotReadRetryTimers.delete(deviceId);
27002
+ this.handleDeviceRegistered(deviceId).catch((err) => {
27003
+ this.ctx.logger.debug("slot-read retry: handleDeviceRegistered failed", {
27004
+ tags: { deviceId },
27005
+ meta: { error: errMsg(err) }
27006
+ });
27007
+ });
27008
+ }, delay);
27009
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
27010
+ this.slotReadRetryTimers.set(deviceId, timer);
27011
+ }
27012
+ /** Cancel any pending slot-read retry + reset the budget for a device. */
27013
+ clearSlotReadRetry(deviceId) {
27014
+ const existing = this.slotReadRetryTimers.get(deviceId);
27015
+ if (existing) {
27016
+ clearTimeout(existing);
27017
+ this.slotReadRetryTimers.delete(deviceId);
27018
+ }
27019
+ this.slotReadRetryAttempts.delete(deviceId);
27020
+ }
27021
+ /**
26960
27022
  * Coalesce bursts of capacity/eligibility/readiness signals into a single
26961
27023
  * `retryPendingDispatches` pass. Mirrors `scheduleReconcile`, but on a
26962
27024
  * dedicated (longer) debounce so a raise-cap / node-connect flurry doesn't
@@ -28833,7 +28895,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28833
28895
  */
28834
28896
  async fetchAssignedProfiles(deviceId) {
28835
28897
  const api = this.api;
28836
- if (!api) return /* @__PURE__ */ new Map();
28898
+ if (!api) return null;
28837
28899
  try {
28838
28900
  const slots = await api.streamBroker.listAllProfileSlots.query();
28839
28901
  const out = /* @__PURE__ */ new Map();
@@ -28843,8 +28905,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28843
28905
  out.set(slot.profile, slot.sourceCamStreamId);
28844
28906
  }
28845
28907
  return out;
28846
- } catch {
28847
- return /* @__PURE__ */ new Map();
28908
+ } catch (err) {
28909
+ this.ctx.logger.debug("fetchAssignedProfiles: slot read failed (transient) — treating as unknown", {
28910
+ tags: { deviceId },
28911
+ meta: { error: errMsg(err) }
28912
+ });
28913
+ return null;
28848
28914
  }
28849
28915
  }
28850
28916
  /**
@@ -28863,6 +28929,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28863
28929
  */
28864
28930
  async buildDetectionConfig(deviceId) {
28865
28931
  const assigned = await this.fetchAssignedProfiles(deviceId);
28932
+ if (assigned === null) return TRANSIENT_SLOT_READ;
28866
28933
  if (assigned.size === 0) return null;
28867
28934
  const resolved = await this.resolveDeviceDetectionSettings(deviceId);
28868
28935
  if (!resolved) return null;
@@ -29006,6 +29073,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29006
29073
  }
29007
29074
  /** Stop detection and purge all persisted config for a removed device. */
29008
29075
  async handleDeviceUnregistered(deviceId) {
29076
+ this.clearSlotReadRetry(deviceId);
29009
29077
  await this.stopDetection(deviceId);
29010
29078
  this.zonesProvider?.forgetDevice(deviceId);
29011
29079
  this.zoneRulesProvider?.forgetDevice(deviceId);
@@ -29027,6 +29095,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29027
29095
  const log = this.ctx.logger.withTags({ deviceId });
29028
29096
  log.info("handleDeviceRegistered", { meta: { phase: "handleDeviceRegistered" } });
29029
29097
  const config = await this.buildDetectionConfig(deviceId);
29098
+ if (config === TRANSIENT_SLOT_READ) {
29099
+ log.warn("Profile-slot read failed transiently — keeping detection, scheduling retry");
29100
+ this.scheduleSlotReadRetry(deviceId);
29101
+ return;
29102
+ }
29103
+ this.clearSlotReadRetry(deviceId);
29030
29104
  log.info("[pipeline-orchestrator] buildDetectionConfig", config ? { meta: {
29031
29105
  enabled: config.enabled,
29032
29106
  motionStreamId: config.motionStreamId,
@@ -29092,6 +29166,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29092
29166
  const log = this.ctx.logger.withTags({ deviceId });
29093
29167
  if (this.activeDetections.has(deviceId)) await this.stopDetection(deviceId);
29094
29168
  const config = await this.buildDetectionConfig(deviceId);
29169
+ if (config === TRANSIENT_SLOT_READ) {
29170
+ log.warn("Settings changed — slot read failed transiently, scheduling retry");
29171
+ this.scheduleSlotReadRetry(deviceId);
29172
+ return;
29173
+ }
29174
+ this.clearSlotReadRetry(deviceId);
29095
29175
  if (!config) {
29096
29176
  log.info("Settings changed — no assigned slot, detection stopped");
29097
29177
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
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",