velocious 1.0.528 → 1.0.529
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/README.md +11 -1
- package/build/background-jobs/forked-runner-child.js +2 -1
- package/build/background-jobs/job-runner.js +14 -2
- package/build/background-jobs/json-socket.js +2 -0
- package/build/background-jobs/main.js +47 -5
- package/build/background-jobs/pooled-runner-child.js +78 -0
- package/build/background-jobs/runner-process-title.js +6 -0
- package/build/background-jobs/scheduler.js +28 -5
- package/build/background-jobs/store.js +79 -15
- package/build/background-jobs/types.js +5 -5
- package/build/background-jobs/worker.js +184 -5
- package/build/configuration-types.js +4 -0
- package/build/configuration.js +21 -1
- package/build/src/background-jobs/forked-runner-child.js +3 -2
- package/build/src/background-jobs/job-runner.d.ts +7 -0
- package/build/src/background-jobs/job-runner.d.ts.map +1 -1
- package/build/src/background-jobs/job-runner.js +14 -3
- package/build/src/background-jobs/json-socket.d.ts +2 -0
- package/build/src/background-jobs/json-socket.d.ts.map +1 -1
- package/build/src/background-jobs/json-socket.js +3 -1
- package/build/src/background-jobs/main.d.ts +16 -0
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +46 -5
- package/build/src/background-jobs/pooled-runner-child.d.ts +2 -0
- package/build/src/background-jobs/pooled-runner-child.d.ts.map +1 -0
- package/build/src/background-jobs/pooled-runner-child.js +77 -0
- package/build/src/background-jobs/runner-process-title.d.ts +3 -0
- package/build/src/background-jobs/runner-process-title.d.ts.map +1 -0
- package/build/src/background-jobs/runner-process-title.js +6 -0
- package/build/src/background-jobs/scheduler.d.ts +16 -2
- package/build/src/background-jobs/scheduler.d.ts.map +1 -1
- package/build/src/background-jobs/scheduler.js +27 -6
- package/build/src/background-jobs/store.d.ts +24 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +81 -15
- package/build/src/background-jobs/types.d.ts +10 -8
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +6 -6
- package/build/src/background-jobs/worker.d.ts +78 -1
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +193 -6
- package/build/src/configuration-types.d.ts +20 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +5 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +22 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/background-jobs/forked-runner-child.js +2 -1
- package/src/background-jobs/job-runner.js +14 -2
- package/src/background-jobs/json-socket.js +2 -0
- package/src/background-jobs/main.js +47 -5
- package/src/background-jobs/pooled-runner-child.js +78 -0
- package/src/background-jobs/runner-process-title.js +6 -0
- package/src/background-jobs/scheduler.js +28 -5
- package/src/background-jobs/store.js +79 -15
- package/src/background-jobs/types.js +5 -5
- package/src/background-jobs/worker.js +184 -5
- package/src/configuration-types.js +4 -0
- package/src/configuration.js +21 -1
|
@@ -18,8 +18,25 @@ const ORPHANED_AFTER_MS = 2 * 60 * 60 * 1000
|
|
|
18
18
|
/**
|
|
19
19
|
* Execution modes.
|
|
20
20
|
* @type {import("./types.js").BackgroundJobExecutionMode[]} */
|
|
21
|
-
const EXECUTION_MODES = ["inline", "forked", "spawned"]
|
|
22
|
-
|
|
21
|
+
const EXECUTION_MODES = ["inline", "forked", "pooled", "spawned"]
|
|
22
|
+
/**
|
|
23
|
+
* Execution mode for a new enqueue that names neither `executionMode` nor the
|
|
24
|
+
* legacy `forked` flag. Pooled routes the job to a warm, reused local runner
|
|
25
|
+
* process — the same isolation as forked without paying a fresh process per job.
|
|
26
|
+
* @type {import("./types.js").BackgroundJobExecutionMode} */
|
|
27
|
+
const DEFAULT_EXECUTION_MODE = "pooled"
|
|
28
|
+
/**
|
|
29
|
+
* Execution mode a legacy `forked = true` row (persisted before the
|
|
30
|
+
* `execution_mode` column existed) backfills to. It must stay `"forked"` so an
|
|
31
|
+
* upgrade never silently reinterprets already-persisted forked jobs as pooled —
|
|
32
|
+
* distinct from {@link DEFAULT_EXECUTION_MODE}, which only governs new enqueues.
|
|
33
|
+
* @type {import("./types.js").BackgroundJobExecutionMode} */
|
|
34
|
+
const LEGACY_FORKED_EXECUTION_MODE = "forked"
|
|
35
|
+
// Pooled jobs persist with the legacy-safe `forked` mode so a pre-pool main can
|
|
36
|
+
// deserialize and run them during a rolling upgrade. Old versions ignore the
|
|
37
|
+
// nullable handoff id on queued rows, making it a backward-compatible marker.
|
|
38
|
+
const POOLED_HANDOFF_ID_PREFIX = "velocious-pooled:"
|
|
39
|
+
const POOLED_QUEUED_HANDOFF_ID = `${POOLED_HANDOFF_ID_PREFIX}queued`
|
|
23
40
|
const DEFAULT_QUEUE = "default"
|
|
24
41
|
// Queue-derived durable concurrency keys are namespaced so they can't collide
|
|
25
42
|
// with explicit caller-supplied concurrencyKeys.
|
|
@@ -138,7 +155,7 @@ export default class BackgroundJobsStore {
|
|
|
138
155
|
job_name: jobName,
|
|
139
156
|
args_json: argsJson,
|
|
140
157
|
forked: executionMode !== "inline",
|
|
141
|
-
execution_mode: executionMode,
|
|
158
|
+
execution_mode: executionMode === "pooled" ? LEGACY_FORKED_EXECUTION_MODE : executionMode,
|
|
142
159
|
queue,
|
|
143
160
|
max_retries: maxRetries,
|
|
144
161
|
attempts: 0,
|
|
@@ -146,7 +163,8 @@ export default class BackgroundJobsStore {
|
|
|
146
163
|
scheduled_at_ms: scheduledAtMs,
|
|
147
164
|
created_at_ms: now,
|
|
148
165
|
concurrency_key: concurrency?.concurrencyKey || null,
|
|
149
|
-
max_concurrency: concurrency?.maxConcurrency || null
|
|
166
|
+
max_concurrency: concurrency?.maxConcurrency || null,
|
|
167
|
+
handoff_id: executionMode === "pooled" ? POOLED_QUEUED_HANDOFF_ID : null
|
|
150
168
|
}
|
|
151
169
|
})
|
|
152
170
|
})
|
|
@@ -221,11 +239,7 @@ export default class BackgroundJobsStore {
|
|
|
221
239
|
if (typeof forked === "boolean") {
|
|
222
240
|
query = query.where({forked})
|
|
223
241
|
}
|
|
224
|
-
if (executionMode) {
|
|
225
|
-
const executionModes = Array.isArray(executionMode) ? executionMode : [executionMode]
|
|
226
|
-
|
|
227
|
-
query = query.where({execution_mode: executionModes})
|
|
228
|
-
}
|
|
242
|
+
if (executionMode) query = this._whereExecutionMode({db, executionMode, query})
|
|
229
243
|
|
|
230
244
|
if (scheduledAtOperator === "<=") {
|
|
231
245
|
const priorityOrder = this._queuePriorityOrderSql(db)
|
|
@@ -401,12 +415,14 @@ export default class BackgroundJobsStore {
|
|
|
401
415
|
await this.ensureReady()
|
|
402
416
|
|
|
403
417
|
const handedOffAtMs = Date.now()
|
|
404
|
-
const handoffId = randomUUID()
|
|
405
418
|
|
|
406
419
|
return await this._withDb(async (db) => await this._transactionResult(db, async () => {
|
|
407
420
|
const queuedJob = await this._getJobRowById(db, jobId)
|
|
408
421
|
if (!queuedJob || queuedJob.status !== "queued") return null
|
|
409
422
|
if (queuedJob.concurrencyKey && !(await this._reserveConcurrency(db, queuedJob.concurrencyKey))) return null
|
|
423
|
+
const handoffId = queuedJob.executionMode === "pooled"
|
|
424
|
+
? `${POOLED_HANDOFF_ID_PREFIX}${randomUUID()}`
|
|
425
|
+
: randomUUID()
|
|
410
426
|
|
|
411
427
|
const affectedRows = await this._updateAffectedRows(db, {
|
|
412
428
|
tableName: JOBS_TABLE,
|
|
@@ -480,7 +496,7 @@ export default class BackgroundJobsStore {
|
|
|
480
496
|
status: "queued",
|
|
481
497
|
scheduled_at_ms: Date.now(),
|
|
482
498
|
handed_off_at_ms: null,
|
|
483
|
-
handoff_id: null,
|
|
499
|
+
handoff_id: job.executionMode === "pooled" ? POOLED_QUEUED_HANDOFF_ID : null,
|
|
484
500
|
worker_id: null
|
|
485
501
|
},
|
|
486
502
|
conditions: {handoff_id: handoffId, id: jobId, status: "handed_off"}
|
|
@@ -990,7 +1006,7 @@ export default class BackgroundJobsStore {
|
|
|
990
1006
|
const executionModeColumnSql = db.quoteColumn("execution_mode")
|
|
991
1007
|
|
|
992
1008
|
await db.query(
|
|
993
|
-
`UPDATE ${tableNameSql} SET ${executionModeColumnSql} = ${db.quote(
|
|
1009
|
+
`UPDATE ${tableNameSql} SET ${executionModeColumnSql} = ${db.quote(LEGACY_FORKED_EXECUTION_MODE)} ` +
|
|
994
1010
|
`WHERE ${forkedColumnSql} = ${db.quote(true)} AND ${executionModeColumnSql} IS NULL`
|
|
995
1011
|
)
|
|
996
1012
|
await db.query(
|
|
@@ -1081,6 +1097,7 @@ export default class BackgroundJobsStore {
|
|
|
1081
1097
|
scheduledAt,
|
|
1082
1098
|
shouldRetry
|
|
1083
1099
|
})
|
|
1100
|
+
if (shouldRetry && job.executionMode === "pooled") update.handoff_id = POOLED_QUEUED_HANDOFF_ID
|
|
1084
1101
|
|
|
1085
1102
|
const affectedRows = await this._updateAffectedRows(db, {
|
|
1086
1103
|
tableName: JOBS_TABLE,
|
|
@@ -1189,8 +1206,10 @@ export default class BackgroundJobsStore {
|
|
|
1189
1206
|
* @returns {import("./types.js").BackgroundJobRow} - Normalized job row.
|
|
1190
1207
|
*/
|
|
1191
1208
|
_normalizeJobRow(row) {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1209
|
+
const persistedExecutionMode = row.execution_mode ? String(row.execution_mode) : null
|
|
1210
|
+
const handoffId = row.handoff_id ? String(row.handoff_id) : null
|
|
1211
|
+
const executionMode = persistedExecutionMode
|
|
1212
|
+
? this._normalizePersistedExecutionMode({executionMode: persistedExecutionMode, handoffId})
|
|
1194
1213
|
: this._normalizeExecutionMode({forked: this._normalizeBoolean(row.forked)})
|
|
1195
1214
|
|
|
1196
1215
|
return {
|
|
@@ -1206,7 +1225,7 @@ export default class BackgroundJobsStore {
|
|
|
1206
1225
|
scheduledAtMs: this._normalizeNumber(row.scheduled_at_ms),
|
|
1207
1226
|
createdAtMs: this._normalizeNumber(row.created_at_ms),
|
|
1208
1227
|
handedOffAtMs: this._normalizeNumber(row.handed_off_at_ms),
|
|
1209
|
-
handoffId
|
|
1228
|
+
handoffId,
|
|
1210
1229
|
completedAtMs: this._normalizeNumber(row.completed_at_ms),
|
|
1211
1230
|
failedAtMs: this._normalizeNumber(row.failed_at_ms),
|
|
1212
1231
|
orphanedAtMs: this._normalizeNumber(row.orphaned_at_ms),
|
|
@@ -1506,7 +1525,11 @@ export default class BackgroundJobsStore {
|
|
|
1506
1525
|
if (executionMode) {
|
|
1507
1526
|
return this._normalizeExecutionModeName(executionMode)
|
|
1508
1527
|
}
|
|
1528
|
+
// The legacy `forked` flag stays authoritative when supplied: `false` is
|
|
1529
|
+
// inline and `true` is forked (never the pooled default). Only an enqueue
|
|
1530
|
+
// that names neither option falls through to the pooled default.
|
|
1509
1531
|
if (options?.forked === false) return "inline"
|
|
1532
|
+
if (options?.forked === true) return LEGACY_FORKED_EXECUTION_MODE
|
|
1510
1533
|
|
|
1511
1534
|
return DEFAULT_EXECUTION_MODE
|
|
1512
1535
|
}
|
|
@@ -1524,6 +1547,47 @@ export default class BackgroundJobsStore {
|
|
|
1524
1547
|
throw new Error(`Invalid background job executionMode: ${executionMode}`)
|
|
1525
1548
|
}
|
|
1526
1549
|
|
|
1550
|
+
/**
|
|
1551
|
+
* Normalizes a legacy-safe persisted execution mode.
|
|
1552
|
+
* @param {object} args - Options.
|
|
1553
|
+
* @param {string} args.executionMode - Persisted legacy execution mode.
|
|
1554
|
+
* @param {string | null} args.handoffId - Persisted handoff id or pooled marker.
|
|
1555
|
+
* @returns {import("./types.js").BackgroundJobExecutionMode} - Runtime execution mode.
|
|
1556
|
+
*/
|
|
1557
|
+
_normalizePersistedExecutionMode({executionMode, handoffId}) {
|
|
1558
|
+
if (executionMode === LEGACY_FORKED_EXECUTION_MODE && handoffId?.startsWith(POOLED_HANDOFF_ID_PREFIX)) return "pooled"
|
|
1559
|
+
|
|
1560
|
+
return this._normalizeExecutionModeName(executionMode)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Filters persisted legacy-safe execution modes.
|
|
1565
|
+
* @param {object} args - Options.
|
|
1566
|
+
* @param {import("../database/drivers/base.js").default} args.db - Database connection.
|
|
1567
|
+
* @param {import("./types.js").BackgroundJobExecutionMode | import("./types.js").BackgroundJobExecutionMode[]} args.executionMode - Runtime modes.
|
|
1568
|
+
* @param {import("../database/query/index.js").default} args.query - Query to filter.
|
|
1569
|
+
* @returns {import("../database/query/index.js").default} - Filtered query.
|
|
1570
|
+
*/
|
|
1571
|
+
_whereExecutionMode({db, executionMode, query}) {
|
|
1572
|
+
const executionModes = Array.isArray(executionMode) ? executionMode : [executionMode]
|
|
1573
|
+
/** @type {string[]} */
|
|
1574
|
+
const conditions = []
|
|
1575
|
+
const executionModeColumn = db.quoteColumn("execution_mode")
|
|
1576
|
+
const handoffIdColumn = db.quoteColumn("handoff_id")
|
|
1577
|
+
|
|
1578
|
+
for (const mode of executionModes) {
|
|
1579
|
+
if (mode === "pooled") {
|
|
1580
|
+
conditions.push(`(${executionModeColumn} = ${db.quote(LEGACY_FORKED_EXECUTION_MODE)} AND ${handoffIdColumn} = ${db.quote(POOLED_QUEUED_HANDOFF_ID)})`)
|
|
1581
|
+
} else if (mode === LEGACY_FORKED_EXECUTION_MODE) {
|
|
1582
|
+
conditions.push(`(${executionModeColumn} = ${db.quote(mode)} AND (${handoffIdColumn} IS NULL OR ${handoffIdColumn} <> ${db.quote(POOLED_QUEUED_HANDOFF_ID)}))`)
|
|
1583
|
+
} else {
|
|
1584
|
+
conditions.push(`${executionModeColumn} = ${db.quote(mode)}`)
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
return query.where(`(${conditions.join(" OR ")})`)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1527
1591
|
/**
|
|
1528
1592
|
* Runs parse args.
|
|
1529
1593
|
* @param {?} value - Input value.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @typedef {"inline" | "forked" | "spawned"} BackgroundJobExecutionMode
|
|
4
|
+
* @typedef {"inline" | "forked" | "pooled" | "spawned"} BackgroundJobExecutionMode
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {object} BackgroundJobHandoff
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
12
|
* @typedef {object} BackgroundJobOptions
|
|
13
|
-
* @property {BackgroundJobExecutionMode} [executionMode] - How the job should run. Defaults to `"forked"
|
|
14
|
-
* @property {boolean} [forked] - Compatibility alias: `false` maps to `"inline"` and `true` maps to `"forked"`.
|
|
13
|
+
* @property {BackgroundJobExecutionMode} [executionMode] - How the job should run. Defaults to `"pooled"` (a warm, reused local runner process). `"forked"` runs the job in a fresh `child_process.fork()` child, `"spawned"` in a detached CLI runner, and `"inline"` inside the worker process.
|
|
14
|
+
* @property {boolean} [forked] - Compatibility alias: `false` maps to `"inline"` and `true` maps to `"forked"`. Omitting both `forked` and `executionMode` uses the default `"pooled"` mode.
|
|
15
15
|
* @property {number} [maxRetries] - Max retries for a failed job before it is marked failed.
|
|
16
16
|
* @property {string} [queue] - Queue name. Defaults to `"default"`. When the queue has a configured cap in `backgroundJobs.queues`, that cap is enforced cluster-wide.
|
|
17
17
|
* @property {string} [concurrencyKey] - Opaque non-empty key used to share a concurrency cap. Overrides any queue-derived cap.
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
* @typedef {"worker" | "client" | "reporter"} BackgroundJobSocketRole
|
|
68
68
|
*/
|
|
69
69
|
/**
|
|
70
|
-
* @typedef {{type: "hello", role: BackgroundJobSocketRole, supportsHandoffIdReporting?: boolean, supportsHeartbeat?: boolean, workerId?: string}} BackgroundJobHelloMessage
|
|
71
|
-
* @typedef {{type: "ready", acceptsForked?: boolean, acceptsInline?: boolean, acceptsSpawned?: boolean}} BackgroundJobReadyMessage
|
|
70
|
+
* @typedef {{type: "hello", role: BackgroundJobSocketRole, supportsHandoffIdReporting?: boolean, supportsHeartbeat?: boolean, supportsPooled?: boolean, workerId?: string}} BackgroundJobHelloMessage
|
|
71
|
+
* @typedef {{type: "ready", acceptsForked?: boolean, acceptsInline?: boolean, acceptsPooled?: boolean, acceptsSpawned?: boolean}} BackgroundJobReadyMessage
|
|
72
72
|
* @typedef {{type: "draining"}} BackgroundJobDrainingMessage
|
|
73
73
|
* @typedef {{type: "heartbeat", workerId?: string}} BackgroundJobHeartbeatMessage
|
|
74
74
|
* @typedef {{type: "enqueue", jobName: string, args?: Array<?>, options?: BackgroundJobOptions}} BackgroundJobEnqueueMessage
|
|
@@ -19,6 +19,7 @@ const FORKED_CHILD_SIGKILL_GRACE_MS = 5000
|
|
|
19
19
|
*/
|
|
20
20
|
const MAX_FORKED_JOB_TIMEOUT_MS = 2_147_483_647
|
|
21
21
|
const FORKED_RUNNER_ENTRY_PATH = fileURLToPath(new URL("./forked-runner-child.js", import.meta.url))
|
|
22
|
+
const POOLED_RUNNER_ENTRY_PATH = fileURLToPath(new URL("./pooled-runner-child.js", import.meta.url))
|
|
22
23
|
/** How often the worker sends a liveness heartbeat to the main. */
|
|
23
24
|
const HEARTBEAT_INTERVAL_MS = 15000
|
|
24
25
|
/** TCP keepalive so a half-open connection to the main surfaces as a close. */
|
|
@@ -26,7 +27,25 @@ const SOCKET_KEEPALIVE_MS = 10000
|
|
|
26
27
|
/**
|
|
27
28
|
* Execution modes.
|
|
28
29
|
* @type {import("./types.js").BackgroundJobExecutionMode[]} */
|
|
29
|
-
const EXECUTION_MODES = ["inline", "forked", "spawned"]
|
|
30
|
+
const EXECUTION_MODES = ["inline", "forked", "pooled", "spawned"]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes a candidate pooled-runner count or job limit.
|
|
34
|
+
* @param {number | undefined} value - Candidate positive integer.
|
|
35
|
+
* @returns {number | undefined} - Normalized value.
|
|
36
|
+
*/
|
|
37
|
+
function positiveInteger(value) {
|
|
38
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normalizes a candidate pooled-runner resource limit.
|
|
43
|
+
* @param {number | undefined} value - Candidate positive number.
|
|
44
|
+
* @returns {number | undefined} - Normalized value.
|
|
45
|
+
*/
|
|
46
|
+
function positiveNumber(value) {
|
|
47
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined
|
|
48
|
+
}
|
|
30
49
|
|
|
31
50
|
/**
|
|
32
51
|
* Per-forked-child timeout bookkeeping.
|
|
@@ -46,11 +65,15 @@ export default class BackgroundJobsWorker {
|
|
|
46
65
|
* @param {number} [args.port] - Port.
|
|
47
66
|
* @param {number} [args.maxConcurrentForkedJobs] - Override the process runner concurrency cap from `configuration.getBackgroundJobsConfig()`.
|
|
48
67
|
* @param {number} [args.maxConcurrentInlineJobs] - Override the inline-job concurrency cap from `configuration.getBackgroundJobsConfig()`.
|
|
68
|
+
* @param {number} [args.pooledRunnerCount] - Override the pooled runner count.
|
|
69
|
+
* @param {number} [args.pooledRunnerMaxJobs] - Override the per-runner recycle job count.
|
|
70
|
+
* @param {number} [args.pooledRunnerMaxRssBytes] - Override the per-runner recycle RSS limit.
|
|
71
|
+
* @param {number} [args.pooledRunnerMaxLifetimeMs] - Override the per-runner recycle lifetime.
|
|
49
72
|
* @param {number} [args.forkedChildSigkillGraceMs] - Override the grace period between SIGTERM and SIGKILL when reaping lingering process runners on stop.
|
|
50
73
|
* @param {number} [args.heartbeatIntervalMs] - Override the liveness heartbeat interval (default 15000ms).
|
|
51
74
|
* @param {number} [args.jobTimeoutMs] - Override the forked-job wall-clock timeout from `configuration.getBackgroundJobsConfig()`. `0` disables it.
|
|
52
75
|
*/
|
|
53
|
-
constructor({configuration, host, port, maxConcurrentForkedJobs, maxConcurrentInlineJobs, forkedChildSigkillGraceMs, heartbeatIntervalMs, jobTimeoutMs} = {}) {
|
|
76
|
+
constructor({configuration, host, port, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, forkedChildSigkillGraceMs, heartbeatIntervalMs, jobTimeoutMs} = {}) {
|
|
54
77
|
/**
|
|
55
78
|
* Narrows the runtime value to the documented type.
|
|
56
79
|
* @type {Promise<import("../configuration.js").default>} */
|
|
@@ -86,6 +109,14 @@ export default class BackgroundJobsWorker {
|
|
|
86
109
|
* Narrows the runtime value to the documented type.
|
|
87
110
|
* @type {number} */
|
|
88
111
|
this.maxConcurrentForkedJobs = this.maxConcurrentForkedJobsOverride || 4
|
|
112
|
+
this.pooledRunnerCountOverride = positiveInteger(pooledRunnerCount)
|
|
113
|
+
this.pooledRunnerMaxJobsOverride = positiveInteger(pooledRunnerMaxJobs)
|
|
114
|
+
this.pooledRunnerMaxRssBytesOverride = positiveNumber(pooledRunnerMaxRssBytes)
|
|
115
|
+
this.pooledRunnerMaxLifetimeMsOverride = positiveNumber(pooledRunnerMaxLifetimeMs)
|
|
116
|
+
this.pooledRunnerCount = this.pooledRunnerCountOverride || 4
|
|
117
|
+
this.pooledRunnerMaxJobs = this.pooledRunnerMaxJobsOverride || 100
|
|
118
|
+
this.pooledRunnerMaxRssBytes = this.pooledRunnerMaxRssBytesOverride || 512 * 1024 * 1024
|
|
119
|
+
this.pooledRunnerMaxLifetimeMs = this.pooledRunnerMaxLifetimeMsOverride || 60 * 60 * 1000
|
|
89
120
|
/**
|
|
90
121
|
* Grace period between SIGTERM and SIGKILL when reaping process runners that
|
|
91
122
|
* outlast a bounded shutdown drain.
|
|
@@ -148,6 +179,12 @@ export default class BackgroundJobsWorker {
|
|
|
148
179
|
* @type {Set<import("node:child_process").ChildProcess>}
|
|
149
180
|
*/
|
|
150
181
|
this.inflightProcessChildren = new Set()
|
|
182
|
+
/** @type {Set<Promise<void>>} */
|
|
183
|
+
this.inflightPooledJobs = new Set()
|
|
184
|
+
/** @type {Set<import("node:child_process").ChildProcess>} */
|
|
185
|
+
this.pooledChildren = new Set()
|
|
186
|
+
/** @type {Map<import("node:child_process").ChildProcess, {createdAtMs: number, jobsRun: number, payload?: import("./types.js").BackgroundJobPayload & {id: string}, resolve?: (value: void) => void, settling?: boolean}>} */
|
|
187
|
+
this.pooledChildStates = new Map()
|
|
151
188
|
}
|
|
152
189
|
|
|
153
190
|
/**
|
|
@@ -171,6 +208,11 @@ export default class BackgroundJobsWorker {
|
|
|
171
208
|
|
|
172
209
|
this.maxConcurrentForkedJobs = config.maxConcurrentForkedJobs || this.maxConcurrentForkedJobs
|
|
173
210
|
}
|
|
211
|
+
const poolConfig = this.configuration.getBackgroundJobsConfig()
|
|
212
|
+
if (typeof this.pooledRunnerCountOverride !== "number") this.pooledRunnerCount = poolConfig.pooledRunnerCount
|
|
213
|
+
if (typeof this.pooledRunnerMaxJobsOverride !== "number") this.pooledRunnerMaxJobs = poolConfig.pooledRunnerMaxJobs
|
|
214
|
+
if (typeof this.pooledRunnerMaxRssBytesOverride !== "number") this.pooledRunnerMaxRssBytes = poolConfig.pooledRunnerMaxRssBytes
|
|
215
|
+
if (typeof this.pooledRunnerMaxLifetimeMsOverride !== "number") this.pooledRunnerMaxLifetimeMs = poolConfig.pooledRunnerMaxLifetimeMs
|
|
174
216
|
|
|
175
217
|
this.statusReporter = new BackgroundJobsStatusReporter({
|
|
176
218
|
configuration: this.configuration,
|
|
@@ -211,6 +253,7 @@ export default class BackgroundJobsWorker {
|
|
|
211
253
|
}
|
|
212
254
|
|
|
213
255
|
await this._drainInflight(this.inflightInlineJobs, timeoutMs)
|
|
256
|
+
await this._drainInflight(this.inflightPooledJobs, timeoutMs)
|
|
214
257
|
await this._drainInflight(this.inflightProcessJobs, timeoutMs)
|
|
215
258
|
await this._terminateProcessChildren()
|
|
216
259
|
// Give in-flight result reports (now decoupled from job slots) a bounded
|
|
@@ -311,7 +354,7 @@ export default class BackgroundJobsWorker {
|
|
|
311
354
|
})
|
|
312
355
|
|
|
313
356
|
socket.on("connect", () => {
|
|
314
|
-
jsonSocket.send({type: "hello", role: "worker", supportsHandoffIdReporting: true, supportsHeartbeat: true, workerId: this.workerId})
|
|
357
|
+
jsonSocket.send({type: "hello", role: "worker", supportsHandoffIdReporting: true, supportsHeartbeat: true, supportsPooled: true, workerId: this.workerId})
|
|
315
358
|
this._sendReadyIfRunning()
|
|
316
359
|
this._startHeartbeat()
|
|
317
360
|
})
|
|
@@ -364,6 +407,11 @@ export default class BackgroundJobsWorker {
|
|
|
364
407
|
|
|
365
408
|
const executionMode = this._executionModeForPayload(identifiedPayload)
|
|
366
409
|
|
|
410
|
+
if (executionMode === "pooled") {
|
|
411
|
+
this._trackPooledJob(this._runPooledJob(identifiedPayload))
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
367
415
|
if (executionMode !== "inline") {
|
|
368
416
|
this._trackProcessJob(this._startProcessJob({executionMode, payload: identifiedPayload}))
|
|
369
417
|
return
|
|
@@ -433,7 +481,9 @@ export default class BackgroundJobsWorker {
|
|
|
433
481
|
|
|
434
482
|
if (executionMode) return this._normalizeExecutionMode(executionMode)
|
|
435
483
|
|
|
436
|
-
|
|
484
|
+
if (options.forked === false) return "inline"
|
|
485
|
+
if (options.forked === true) return "forked"
|
|
486
|
+
return "pooled"
|
|
437
487
|
}
|
|
438
488
|
|
|
439
489
|
/**
|
|
@@ -532,17 +582,146 @@ export default class BackgroundJobsWorker {
|
|
|
532
582
|
_readyMessage() {
|
|
533
583
|
const acceptsProcessJob = this.inflightProcessJobs.size < this.maxConcurrentForkedJobs
|
|
534
584
|
const acceptsInline = this.inflightInlineJobs.size < this.maxConcurrentInlineJobs
|
|
585
|
+
const acceptsPooled = this.inflightPooledJobs.size < this.pooledRunnerCount
|
|
535
586
|
|
|
536
|
-
if (!acceptsProcessJob && !acceptsInline) return null
|
|
587
|
+
if (!acceptsProcessJob && !acceptsInline && !acceptsPooled) return null
|
|
537
588
|
|
|
538
589
|
return {
|
|
539
590
|
type: "ready",
|
|
540
591
|
acceptsForked: acceptsProcessJob,
|
|
541
592
|
acceptsInline,
|
|
593
|
+
acceptsPooled,
|
|
542
594
|
acceptsSpawned: acceptsProcessJob
|
|
543
595
|
}
|
|
544
596
|
}
|
|
545
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Tracks a pooled job and re-advertises capacity.
|
|
600
|
+
* @param {Promise<void>} pooledJob - Pooled job promise.
|
|
601
|
+
* @returns {void}
|
|
602
|
+
*/
|
|
603
|
+
_trackPooledJob(pooledJob) {
|
|
604
|
+
/** @type {Promise<void>} */
|
|
605
|
+
let inflight
|
|
606
|
+
inflight = pooledJob.finally(() => {
|
|
607
|
+
this.inflightPooledJobs.delete(inflight)
|
|
608
|
+
if (!this.shouldStop) this._sendReadyIfRunning()
|
|
609
|
+
})
|
|
610
|
+
this.inflightPooledJobs.add(inflight)
|
|
611
|
+
this._sendReadyIfRunning()
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Runs a payload in an idle pooled child.
|
|
616
|
+
* @param {import("./types.js").BackgroundJobPayload & {id: string}} payload - Job payload.
|
|
617
|
+
* @returns {Promise<void>} - Resolves after the durable report.
|
|
618
|
+
*/
|
|
619
|
+
_runPooledJob(payload) {
|
|
620
|
+
let child = [...this.pooledChildren].find((candidate) => !this.pooledChildStates.get(candidate)?.payload)
|
|
621
|
+
if (!child) child = this._createPooledChild()
|
|
622
|
+
const state = this.pooledChildStates.get(child)
|
|
623
|
+
if (!state) throw new Error("Pooled runner state missing")
|
|
624
|
+
|
|
625
|
+
return new Promise((resolve) => {
|
|
626
|
+
state.payload = payload
|
|
627
|
+
state.resolve = resolve
|
|
628
|
+
try {
|
|
629
|
+
child.send({type: "job", payload})
|
|
630
|
+
} catch (error) {
|
|
631
|
+
this._handlePooledChildFailure({child, error})
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Creates a reusable pooled child.
|
|
638
|
+
* @returns {import("node:child_process").ChildProcess} - New pooled child.
|
|
639
|
+
*/
|
|
640
|
+
_createPooledChild() {
|
|
641
|
+
const configuration = this.configuration
|
|
642
|
+
if (!configuration) throw new Error("Background jobs worker configuration not initialized")
|
|
643
|
+
const config = configuration.getBackgroundJobsConfig()
|
|
644
|
+
const child = fork(POOLED_RUNNER_ENTRY_PATH, [], {
|
|
645
|
+
cwd: configuration.getDirectory(), execArgv: [], stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
646
|
+
env: Object.assign({}, process.env, {VELOCIOUS_ENV: configuration.getEnvironment(), VELOCIOUS_BACKGROUND_JOBS_HOST: config.host, VELOCIOUS_BACKGROUND_JOBS_PORT: `${config.port}`})
|
|
647
|
+
})
|
|
648
|
+
this.pooledChildren.add(child)
|
|
649
|
+
this.inflightProcessChildren.add(child)
|
|
650
|
+
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0})
|
|
651
|
+
child.on("message", (message) => this._handlePooledChildMessage({child, message}))
|
|
652
|
+
child.once("exit", (code, signal) => this._handlePooledChildFailure({child, error: new Error(`Pooled background job runner exited: code=${code} signal=${signal || "none"}`)}))
|
|
653
|
+
child.once("error", (error) => this._handlePooledChildFailure({child, error}))
|
|
654
|
+
return child
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Handles a pooled child's durable-report acknowledgement.
|
|
659
|
+
* @param {object} args - Message details.
|
|
660
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
661
|
+
* @param {?} args.message - IPC message.
|
|
662
|
+
* @returns {void}
|
|
663
|
+
*/
|
|
664
|
+
_handlePooledChildMessage({child, message}) {
|
|
665
|
+
if (!message || typeof message !== "object") return
|
|
666
|
+
const record = /** @type {{type?: ?, jobId?: ?, acknowledged?: ?, rssBytes?: ?, error?: ?}} */ (message)
|
|
667
|
+
const state = this.pooledChildStates.get(child)
|
|
668
|
+
if (record.type !== "job-outcome" || !state?.payload || record.jobId !== state.payload.id || state.settling) return
|
|
669
|
+
if (record.acknowledged !== true) {
|
|
670
|
+
void this._handlePooledChildFailure({child, error: new Error(typeof record.error === "string" ? record.error : "Pooled runner terminal report was not acknowledged")})
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
const resolve = state.resolve
|
|
674
|
+
state.payload = undefined
|
|
675
|
+
state.resolve = undefined
|
|
676
|
+
state.jobsRun += 1
|
|
677
|
+
if (resolve) resolve(undefined)
|
|
678
|
+
const rssBytes = typeof record.rssBytes === "number" ? record.rssBytes : Number.POSITIVE_INFINITY
|
|
679
|
+
const runnerAgeMs = Date.now() - state.createdAtMs
|
|
680
|
+
if (state.jobsRun >= this.pooledRunnerMaxJobs || rssBytes >= this.pooledRunnerMaxRssBytes || runnerAgeMs >= this.pooledRunnerMaxLifetimeMs || this.shouldStop) this._retirePooledChild(child)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Retires an idle pooled child.
|
|
685
|
+
* @param {import("node:child_process").ChildProcess} child - Child process to retire.
|
|
686
|
+
* @returns {void}
|
|
687
|
+
*/
|
|
688
|
+
_retirePooledChild(child) {
|
|
689
|
+
this.pooledChildren.delete(child)
|
|
690
|
+
this.pooledChildStates.delete(child)
|
|
691
|
+
this.inflightProcessChildren.delete(child)
|
|
692
|
+
child.kill("SIGTERM")
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Removes and reports an unhealthy pooled child.
|
|
697
|
+
* @param {object} args - Failure details.
|
|
698
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
699
|
+
* @param {?} args.error - Failure.
|
|
700
|
+
* @returns {Promise<void>}
|
|
701
|
+
*/
|
|
702
|
+
async _handlePooledChildFailure({child, error}) {
|
|
703
|
+
const state = this.pooledChildStates.get(child)
|
|
704
|
+
if (state?.settling) return
|
|
705
|
+
if (state) state.settling = true
|
|
706
|
+
this.pooledChildren.delete(child)
|
|
707
|
+
this.inflightProcessChildren.delete(child)
|
|
708
|
+
if (!state?.payload) {
|
|
709
|
+
this.pooledChildStates.delete(child)
|
|
710
|
+
return
|
|
711
|
+
}
|
|
712
|
+
const payload = state.payload
|
|
713
|
+
await this._reportJobResult({
|
|
714
|
+
jobId: payload.id,
|
|
715
|
+
status: "failed",
|
|
716
|
+
error,
|
|
717
|
+
handoffId: payload.handoffId,
|
|
718
|
+
handedOffAtMs: payload.handedOffAtMs,
|
|
719
|
+
workerId: payload.workerId || this.workerId
|
|
720
|
+
})
|
|
721
|
+
this.pooledChildStates.delete(child)
|
|
722
|
+
if (state.resolve) state.resolve(undefined)
|
|
723
|
+
}
|
|
724
|
+
|
|
546
725
|
/**
|
|
547
726
|
* Runs run job inline.
|
|
548
727
|
* @param {import("./types.js").BackgroundJobPayload} payload - Payload.
|
|
@@ -164,6 +164,10 @@
|
|
|
164
164
|
* allowed to keep in flight. Default: `4`. This is a per-worker safety cap;
|
|
165
165
|
* for workload-shaped limits use per-queue caps (`queues`) instead, which are
|
|
166
166
|
* enforced cluster-wide and are immune to duplicate worker processes.
|
|
167
|
+
* @property {number} [pooledRunnerCount] - Number of warm, reusable child runners owned by each worker. Pooled capacity is separate from inline and forked/spawned capacity. Default: `4`.
|
|
168
|
+
* @property {number} [pooledRunnerMaxJobs] - Number of sequential jobs a pooled child runs before it is replaced, bounding process-level resource accumulation. Default: `100`.
|
|
169
|
+
* @property {number} [pooledRunnerMaxRssBytes] - RSS bytes after an acknowledged job at which a pooled child is replaced. Default: `536870912` (512 MiB).
|
|
170
|
+
* @property {number} [pooledRunnerMaxLifetimeMs] - Age after an acknowledged job at which a pooled child is replaced. Default: `3600000` (one hour).
|
|
167
171
|
* @property {Record<string, {maxConcurrent?: number, priority?: number}>} [queues] - Per-queue
|
|
168
172
|
* concurrency caps and dispatch priorities, Sidekiq-style. A job declares its queue (static
|
|
169
173
|
* `queue` on the job class, or the `queue` enqueue option; defaults to `"default"`), and
|
package/src/configuration.js
CHANGED
|
@@ -1268,12 +1268,20 @@ export default class VelociousConfiguration {
|
|
|
1268
1268
|
const envDatabaseIdentifier = process.env.VELOCIOUS_BACKGROUND_JOBS_DATABASE_IDENTIFIER
|
|
1269
1269
|
const envMaxConcurrentForkedRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_FORKED_JOBS
|
|
1270
1270
|
const envMaxConcurrentRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_INLINE_JOBS
|
|
1271
|
+
const envPooledRunnerCountRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_COUNT
|
|
1272
|
+
const envPooledRunnerMaxJobsRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_JOBS
|
|
1273
|
+
const envPooledRunnerMaxRssBytesRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_RSS_BYTES
|
|
1274
|
+
const envPooledRunnerMaxLifetimeMsRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_LIFETIME_MS
|
|
1271
1275
|
const envDispatchStrategy = process.env.VELOCIOUS_BACKGROUND_JOBS_DISPATCH_STRATEGY
|
|
1272
1276
|
const envPollIntervalRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POLL_INTERVAL_MS
|
|
1273
1277
|
const envJobTimeoutRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS
|
|
1274
1278
|
const envPort = envPortRaw ? Number(envPortRaw) : undefined
|
|
1275
1279
|
const envMaxConcurrentForked = envMaxConcurrentForkedRaw ? Number(envMaxConcurrentForkedRaw) : undefined
|
|
1276
1280
|
const envMaxConcurrent = envMaxConcurrentRaw ? Number(envMaxConcurrentRaw) : undefined
|
|
1281
|
+
const envPooledRunnerCount = envPooledRunnerCountRaw ? Number(envPooledRunnerCountRaw) : undefined
|
|
1282
|
+
const envPooledRunnerMaxJobs = envPooledRunnerMaxJobsRaw ? Number(envPooledRunnerMaxJobsRaw) : undefined
|
|
1283
|
+
const envPooledRunnerMaxRssBytes = envPooledRunnerMaxRssBytesRaw ? Number(envPooledRunnerMaxRssBytesRaw) : undefined
|
|
1284
|
+
const envPooledRunnerMaxLifetimeMs = envPooledRunnerMaxLifetimeMsRaw ? Number(envPooledRunnerMaxLifetimeMsRaw) : undefined
|
|
1277
1285
|
const envPollInterval = envPollIntervalRaw ? Number(envPollIntervalRaw) : undefined
|
|
1278
1286
|
const envJobTimeout = envJobTimeoutRaw ? Number(envJobTimeoutRaw) : undefined
|
|
1279
1287
|
const configured = this._backgroundJobs || {}
|
|
@@ -1288,6 +1296,18 @@ export default class VelociousConfiguration {
|
|
|
1288
1296
|
const maxConcurrentForkedJobs = typeof configured.maxConcurrentForkedJobs === "number" && configured.maxConcurrentForkedJobs >= 1
|
|
1289
1297
|
? configured.maxConcurrentForkedJobs
|
|
1290
1298
|
: (typeof envMaxConcurrentForked === "number" && Number.isFinite(envMaxConcurrentForked) && envMaxConcurrentForked >= 1 ? envMaxConcurrentForked : 4)
|
|
1299
|
+
const pooledRunnerCount = typeof configured.pooledRunnerCount === "number" && Number.isFinite(configured.pooledRunnerCount) && Number.isInteger(configured.pooledRunnerCount) && configured.pooledRunnerCount >= 1
|
|
1300
|
+
? configured.pooledRunnerCount
|
|
1301
|
+
: (!("pooledRunnerCount" in configured) && typeof envPooledRunnerCount === "number" && Number.isFinite(envPooledRunnerCount) && Number.isInteger(envPooledRunnerCount) && envPooledRunnerCount >= 1 ? envPooledRunnerCount : 4)
|
|
1302
|
+
const pooledRunnerMaxJobs = typeof configured.pooledRunnerMaxJobs === "number" && Number.isFinite(configured.pooledRunnerMaxJobs) && Number.isInteger(configured.pooledRunnerMaxJobs) && configured.pooledRunnerMaxJobs >= 1
|
|
1303
|
+
? configured.pooledRunnerMaxJobs
|
|
1304
|
+
: (!("pooledRunnerMaxJobs" in configured) && typeof envPooledRunnerMaxJobs === "number" && Number.isFinite(envPooledRunnerMaxJobs) && Number.isInteger(envPooledRunnerMaxJobs) && envPooledRunnerMaxJobs >= 1 ? envPooledRunnerMaxJobs : 100)
|
|
1305
|
+
const pooledRunnerMaxRssBytes = typeof configured.pooledRunnerMaxRssBytes === "number" && Number.isFinite(configured.pooledRunnerMaxRssBytes) && configured.pooledRunnerMaxRssBytes >= 1
|
|
1306
|
+
? configured.pooledRunnerMaxRssBytes
|
|
1307
|
+
: (!("pooledRunnerMaxRssBytes" in configured) && typeof envPooledRunnerMaxRssBytes === "number" && Number.isFinite(envPooledRunnerMaxRssBytes) && envPooledRunnerMaxRssBytes >= 1 ? envPooledRunnerMaxRssBytes : 512 * 1024 * 1024)
|
|
1308
|
+
const pooledRunnerMaxLifetimeMs = typeof configured.pooledRunnerMaxLifetimeMs === "number" && Number.isFinite(configured.pooledRunnerMaxLifetimeMs) && configured.pooledRunnerMaxLifetimeMs >= 1
|
|
1309
|
+
? configured.pooledRunnerMaxLifetimeMs
|
|
1310
|
+
: (!("pooledRunnerMaxLifetimeMs" in configured) && typeof envPooledRunnerMaxLifetimeMs === "number" && Number.isFinite(envPooledRunnerMaxLifetimeMs) && envPooledRunnerMaxLifetimeMs >= 1 ? envPooledRunnerMaxLifetimeMs : 60 * 60 * 1000)
|
|
1291
1311
|
const dispatchStrategyRaw = configured.dispatchStrategy || envDispatchStrategy
|
|
1292
1312
|
const dispatchStrategy = dispatchStrategyRaw === "polling" ? "polling" : "beacon"
|
|
1293
1313
|
const pollIntervalMs = typeof configured.pollIntervalMs === "number" && configured.pollIntervalMs >= 1
|
|
@@ -1316,7 +1336,7 @@ export default class VelociousConfiguration {
|
|
|
1316
1336
|
: 60 * 60 * 1000
|
|
1317
1337
|
}
|
|
1318
1338
|
|
|
1319
|
-
return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, dispatchStrategy, pollIntervalMs, queues, jobTimeoutMs, retention}
|
|
1339
|
+
return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, dispatchStrategy, pollIntervalMs, queues, jobTimeoutMs, retention}
|
|
1320
1340
|
}
|
|
1321
1341
|
|
|
1322
1342
|
/**
|