velocious 1.0.702 → 1.0.704

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.702",
6
+ "version": "1.0.704",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -10,6 +10,7 @@ export const POOLED_CHILD_SHUTDOWN_REASONS = Object.freeze([
10
10
  "parent_retire_drained",
11
11
  "job_timeout",
12
12
  "worker_stop",
13
+ "child_silence",
13
14
  "signal_sigterm",
14
15
  "signal_sigint",
15
16
  "signal_sigkill",
@@ -9,9 +9,9 @@
9
9
  /** @typedef {"queued" | "handed_off"} BackgroundJobActiveStatus */
10
10
  /** @typedef {"cancelled" | "completed" | "failed" | "orphaned"} BackgroundJobTerminalStatus */
11
11
  /** @typedef {BackgroundJobActiveStatus | BackgroundJobTerminalStatus} BackgroundJobStatus */
12
- /** @typedef {"exit" | "process-error" | "ipc-send"} PooledRunnerFailureOrigin */
12
+ /** @typedef {"exit" | "process-error" | "ipc-send" | "child-silence"} PooledRunnerFailureOrigin */
13
13
  /** @typedef {"starting" | "running" | "retiring"} PooledRunnerLifecycleState */
14
- /** @typedef {"parent_retire_drained" | "job_timeout" | "worker_stop" | "signal_sigterm" | "signal_sigint" | "signal_sigkill" | "signal_other" | "ipc_disconnect" | "process_error" | "unexpected_exit"} PooledChildShutdownReason */
14
+ /** @typedef {"parent_retire_drained" | "job_timeout" | "worker_stop" | "child_silence" | "signal_sigterm" | "signal_sigint" | "signal_sigkill" | "signal_other" | "ipc_disconnect" | "process_error" | "unexpected_exit"} PooledChildShutdownReason */
15
15
  /**
16
16
  * @deprecated Use PooledChildShutdownReason for exact shutdown provenance.
17
17
  * @typedef {"unexpected" | "job-timeout" | "worker-shutdown-timeout"} PooledRunnerTerminationReason
@@ -50,11 +50,23 @@ import { POOLED_RUNNER_INFLIGHT_JOB_ID_LIMIT, boundedPooledRunnerInflightJobIds,
50
50
  * @property {ReturnType<typeof setTimeout>} [shutdownSignalTimer] - Drained-retirement fallback signal timer.
51
51
  * @property {ReturnType<typeof setTimeout> | null} [timeoutSigkillTimer] - Pending timeout SIGKILL timer.
52
52
  * @property {string} [timeoutJobId] - Job whose timeout initiated termination.
53
+ * @property {number} lastActivityAtMs - Last time the worker saw any IPC message from this child, or dispatched to it.
54
+ * @property {ReturnType<typeof setTimeout>} [silenceTimer] - Armed per-child IPC-silence watchdog timer.
55
+ * @property {ReturnType<typeof setTimeout>} [silenceSigkillTimer] - Pending SIGKILL for a child the silence watchdog declared wedged.
53
56
  */
54
57
  /** Grace period after SIGTERM before a lingering process runner is SIGKILLed. */
55
58
  const FORKED_CHILD_SIGKILL_GRACE_MS = 5000
56
59
  /** Time a drained retirement gives the child IPC request to begin teardown before SIGTERM fallback. */
57
60
  const POOLED_RUNNER_SHUTDOWN_REQUEST_GRACE_MS = 250
61
+ /**
62
+ * Default bound on how long a pooled child with in-flight jobs may stay silent on
63
+ * its IPC channel before the worker declares it wedged. A healthy child samples its
64
+ * memory every 10 seconds while jobs run, so silence past this bound means its event
65
+ * loop cannot run its own timers — the signature of the silent transport wedge. The
66
+ * bound is deliberately far above a healthy long job's quiet window: it only fires on
67
+ * a child that cannot prove it is alive, never on a genuinely busy runner.
68
+ */
69
+ const POOLED_CHILD_SILENCE_TIMEOUT_MS = 120000
58
70
  /**
59
71
  * Largest delay Node's `setTimeout` accepts without overflowing to a 1ms delay
60
72
  * (a 32-bit signed int of ms, ~24.8 days). A `jobTimeoutMs` above this — or a
@@ -198,13 +210,14 @@ export default class BackgroundJobsWorker {
198
210
  * @param {number} [args.generationHandshakeTimeoutMs] - Maximum time to wait for generation acknowledgement (default: 4000).
199
211
  * @param {number} [args.reconnectDelayMs] - Delay before reconnecting an established worker connection (default: 1000).
200
212
  * @param {number} [args.jobTimeoutMs] - Override the wall-clock timeout for forked and pooled jobs from `configuration.getBackgroundJobsConfig()`. `0` disables it.
213
+ * @param {number} [args.pooledRunnerSilenceTimeoutMs] - Override the per-child IPC-silence bound for pooled children with in-flight jobs from `configuration.getBackgroundJobsConfig()` (default 120000ms). `0` disables the watchdog.
201
214
  * @param {boolean} [args.closeDatabaseConnectionsOnStop] - Whether stop owns closing the configuration's database pools (default true).
202
215
  * @param {() => void | Promise<void>} [args.onStopped] - Lifecycle hook invoked after the worker finishes stopping.
203
216
  * @param {() => void} [args.onGenerationAccepted] - Explicit generation-acceptance observation hook.
204
217
  * @param {() => void} [args.onRetireMessage] - Explicit retire-message observation hook.
205
218
  * @param {(observation: import("./types.js").PooledChildMemoryObservation) => void | Promise<void>} [args.onPooledRunnerMemoryObservation] - Explicit pooled-child memory observation hook. Every validated observation (periodic while a child has in-flight jobs, plus on demand) is forwarded here, in addition to the worker's compact stderr log line, so an application can route memory diagnostics (e.g. to a bug reporter) without parsing logs.
206
219
  */
207
- constructor({configuration, host, port, generationId, workerInstanceId, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, forkedChildSigkillGraceMs, heartbeatIntervalMs, generationHandshakeTimeoutMs = DEFAULT_GENERATION_HANDSHAKE_TIMEOUT_MS, reconnectDelayMs = 1000, jobTimeoutMs, closeDatabaseConnectionsOnStop = true, onStopped, onGenerationAccepted, onRetireMessage, onPooledRunnerMemoryObservation} = {}) {
220
+ constructor({configuration, host, port, generationId, workerInstanceId, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, forkedChildSigkillGraceMs, heartbeatIntervalMs, generationHandshakeTimeoutMs = DEFAULT_GENERATION_HANDSHAKE_TIMEOUT_MS, reconnectDelayMs = 1000, jobTimeoutMs, pooledRunnerSilenceTimeoutMs, closeDatabaseConnectionsOnStop = true, onStopped, onGenerationAccepted, onRetireMessage, onPooledRunnerMemoryObservation} = {}) {
208
221
  /**
209
222
  * Narrows the runtime value to the documented type.
210
223
  * @type {Promise<import("../configuration.js").default>} */
@@ -261,6 +274,8 @@ export default class BackgroundJobsWorker {
261
274
  this.pooledRunnerMaxJobs = this.pooledRunnerMaxJobsOverride || 100
262
275
  this.pooledRunnerMaxRssBytes = this.pooledRunnerMaxRssBytesOverride || 512 * 1024 * 1024
263
276
  this.pooledRunnerMaxLifetimeMs = this.pooledRunnerMaxLifetimeMsOverride || 60 * 60 * 1000
277
+ this.pooledRunnerSilenceTimeoutMsOverride = typeof pooledRunnerSilenceTimeoutMs === "number" ? pooledRunnerSilenceTimeoutMs : undefined
278
+ this.pooledRunnerSilenceTimeoutMs = this.pooledRunnerSilenceTimeoutMsOverride ?? POOLED_CHILD_SILENCE_TIMEOUT_MS
264
279
  this.runnerMaxOldSpaceSizeMb = this.runnerMaxOldSpaceSizeMbOverride || 2048
265
280
  this.runnerMallocArenaMax = this.runnerMallocArenaMaxOverride || 4
266
281
  /**
@@ -429,6 +444,7 @@ export default class BackgroundJobsWorker {
429
444
  if (typeof this.pooledRunnerMaxJobsOverride !== "number") this.pooledRunnerMaxJobs = poolConfig.pooledRunnerMaxJobs
430
445
  if (typeof this.pooledRunnerMaxRssBytesOverride !== "number") this.pooledRunnerMaxRssBytes = poolConfig.pooledRunnerMaxRssBytes
431
446
  if (typeof this.pooledRunnerMaxLifetimeMsOverride !== "number") this.pooledRunnerMaxLifetimeMs = poolConfig.pooledRunnerMaxLifetimeMs
447
+ if (typeof this.pooledRunnerSilenceTimeoutMsOverride !== "number" && typeof poolConfig.pooledRunnerSilenceTimeoutMs === "number") this.pooledRunnerSilenceTimeoutMs = poolConfig.pooledRunnerSilenceTimeoutMs
432
448
  if (typeof this.runnerMaxOldSpaceSizeMbOverride !== "number" && typeof poolConfig.runnerMaxOldSpaceSizeMb === "number") this.runnerMaxOldSpaceSizeMb = poolConfig.runnerMaxOldSpaceSizeMb
433
449
  if (typeof this.runnerMallocArenaMaxOverride !== "number" && typeof poolConfig.runnerMallocArenaMax === "number") this.runnerMallocArenaMax = poolConfig.runnerMallocArenaMax
434
450
 
@@ -1279,6 +1295,7 @@ export default class BackgroundJobsWorker {
1279
1295
  const timeoutTimer = this._armPooledJobTimeout({child, payload})
1280
1296
 
1281
1297
  state.inflight.set(payload.id, {payload, resolve: resolvePooledJob, pooledJob, timeoutTimer})
1298
+ this._notePooledChildActivity({child, state})
1282
1299
  try {
1283
1300
  child.send({type: "job", payload, sharedTransactionBroker: this._pooledJobSharedTransactionBrokerConfig()})
1284
1301
  } catch (error) {
@@ -1403,7 +1420,7 @@ export default class BackgroundJobsWorker {
1403
1420
  })
1404
1421
  this.pooledChildren.add(child)
1405
1422
  this.inflightProcessChildren.add(child)
1406
- this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false, started: false})
1423
+ this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false, started: false, lastActivityAtMs: Date.now()})
1407
1424
  child.on("message", (message) => this._handlePooledChildMessage({child, message}))
1408
1425
  child.once("exit", (exitCode, signal) => this._handlePooledChildFailure({
1409
1426
  child,
@@ -1438,6 +1455,7 @@ export default class BackgroundJobsWorker {
1438
1455
  if (!message || typeof message !== "object") return
1439
1456
  const record = /** @type {{type?: ReturnType<typeof JSON.parse>, childInstanceId?: ReturnType<typeof JSON.parse>, jobId?: ReturnType<typeof JSON.parse>, acknowledged?: ReturnType<typeof JSON.parse>, rssBytes?: ReturnType<typeof JSON.parse>, peakRssBytes?: ReturnType<typeof JSON.parse>, error?: ReturnType<typeof JSON.parse>}} */ (message)
1440
1457
  const state = this.pooledChildStates.get(child)
1458
+ this._notePooledChildActivity({child, state})
1441
1459
  if (record.type === "ready") {
1442
1460
  if (state) {
1443
1461
  state.started = true
@@ -1700,6 +1718,112 @@ export default class BackgroundJobsWorker {
1700
1718
  return false
1701
1719
  }
1702
1720
 
1721
+ /**
1722
+ * Records one pooled child's IPC activity and re-arms its silence watchdog while
1723
+ * that child still owns in-flight jobs. Every message the child sends — readiness,
1724
+ * acceptance, memory observation, shutdown observation, or job outcome — proves its
1725
+ * event loop is running, because a healthy child samples itself every 10 seconds
1726
+ * while jobs are in flight. Silence past the bound therefore means a wedged child
1727
+ * that can no longer prove it is alive, not a busy one.
1728
+ * @param {object} args - Activity details.
1729
+ * @param {import("node:child_process").ChildProcess} args.child - Pooled child.
1730
+ * @param {PooledChildState | undefined} args.state - Child state.
1731
+ * @returns {void}
1732
+ */
1733
+ _notePooledChildActivity({child, state}) {
1734
+ if (!state) return
1735
+ state.lastActivityAtMs = Date.now()
1736
+ if (state.inflight.size > 0 && !state.settling) this._armPooledChildSilenceTimer({child, state})
1737
+ }
1738
+
1739
+ /**
1740
+ * Resolves the effective pooled-child silence bound in ms, or null when disabled.
1741
+ * A non-positive or non-finite value disables the watchdog at whichever level
1742
+ * supplied it, matching the job-timeout contract.
1743
+ * @returns {number | null} - Bound in ms, or null when disabled.
1744
+ */
1745
+ _resolvePooledChildSilenceTimeoutMs() {
1746
+ const raw = this.pooledRunnerSilenceTimeoutMs
1747
+
1748
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return null
1749
+
1750
+ return Math.min(raw, MAX_FORKED_JOB_TIMEOUT_MS)
1751
+ }
1752
+
1753
+ /**
1754
+ * Re-arms one child's silence watchdog from its last observed activity. Only
1755
+ * children with in-flight jobs are watched: a child with nothing in flight cannot
1756
+ * strand work, and its periodic sampler is inactive then, so silence is not
1757
+ * evidence of a wedge.
1758
+ * @param {object} args - Watchdog details.
1759
+ * @param {import("node:child_process").ChildProcess} args.child - Pooled child.
1760
+ * @param {PooledChildState} args.state - Child state.
1761
+ * @returns {void}
1762
+ */
1763
+ _armPooledChildSilenceTimer({child, state}) {
1764
+ const timeoutMs = this._resolvePooledChildSilenceTimeoutMs()
1765
+ if (!(typeof timeoutMs === "number" && timeoutMs > 0)) return
1766
+
1767
+ if (state.silenceTimer) clearTimeout(state.silenceTimer)
1768
+ state.silenceTimer = setTimeout(() => this._onPooledChildSilence({child, state}), timeoutMs)
1769
+ if (typeof state.silenceTimer.unref === "function") state.silenceTimer.unref()
1770
+ }
1771
+
1772
+ /**
1773
+ * Fired when a pooled child with in-flight jobs stays silent past the bound. A
1774
+ * wedged child keeps its IPC channel open but stops running its own timers, so the
1775
+ * disconnect/exit path never fires and its jobs stay stranded until a human
1776
+ * notices. The worker declares the child dead: it requests shutdown under the exact
1777
+ * `child_silence` reason, reports the whole in-flight set as failed through the
1778
+ * established failure path (so the jobs requeue and capacity is re-advertised), and
1779
+ * signals the wedged process — SIGTERM, then SIGKILL after the existing grace — so
1780
+ * it actually goes away instead of blocking the event loop forever.
1781
+ * @param {object} args - Watchdog details.
1782
+ * @param {import("node:child_process").ChildProcess} args.child - Pooled child.
1783
+ * @param {PooledChildState} args.state - Child state.
1784
+ * @returns {void}
1785
+ */
1786
+ _onPooledChildSilence({child, state}) {
1787
+ state.silenceTimer = undefined
1788
+ if (state.settling || state.shutdownReason) return
1789
+ const silentMs = Date.now() - state.lastActivityAtMs
1790
+ const boundMs = this._resolvePooledChildSilenceTimeoutMs()
1791
+ if (!(typeof boundMs === "number" && silentMs >= boundMs)) return
1792
+
1793
+ const error = new Error(`Pooled background job runner went silent for ${silentMs}ms past the ${boundMs}ms bound — treated as wedged`)
1794
+ this._logPooledSilenceEvent({child, state, silentMs, boundMs})
1795
+ this._requestPooledChildShutdown({child, reason: "child_silence", signal: "SIGTERM"})
1796
+ void this._handlePooledChildFailure({child, error, origin: "child-silence", signal: null})
1797
+ state.silenceSigkillTimer = setTimeout(() => {
1798
+ state.silenceSigkillTimer = undefined
1799
+ this._signalPooledChild({child, signal: "SIGKILL"})
1800
+ }, this.forkedChildSigkillGraceMs)
1801
+ if (typeof state.silenceSigkillTimer.unref === "function") state.silenceSigkillTimer.unref()
1802
+ }
1803
+
1804
+ /**
1805
+ * Logs one compact JSON line naming a child the silence watchdog declared wedged
1806
+ * (the same stderr surface the memory observations use, which reaches the production
1807
+ * log), so the recovery is attributable after the fact instead of looking like a
1808
+ * random runner exit.
1809
+ * @param {object} args - Event details.
1810
+ * @param {import("node:child_process").ChildProcess} args.child - Pooled child.
1811
+ * @param {PooledChildState} args.state - Child state.
1812
+ * @param {number} args.silentMs - Observed silence in ms.
1813
+ * @param {number} args.boundMs - Configured bound in ms.
1814
+ * @returns {void}
1815
+ */
1816
+ _logPooledSilenceEvent({child, state, silentMs, boundMs}) {
1817
+ console.error(JSON.stringify({
1818
+ event: "pooled-child-silence",
1819
+ childInstanceId: state.childInstanceId ?? null,
1820
+ childPid: child.pid ?? null,
1821
+ silentMs,
1822
+ boundMs,
1823
+ jobs: state.inflight.size
1824
+ }))
1825
+ }
1826
+
1703
1827
  /**
1704
1828
  * Logs one compact pooled-capacity event to the worker's own stderr (the
1705
1829
  * surface that reaches the production log). Used by the bounded liveness
@@ -1848,6 +1972,8 @@ export default class BackgroundJobsWorker {
1848
1972
  // the SIGKILL grace from a timeout kill, and every armed per-job backstop.
1849
1973
  if (state.timeoutSigkillTimer) clearTimeout(state.timeoutSigkillTimer)
1850
1974
  if (state.shutdownSignalTimer) clearTimeout(state.shutdownSignalTimer)
1975
+ if (state.silenceTimer) clearTimeout(state.silenceTimer)
1976
+ if (state.silenceSigkillTimer) clearTimeout(state.silenceSigkillTimer)
1851
1977
  for (const inflightEntry of state.inflight.values()) {
1852
1978
  if (inflightEntry.timeoutTimer) clearTimeout(inflightEntry.timeoutTimer)
1853
1979
  }
@@ -199,6 +199,7 @@
199
199
  * @property {number} [pooledRunnerMaxJobs] - Number of sequential jobs a pooled child runs before it is replaced, bounding process-level resource accumulation. Default: `100`.
200
200
  * @property {number} [pooledRunnerMaxRssBytes] - Peak RSS (the kernel `VmHWM` high-water mark reported by the child) at which a pooled child is replaced after an acknowledged job. The child's peak, not its settled RSS at job-outcome time, is the trigger, so a burst working-set that reclaims before the outcome still retires the child. Default: `536870912` (512 MiB).
201
201
  * @property {number} [pooledRunnerMaxLifetimeMs] - Age after an acknowledged job at which a pooled child is replaced. Default: `3600000` (one hour).
202
+ * @property {number} [pooledRunnerSilenceTimeoutMs] - IPC-silence bound for a pooled child with in-flight jobs. A child that sends nothing (readiness, acceptance, memory observation, or job outcome) for longer than this is declared wedged: its in-flight jobs are reported failed for requeue, its capacity is re-advertised, and the wedged process is terminated. A healthy child samples itself every 10 seconds while jobs run, so the bound only fires on a child that cannot prove it is alive. Default: `120000`.
202
203
  * @property {number} [runnerMaxOldSpaceSizeMb] - V8 heap cap (`--max-old-space-size`, MiB) applied to every background-jobs runner child process (pooled, forked, and spawned) so a single child can never commit multiple GBs of heap. Default: `2048`.
203
204
  * @property {number} [runnerMallocArenaMax] - `MALLOC_ARENA_MAX` exported to every background-jobs runner child process, bounding glibc per-thread arenas. Defaults to an inherited `MALLOC_ARENA_MAX`, else `4`.
204
205
  * @property {Record<string, {maxConcurrent?: number, priority?: number}>} [queues] - Per-queue
@@ -662,11 +663,20 @@
662
663
  * }} NormalizedVelociousSyncClientConfiguration
663
664
  */
664
665
 
666
+ /**
667
+ * Normalized sync configuration as stored and returned by
668
+ * `Configuration.getSyncConfiguration()`. Same shape as
669
+ * {@link VelociousSyncConfiguration} with the defaults filled in.
670
+ * @typedef {VelociousSyncConfiguration & {
671
+ * client: NormalizedVelociousSyncClientConfiguration | undefined
672
+ * }} NormalizedVelociousSyncConfiguration
673
+ */
674
+
665
675
  /**
666
676
  * Velocious sync configuration.
667
677
  * @typedef {object} VelociousSyncConfiguration
668
678
  * @property {VelociousSyncApiConfiguration} [api] - Auto-mounts the Velocious sync changes/replay endpoints for this resource class.
669
- * @property {NormalizedVelociousSyncClientConfiguration | undefined} client - Normalized client-side sync configuration consumed by `SyncClient.fromConfiguration(...)`.
679
+ * @property {VelociousSyncClientConfiguration} [client] - Client-side sync configuration as written by the app. The framework normalizes it into {@link NormalizedVelociousSyncClientConfiguration} (see {@link NormalizedVelociousSyncConfiguration}).
670
680
  * @property {import("./sync/device-identity.js").SyncJsonWebKey | null} [deviceCertificateBackendPublicKey] - Public backend key used to verify offline device certificates for sync replay.
671
681
  * @property {number} [changeFeedRetentionSize] - Number of accepted server changes retained before clients must refresh from snapshot.
672
682
  * @property {Array<import("./sync/offline-grant.js").OfflineGrantSigningKey>} offlineGrantSigningKeys - Signing keys used to issue and verify offline grants. Secrets must never be exposed to clients.
@@ -848,7 +848,7 @@ export default class VelociousConfiguration {
848
848
 
849
849
  /**
850
850
  * Runs get sync configuration.
851
- * @returns {import("./configuration-types.js").VelociousSyncConfiguration} - Sync configuration.
851
+ * @returns {import("./configuration-types.js").NormalizedVelociousSyncConfiguration} - Sync configuration.
852
852
  */
853
853
  getSyncConfiguration() {
854
854
  return this._sync
@@ -867,7 +867,7 @@ export default class VelociousConfiguration {
867
867
  /**
868
868
  * Normalizes sync configuration.
869
869
  * @param {import("./configuration-types.js").VelociousSyncConfiguration | undefined} sync - Sync configuration.
870
- * @returns {import("./configuration-types.js").VelociousSyncConfiguration} - Normalized sync configuration.
870
+ * @returns {import("./configuration-types.js").NormalizedVelociousSyncConfiguration} - Normalized sync configuration.
871
871
  */
872
872
  _normalizeSyncConfiguration(sync) {
873
873
  const api = sync?.api
@@ -1820,6 +1820,7 @@ export default class VelociousConfiguration {
1820
1820
  const envPooledRunnerMaxJobsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_JOBS
1821
1821
  const envPooledRunnerMaxRssBytesRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_RSS_BYTES
1822
1822
  const envPooledRunnerMaxLifetimeMsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_LIFETIME_MS
1823
+ const envPooledRunnerSilenceTimeoutMsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_SILENCE_TIMEOUT_MS
1823
1824
  const envRunnerMaxOldSpaceSizeMbRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_RUNNER_MAX_OLD_SPACE_SIZE_MB
1824
1825
  const envRunnerMallocArenaMaxRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_RUNNER_MALLOC_ARENA_MAX
1825
1826
  const envDispatchStrategy = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_DISPATCH_STRATEGY
@@ -1833,6 +1834,7 @@ export default class VelociousConfiguration {
1833
1834
  const envPooledRunnerMaxJobs = envPooledRunnerMaxJobsRaw ? Number(envPooledRunnerMaxJobsRaw) : undefined
1834
1835
  const envPooledRunnerMaxRssBytes = envPooledRunnerMaxRssBytesRaw ? Number(envPooledRunnerMaxRssBytesRaw) : undefined
1835
1836
  const envPooledRunnerMaxLifetimeMs = envPooledRunnerMaxLifetimeMsRaw ? Number(envPooledRunnerMaxLifetimeMsRaw) : undefined
1837
+ const envPooledRunnerSilenceTimeoutMs = envPooledRunnerSilenceTimeoutMsRaw ? Number(envPooledRunnerSilenceTimeoutMsRaw) : undefined
1836
1838
  const envRunnerMaxOldSpaceSizeMb = envRunnerMaxOldSpaceSizeMbRaw ? Number(envRunnerMaxOldSpaceSizeMbRaw) : undefined
1837
1839
  const envRunnerMallocArenaMax = envRunnerMallocArenaMaxRaw ? Number(envRunnerMallocArenaMaxRaw) : undefined
1838
1840
  const envPollInterval = envPollIntervalRaw ? Number(envPollIntervalRaw) : undefined
@@ -1870,6 +1872,9 @@ export default class VelociousConfiguration {
1870
1872
  const pooledRunnerMaxLifetimeMs = typeof configured.pooledRunnerMaxLifetimeMs === "number" && Number.isFinite(configured.pooledRunnerMaxLifetimeMs) && configured.pooledRunnerMaxLifetimeMs >= 1
1871
1873
  ? configured.pooledRunnerMaxLifetimeMs
1872
1874
  : (!("pooledRunnerMaxLifetimeMs" in configured) && typeof envPooledRunnerMaxLifetimeMs === "number" && Number.isFinite(envPooledRunnerMaxLifetimeMs) && envPooledRunnerMaxLifetimeMs >= 1 ? envPooledRunnerMaxLifetimeMs : 60 * 60 * 1000)
1875
+ const pooledRunnerSilenceTimeoutMs = typeof configured.pooledRunnerSilenceTimeoutMs === "number" && Number.isFinite(configured.pooledRunnerSilenceTimeoutMs) && configured.pooledRunnerSilenceTimeoutMs >= 1
1876
+ ? configured.pooledRunnerSilenceTimeoutMs
1877
+ : (!("pooledRunnerSilenceTimeoutMs" in configured) && typeof envPooledRunnerSilenceTimeoutMs === "number" && Number.isFinite(envPooledRunnerSilenceTimeoutMs) && envPooledRunnerSilenceTimeoutMs >= 1 ? envPooledRunnerSilenceTimeoutMs : 120000)
1873
1878
  // Runner children get a hard V8 heap cap and a bounded glibc arena count
1874
1879
  // so one child can never commit multiple GBs of heap or unbounded
1875
1880
  // allocator arenas. The cap stays far above observed runner usage; an
@@ -1930,7 +1935,7 @@ export default class VelociousConfiguration {
1930
1935
 
1931
1936
  const jobClasses = this.getBackgroundJobClasses()
1932
1937
 
1933
- return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, mode, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, dispatchStrategy, pollIntervalMs, drainStoreOperationTimeoutMs, scheduledEnqueueTimeoutMs, queues, jobClasses, jobTimeoutMs, retention, generationId, initialGenerationState, lifecycleSocketPath}
1938
+ return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, mode, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, pooledRunnerSilenceTimeoutMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, dispatchStrategy, pollIntervalMs, drainStoreOperationTimeoutMs, scheduledEnqueueTimeoutMs, queues, jobClasses, jobTimeoutMs, retention, generationId, initialGenerationState, lifecycleSocketPath}
1934
1939
  }
1935
1940
 
1936
1941
  /**