velocious 1.0.529 → 1.0.532
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 +5 -2
- package/build/background-jobs/job-runner.js +5 -3
- package/build/background-jobs/pooled-runner-child.js +53 -16
- package/build/background-jobs/worker.js +193 -42
- package/build/configuration-types.js +1 -0
- package/build/configuration.js +33 -4
- package/build/database/drivers/mysql/sql/alter-table.js +42 -0
- package/build/database/query/preloader/belongs-to.js +9 -9
- package/build/database/query/preloader/has-many.js +8 -8
- package/build/database/query/preloader/has-one.js +5 -5
- package/build/database/tenants/schema-cloner.js +46 -9
- package/build/src/background-jobs/job-runner.d.ts +3 -1
- package/build/src/background-jobs/job-runner.d.ts.map +1 -1
- package/build/src/background-jobs/job-runner.js +8 -4
- package/build/src/background-jobs/pooled-runner-child.js +49 -18
- package/build/src/background-jobs/worker.d.ts +85 -14
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +189 -45
- package/build/src/configuration-types.d.ts +5 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +2 -1
- package/build/src/configuration.d.ts +6 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +34 -4
- package/build/src/database/drivers/mysql/sql/alter-table.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/sql/alter-table.js +36 -1
- package/build/src/database/query/preloader/belongs-to.js +10 -12
- package/build/src/database/query/preloader/has-many.js +9 -11
- package/build/src/database/query/preloader/has-one.js +6 -7
- package/build/src/database/tenants/schema-cloner.d.ts +10 -0
- package/build/src/database/tenants/schema-cloner.d.ts.map +1 -1
- package/build/src/database/tenants/schema-cloner.js +36 -9
- package/package.json +3 -1
- package/src/background-jobs/job-runner.js +5 -3
- package/src/background-jobs/pooled-runner-child.js +53 -16
- package/src/background-jobs/worker.js +193 -42
- package/src/configuration-types.js +1 -0
- package/src/configuration.js +33 -4
- package/src/database/drivers/mysql/sql/alter-table.js +42 -0
- package/src/database/query/preloader/belongs-to.js +9 -9
- package/src/database/query/preloader/has-many.js +8 -8
- package/src/database/query/preloader/has-one.js +5 -5
- package/src/database/tenants/schema-cloner.js +46 -9
package/README.md
CHANGED
|
@@ -1977,6 +1977,7 @@ export default new Configuration({
|
|
|
1977
1977
|
maxConcurrentForkedJobs: 4,
|
|
1978
1978
|
maxConcurrentInlineJobs: 4,
|
|
1979
1979
|
pooledRunnerCount: 4,
|
|
1980
|
+
pooledRunnerConcurrency: 1,
|
|
1980
1981
|
pooledRunnerMaxJobs: 100,
|
|
1981
1982
|
pooledRunnerMaxRssBytes: 536870912,
|
|
1982
1983
|
pooledRunnerMaxLifetimeMs: 3600000,
|
|
@@ -2008,11 +2009,11 @@ VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS=5400000
|
|
|
2008
2009
|
|
|
2009
2010
|
`maxConcurrentInlineJobs` (default: `4`) caps how many `executionMode: "inline"` jobs a single `background-jobs-worker` process runs in parallel. Concurrency is at the JS event-loop level: every job in flight shares the worker's process and DB connection pool, so the cap should fit the pool, not the CPU count. Forking remains the right tool when you need memory isolation across long-running jobs or want to use more cores. The older `forked: false` option still maps to inline mode.
|
|
2010
2011
|
|
|
2011
|
-
New jobs default to `executionMode: "pooled"`: a worker runs them
|
|
2012
|
+
New jobs default to `executionMode: "pooled"`: a worker runs them in warm, reusable Node child runners. `pooledRunnerCount` (default: `4`) bounds this independent per-worker pool, and `pooledRunnerConcurrency` (default: `1`) sets how many jobs each child runs at once on its own event loop, so total pooled capacity is `pooledRunnerCount × pooledRunnerConcurrency` — raise concurrency for I/O-bound jobs to get high throughput from a bounded, isolated set of processes. `pooledRunnerCount`, `pooledRunnerConcurrency`, and `pooledRunnerMaxJobs` must be finite positive integers; the RSS and lifetime limits must be finite positive numbers. A child is recycled after an acknowledged job when it reaches `pooledRunnerMaxJobs` (default: `100`), `pooledRunnerMaxRssBytes` (default: `536870912`, or 512 MiB), or `pooledRunnerMaxLifetimeMs` (default: `3600000`, or one hour). Pooled rows remain readable by older mains during rolling upgrades and run there with legacy one-shot fork behavior. `forked: true` remains an explicit alias for `"forked"`; `forked: false` remains inline. See [execution modes and pooled runners](docs/background-jobs.md#execution-modes-and-pooled-runners).
|
|
2012
2013
|
|
|
2013
2014
|
`maxConcurrentForkedJobs` (default: `4`) caps how many out-of-process `executionMode: "forked"` or `executionMode: "spawned"` jobs one worker may keep in flight. Forked jobs use `child_process.fork()` with an attached IPC channel. After the main process acknowledges their durable status report, forked and spawned one-shot runners exit without waiting for graceful Beacon/database teardown; the OS closes their process-owned resources. A missing or rejected status acknowledgement makes the runner exit as failed instead of reporting clean success. Spawned jobs use the legacy `background-jobs-runner` CLI process via `child_process.spawn()` and are only for callers that intentionally want that spawned behavior.
|
|
2014
2015
|
|
|
2015
|
-
`jobTimeoutMs` (or `VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS`, milliseconds; default: disabled) is a wall-clock backstop for
|
|
2016
|
+
`jobTimeoutMs` (or `VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS`, milliseconds; default: disabled) is a wall-clock backstop for `"forked"` and `"pooled"` jobs. A job still running after the timeout is terminated (`SIGTERM`, then `SIGKILL` after the reaping grace) and reported `failed`, so a genuinely-hung job can't pin a worker's capacity — and its whole-app boot and DB connections — indefinitely (notably a retired-release worker draining after a deploy). For a **pooled** job the whole child running it is killed, so its concurrent in-flight siblings on that child are also reported `failed` and requeued — a hung JS job can't be cancelled any other way — before a replacement child is spawned. It's a coarse safety net, not per-job tuning: it applies to every forked and pooled job, so set it well above the longest legitimate job. Omit it, or set `null`/`<= 0`, to disable. `"inline"` jobs are not covered — they share the worker's process and can't be killed without killing the worker. See [docs/background-jobs.md](docs/background-jobs.md#job-timeout-hung-runner-backstop).
|
|
2016
2017
|
|
|
2017
2018
|
### Dispatch strategy
|
|
2018
2019
|
|
|
@@ -2326,4 +2327,6 @@ npx velocious db:tenants:migrate projectTenant --parallel 20
|
|
|
2326
2327
|
|
|
2327
2328
|
At runtime, the apartment-style `Tenant` façade (`velocious/build/src/tenants/tenant.js`) is the single entry point: `Tenant.with(tenant, callback)` / `Tenant.current()` to switch into and read a tenant context, `Tenant.each({identifier, callback, parallel?, filter?})` to run a callback within every provider-listed tenant, and `Tenant.drop({identifier, tenant})` (plus the `db:tenants:drop` CLI command) to drop a tenant's database through the provider's `dropDatabase` hook. `Tenant.with` and `Tenant.each` run their callbacks inside `ensureConnections`, so switching into a tenant establishes its database connections (global + tenant) and passes them to the callback — the tenant is immediately queryable without the caller wiring up connections (already-open connections are reused, so nesting does not double-connect). `Tenant.with` is generic over its callback's return type, so a value returned from inside the tenant context comes back to the caller with its type preserved (no cast needed). `Tenant.aggregateAcross({identifier, aggregates, keyColumns, subquery, tenants?, filter?})` runs one aggregate over the same table across many tenant databases and returns the merged result — grouping tenants by server and using a single cross-database `UNION ALL` where the driver supports two-part `` `database`.`table` `` references (MySQL/MariaDB) or one query per tenant otherwise (PostgreSQL/SQLite/MSSQL).
|
|
2328
2329
|
|
|
2330
|
+
`SchemaCloner` adds a missing auto-increment column and its separate source unique index in one schema alteration, including on MySQL/MariaDB where an auto-increment column must be keyed when it is created.
|
|
2331
|
+
|
|
2329
2332
|
See [docs/tenant-databases.md](docs/tenant-databases.md) for the full configuration and migration pattern.
|
|
@@ -78,9 +78,10 @@ function runnerProcessTitle(JobClass, payload) {
|
|
|
78
78
|
* @param {import("./types.js").BackgroundJobPayload} payload - Payload.
|
|
79
79
|
* @param {object} [options] - Runner options.
|
|
80
80
|
* @param {boolean} [options.closeConnections] - Whether to gracefully close framework connections after the job.
|
|
81
|
+
* @param {boolean} [options.manageProcessTitle] - Whether to set the per-job process title and restore it afterwards. Off for concurrent pooled runners, where interleaved snapshot/restore of the single process-wide `process.title` would corrupt it; the pooled child owns an aggregate title instead.
|
|
81
82
|
* @returns {Promise<void>} - Resolves when complete.
|
|
82
83
|
*/
|
|
83
|
-
export default async function runJobPayload(payload, {closeConnections = true} = {}) {
|
|
84
|
+
export default async function runJobPayload(payload, {closeConnections = true, manageProcessTitle = true} = {}) {
|
|
84
85
|
const configuration = await configurationResolver()
|
|
85
86
|
configuration.setCurrent()
|
|
86
87
|
await configuration.initialize({type: "background-jobs-runner"})
|
|
@@ -98,8 +99,9 @@ export default async function runJobPayload(payload, {closeConnections = true} =
|
|
|
98
99
|
|
|
99
100
|
// Name the process after the job it is running so `ps`/`top` show what each
|
|
100
101
|
// runner is doing; restored in the `finally` below when the job finishes.
|
|
102
|
+
// Skipped for concurrent pooled runners, whose child owns an aggregate title.
|
|
101
103
|
const previousTitle = process.title
|
|
102
|
-
process.title = runnerProcessTitle(JobClass, payload)
|
|
104
|
+
if (manageProcessTitle) process.title = runnerProcessTitle(JobClass, payload)
|
|
103
105
|
|
|
104
106
|
try {
|
|
105
107
|
try {
|
|
@@ -136,7 +138,7 @@ export default async function runJobPayload(payload, {closeConnections = true} =
|
|
|
136
138
|
} finally {
|
|
137
139
|
// Restore the runner's base title so a lingering/idle runner (or a reused
|
|
138
140
|
// one) doesn't misreport a finished job as still running.
|
|
139
|
-
process.title = previousTitle
|
|
141
|
+
if (manageProcessTitle) process.title = previousTitle
|
|
140
142
|
if (closeConnections) {
|
|
141
143
|
try {
|
|
142
144
|
await configuration.disconnectBeacon()
|
|
@@ -3,8 +3,32 @@
|
|
|
3
3
|
import runJobPayload, { BackgroundJobPerformedFailure } from "./job-runner.js"
|
|
4
4
|
import setRunnerProcessTitle from "./runner-process-title.js"
|
|
5
5
|
|
|
6
|
+
const BASE_PROCESS_TITLE = "velocious background-jobs-runner"
|
|
7
|
+
|
|
6
8
|
setRunnerProcessTitle()
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ids of jobs currently running in this child. A pooled child runs up to
|
|
12
|
+
* `pooledRunnerConcurrency` jobs at once (the worker only dispatches within that
|
|
13
|
+
* bound); the set dedupes a redelivered job id and lets each job settle
|
|
14
|
+
* independently.
|
|
15
|
+
* @type {Set<string>}
|
|
16
|
+
*/
|
|
17
|
+
const runningJobIds = new Set()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sets an aggregate process title from the current in-flight count. A child runs
|
|
21
|
+
* jobs concurrently, so a per-job title (which `runJobPayload` would snapshot and
|
|
22
|
+
* restore around a single job) cannot represent the process — interleaved
|
|
23
|
+
* completions would leave a stale label. Recomputing from `runningJobIds.size` is
|
|
24
|
+
* concurrency-safe and honest: `ps`/`top` show how many jobs the child is running.
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
function updateProcessTitle() {
|
|
28
|
+
const count = runningJobIds.size
|
|
29
|
+
|
|
30
|
+
process.title = count > 0 ? `${BASE_PROCESS_TITLE}: ${count} ${count === 1 ? "job" : "jobs"}` : BASE_PROCESS_TITLE
|
|
31
|
+
}
|
|
8
32
|
|
|
9
33
|
/**
|
|
10
34
|
* Checks whether an IPC value is a runnable pooled job message.
|
|
@@ -46,33 +70,46 @@ function sendOutcome({jobId, acknowledged, status, error}) {
|
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
73
|
+
* Runs one job concurrently with any siblings and reports its own terminal
|
|
74
|
+
* outcome. A single job's unexpected failure reports that job for reclamation
|
|
75
|
+
* (`acknowledged: false`) but does NOT take down the child — its concurrent
|
|
76
|
+
* siblings keep running. Only a process-level fault (which escapes every
|
|
77
|
+
* per-job try/catch) ends the child, which the worker sees as an exit and
|
|
78
|
+
* reclaims for the whole in-flight set.
|
|
79
|
+
* @param {import("./types.js").BackgroundJobPayload & {id: string}} payload - Job payload.
|
|
51
80
|
* @returns {Promise<void>} - Resolves after reporting.
|
|
52
81
|
*/
|
|
53
|
-
async function
|
|
54
|
-
if (running || !isJobMessage(message)) return
|
|
55
|
-
|
|
56
|
-
running = true
|
|
82
|
+
async function runJob(payload) {
|
|
57
83
|
try {
|
|
58
|
-
await runJobPayload(
|
|
59
|
-
await sendOutcome({jobId:
|
|
84
|
+
await runJobPayload(payload, {closeConnections: false, manageProcessTitle: false})
|
|
85
|
+
await sendOutcome({jobId: payload.id, acknowledged: true, status: "completed"})
|
|
60
86
|
} catch (error) {
|
|
61
87
|
if (error instanceof BackgroundJobPerformedFailure) {
|
|
62
|
-
await sendOutcome({jobId:
|
|
88
|
+
await sendOutcome({jobId: payload.id, acknowledged: true, status: "failed"})
|
|
63
89
|
} else {
|
|
64
90
|
const reportError = error instanceof Error ? error : new Error(String(error))
|
|
65
91
|
console.error("Pooled background job runner failed before terminal acknowledgement:", reportError)
|
|
66
|
-
await sendOutcome({jobId:
|
|
67
|
-
process.exitCode = 1
|
|
68
|
-
if (process.disconnect) process.disconnect()
|
|
69
|
-
setImmediate(() => process.exit(1))
|
|
92
|
+
await sendOutcome({jobId: payload.id, acknowledged: false, error: reportError})
|
|
70
93
|
}
|
|
71
94
|
} finally {
|
|
72
|
-
|
|
95
|
+
runningJobIds.delete(payload.id)
|
|
96
|
+
updateProcessTitle()
|
|
73
97
|
}
|
|
74
98
|
}
|
|
75
99
|
|
|
76
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Handles a job message, starting it alongside any concurrent siblings.
|
|
102
|
+
* @param {?} message - IPC message.
|
|
103
|
+
* @returns {void}
|
|
104
|
+
*/
|
|
105
|
+
function handleMessage(message) {
|
|
106
|
+
if (!isJobMessage(message) || runningJobIds.has(message.payload.id)) return
|
|
107
|
+
|
|
108
|
+
runningJobIds.add(message.payload.id)
|
|
109
|
+
updateProcessTitle()
|
|
110
|
+
void runJob(message.payload)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.on("message", (message) => handleMessage(message))
|
|
77
114
|
process.once("disconnect", () => process.exit(0))
|
|
78
115
|
for (const signal of ["SIGTERM", "SIGINT"]) process.once(signal, () => process.exit(1))
|
|
@@ -66,14 +66,15 @@ export default class BackgroundJobsWorker {
|
|
|
66
66
|
* @param {number} [args.maxConcurrentForkedJobs] - Override the process runner concurrency cap from `configuration.getBackgroundJobsConfig()`.
|
|
67
67
|
* @param {number} [args.maxConcurrentInlineJobs] - Override the inline-job concurrency cap from `configuration.getBackgroundJobsConfig()`.
|
|
68
68
|
* @param {number} [args.pooledRunnerCount] - Override the pooled runner count.
|
|
69
|
+
* @param {number} [args.pooledRunnerConcurrency] - Override the per-runner concurrency.
|
|
69
70
|
* @param {number} [args.pooledRunnerMaxJobs] - Override the per-runner recycle job count.
|
|
70
71
|
* @param {number} [args.pooledRunnerMaxRssBytes] - Override the per-runner recycle RSS limit.
|
|
71
72
|
* @param {number} [args.pooledRunnerMaxLifetimeMs] - Override the per-runner recycle lifetime.
|
|
72
73
|
* @param {number} [args.forkedChildSigkillGraceMs] - Override the grace period between SIGTERM and SIGKILL when reaping lingering process runners on stop.
|
|
73
74
|
* @param {number} [args.heartbeatIntervalMs] - Override the liveness heartbeat interval (default 15000ms).
|
|
74
|
-
* @param {number} [args.jobTimeoutMs] - Override the
|
|
75
|
+
* @param {number} [args.jobTimeoutMs] - Override the wall-clock timeout for forked and pooled jobs from `configuration.getBackgroundJobsConfig()`. `0` disables it.
|
|
75
76
|
*/
|
|
76
|
-
constructor({configuration, host, port, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, forkedChildSigkillGraceMs, heartbeatIntervalMs, jobTimeoutMs} = {}) {
|
|
77
|
+
constructor({configuration, host, port, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, forkedChildSigkillGraceMs, heartbeatIntervalMs, jobTimeoutMs} = {}) {
|
|
77
78
|
/**
|
|
78
79
|
* Narrows the runtime value to the documented type.
|
|
79
80
|
* @type {Promise<import("../configuration.js").default>} */
|
|
@@ -110,10 +111,12 @@ export default class BackgroundJobsWorker {
|
|
|
110
111
|
* @type {number} */
|
|
111
112
|
this.maxConcurrentForkedJobs = this.maxConcurrentForkedJobsOverride || 4
|
|
112
113
|
this.pooledRunnerCountOverride = positiveInteger(pooledRunnerCount)
|
|
114
|
+
this.pooledRunnerConcurrencyOverride = positiveInteger(pooledRunnerConcurrency)
|
|
113
115
|
this.pooledRunnerMaxJobsOverride = positiveInteger(pooledRunnerMaxJobs)
|
|
114
116
|
this.pooledRunnerMaxRssBytesOverride = positiveNumber(pooledRunnerMaxRssBytes)
|
|
115
117
|
this.pooledRunnerMaxLifetimeMsOverride = positiveNumber(pooledRunnerMaxLifetimeMs)
|
|
116
118
|
this.pooledRunnerCount = this.pooledRunnerCountOverride || 4
|
|
119
|
+
this.pooledRunnerConcurrency = this.pooledRunnerConcurrencyOverride || 1
|
|
117
120
|
this.pooledRunnerMaxJobs = this.pooledRunnerMaxJobsOverride || 100
|
|
118
121
|
this.pooledRunnerMaxRssBytes = this.pooledRunnerMaxRssBytesOverride || 512 * 1024 * 1024
|
|
119
122
|
this.pooledRunnerMaxLifetimeMs = this.pooledRunnerMaxLifetimeMsOverride || 60 * 60 * 1000
|
|
@@ -126,7 +129,7 @@ export default class BackgroundJobsWorker {
|
|
|
126
129
|
? forkedChildSigkillGraceMs
|
|
127
130
|
: FORKED_CHILD_SIGKILL_GRACE_MS
|
|
128
131
|
/**
|
|
129
|
-
* Constructor override for the forked
|
|
132
|
+
* Constructor override for the forked and pooled wall-clock job timeout. When unset the
|
|
130
133
|
* timeout is read from `configuration.getBackgroundJobsConfig().jobTimeoutMs`
|
|
131
134
|
* at fork time (default: disabled).
|
|
132
135
|
* @type {number | undefined}
|
|
@@ -183,7 +186,7 @@ export default class BackgroundJobsWorker {
|
|
|
183
186
|
this.inflightPooledJobs = new Set()
|
|
184
187
|
/** @type {Set<import("node:child_process").ChildProcess>} */
|
|
185
188
|
this.pooledChildren = new Set()
|
|
186
|
-
/** @type {Map<import("node:child_process").ChildProcess, {createdAtMs: number, jobsRun: number, payload
|
|
189
|
+
/** @type {Map<import("node:child_process").ChildProcess, {createdAtMs: number, jobsRun: number, inflight: Map<string, {payload: import("./types.js").BackgroundJobPayload & {id: string}, resolve?: (value: void) => void, timeoutTimer?: ReturnType<typeof setTimeout> | null}>, retiring: boolean, settling?: boolean, timeoutSigkillTimer?: ReturnType<typeof setTimeout> | null}>} */
|
|
187
190
|
this.pooledChildStates = new Map()
|
|
188
191
|
}
|
|
189
192
|
|
|
@@ -210,6 +213,7 @@ export default class BackgroundJobsWorker {
|
|
|
210
213
|
}
|
|
211
214
|
const poolConfig = this.configuration.getBackgroundJobsConfig()
|
|
212
215
|
if (typeof this.pooledRunnerCountOverride !== "number") this.pooledRunnerCount = poolConfig.pooledRunnerCount
|
|
216
|
+
if (typeof this.pooledRunnerConcurrencyOverride !== "number") this.pooledRunnerConcurrency = poolConfig.pooledRunnerConcurrency
|
|
213
217
|
if (typeof this.pooledRunnerMaxJobsOverride !== "number") this.pooledRunnerMaxJobs = poolConfig.pooledRunnerMaxJobs
|
|
214
218
|
if (typeof this.pooledRunnerMaxRssBytesOverride !== "number") this.pooledRunnerMaxRssBytes = poolConfig.pooledRunnerMaxRssBytes
|
|
215
219
|
if (typeof this.pooledRunnerMaxLifetimeMsOverride !== "number") this.pooledRunnerMaxLifetimeMs = poolConfig.pooledRunnerMaxLifetimeMs
|
|
@@ -582,7 +586,7 @@ export default class BackgroundJobsWorker {
|
|
|
582
586
|
_readyMessage() {
|
|
583
587
|
const acceptsProcessJob = this.inflightProcessJobs.size < this.maxConcurrentForkedJobs
|
|
584
588
|
const acceptsInline = this.inflightInlineJobs.size < this.maxConcurrentInlineJobs
|
|
585
|
-
const acceptsPooled = this.
|
|
589
|
+
const acceptsPooled = this._availablePooledSlots() > 0
|
|
586
590
|
|
|
587
591
|
if (!acceptsProcessJob && !acceptsInline && !acceptsPooled) return null
|
|
588
592
|
|
|
@@ -612,27 +616,112 @@ export default class BackgroundJobsWorker {
|
|
|
612
616
|
}
|
|
613
617
|
|
|
614
618
|
/**
|
|
615
|
-
*
|
|
619
|
+
* Free pooled slots across the pool: open slots in non-retiring children plus
|
|
620
|
+
* the slots we could add by spawning more children up to `pooledRunnerCount`.
|
|
621
|
+
* Retiring children (draining before replacement) never contribute capacity.
|
|
622
|
+
* @returns {number} - Number of pooled jobs the worker can accept right now.
|
|
623
|
+
*/
|
|
624
|
+
_availablePooledSlots() {
|
|
625
|
+
let openInExisting = 0
|
|
626
|
+
let nonRetiringChildren = 0
|
|
627
|
+
|
|
628
|
+
for (const child of this.pooledChildren) {
|
|
629
|
+
const state = this.pooledChildStates.get(child)
|
|
630
|
+
if (!state || state.retiring) continue
|
|
631
|
+
nonRetiringChildren += 1
|
|
632
|
+
openInExisting += this.pooledRunnerConcurrency - state.inflight.size
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const spawnableChildren = Math.max(0, this.pooledRunnerCount - nonRetiringChildren)
|
|
636
|
+
|
|
637
|
+
return openInExisting + spawnableChildren * this.pooledRunnerConcurrency
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Runs a payload on a pooled child with a free concurrency slot, spawning a
|
|
642
|
+
* new child when every non-retiring child is full and the pool is below
|
|
643
|
+
* `pooledRunnerCount`. Each child runs up to `pooledRunnerConcurrency` jobs at
|
|
644
|
+
* once on its own event loop.
|
|
616
645
|
* @param {import("./types.js").BackgroundJobPayload & {id: string}} payload - Job payload.
|
|
617
646
|
* @returns {Promise<void>} - Resolves after the durable report.
|
|
618
647
|
*/
|
|
619
648
|
_runPooledJob(payload) {
|
|
620
|
-
let child
|
|
649
|
+
let child
|
|
650
|
+
for (const candidate of this.pooledChildren) {
|
|
651
|
+
const state = this.pooledChildStates.get(candidate)
|
|
652
|
+
if (state && !state.retiring && state.inflight.size < this.pooledRunnerConcurrency) {
|
|
653
|
+
child = candidate
|
|
654
|
+
break
|
|
655
|
+
}
|
|
656
|
+
}
|
|
621
657
|
if (!child) child = this._createPooledChild()
|
|
622
658
|
const state = this.pooledChildStates.get(child)
|
|
623
659
|
if (!state) throw new Error("Pooled runner state missing")
|
|
624
660
|
|
|
625
661
|
return new Promise((resolve) => {
|
|
626
|
-
|
|
627
|
-
|
|
662
|
+
const timeoutTimer = this._armPooledJobTimeout({child, jobId: payload.id})
|
|
663
|
+
|
|
664
|
+
state.inflight.set(payload.id, {payload, resolve, timeoutTimer})
|
|
628
665
|
try {
|
|
629
666
|
child.send({type: "job", payload})
|
|
630
667
|
} catch (error) {
|
|
631
|
-
this._handlePooledChildFailure({child, error})
|
|
668
|
+
void this._handlePooledChildFailure({child, error})
|
|
632
669
|
}
|
|
633
670
|
})
|
|
634
671
|
}
|
|
635
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Arms a per-job wall-clock backstop for a pooled job. A pooled child hosts many
|
|
675
|
+
* concurrent jobs, so a single genuinely-hung job would otherwise pin its
|
|
676
|
+
* runner's concurrency slot forever — the lifetime recycle only retires a child
|
|
677
|
+
* once its in-flight set drains, which a hung job never does. On overrun the
|
|
678
|
+
* whole child is terminated so the hung job (and its siblings) requeue. Returns
|
|
679
|
+
* the timer, or null when no timeout is configured.
|
|
680
|
+
* @param {object} args - Options.
|
|
681
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
682
|
+
* @param {string} args.jobId - Job id whose overrun is guarded.
|
|
683
|
+
* @returns {ReturnType<typeof setTimeout> | null} - The armed timer, or null.
|
|
684
|
+
*/
|
|
685
|
+
_armPooledJobTimeout({child, jobId}) {
|
|
686
|
+
const timeoutMs = this._resolveJobTimeoutMs()
|
|
687
|
+
|
|
688
|
+
if (!(typeof timeoutMs === "number" && timeoutMs > 0)) return null
|
|
689
|
+
|
|
690
|
+
return setTimeout(() => this._onPooledJobTimeout({child, jobId}), timeoutMs)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Fired when a pooled job overruns its timeout. Terminates the child running it
|
|
695
|
+
* (SIGTERM, then SIGKILL after the grace) — a hung JS job cannot be cancelled
|
|
696
|
+
* any other way. The non-clean exit flows through `_handlePooledChildFailure`,
|
|
697
|
+
* which reports every in-flight job on the child failed (so they requeue) and
|
|
698
|
+
* drops it from tracking; capacity is refilled on the next dispatch.
|
|
699
|
+
* @param {object} args - Options.
|
|
700
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
701
|
+
* @param {string} args.jobId - Job id that overran.
|
|
702
|
+
* @returns {void}
|
|
703
|
+
*/
|
|
704
|
+
_onPooledJobTimeout({child, jobId}) {
|
|
705
|
+
const state = this.pooledChildStates.get(child)
|
|
706
|
+
|
|
707
|
+
// Already settling/gone, or the job finished in the race with this timer.
|
|
708
|
+
if (!state || state.settling || !state.inflight.has(jobId)) return
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
child.kill("SIGTERM")
|
|
712
|
+
} catch {
|
|
713
|
+
// Child already exited; nothing to do.
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
state.timeoutSigkillTimer = setTimeout(() => {
|
|
717
|
+
try {
|
|
718
|
+
child.kill("SIGKILL")
|
|
719
|
+
} catch {
|
|
720
|
+
// Child already exited; nothing to do.
|
|
721
|
+
}
|
|
722
|
+
}, this.forkedChildSigkillGraceMs)
|
|
723
|
+
}
|
|
724
|
+
|
|
636
725
|
/**
|
|
637
726
|
* Creates a reusable pooled child.
|
|
638
727
|
* @returns {import("node:child_process").ChildProcess} - New pooled child.
|
|
@@ -647,7 +736,7 @@ export default class BackgroundJobsWorker {
|
|
|
647
736
|
})
|
|
648
737
|
this.pooledChildren.add(child)
|
|
649
738
|
this.inflightProcessChildren.add(child)
|
|
650
|
-
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0})
|
|
739
|
+
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), retiring: false})
|
|
651
740
|
child.on("message", (message) => this._handlePooledChildMessage({child, message}))
|
|
652
741
|
child.once("exit", (code, signal) => this._handlePooledChildFailure({child, error: new Error(`Pooled background job runner exited: code=${code} signal=${signal || "none"}`)}))
|
|
653
742
|
child.once("error", (error) => this._handlePooledChildFailure({child, error}))
|
|
@@ -655,7 +744,8 @@ export default class BackgroundJobsWorker {
|
|
|
655
744
|
}
|
|
656
745
|
|
|
657
746
|
/**
|
|
658
|
-
* Handles a pooled child's durable-report acknowledgement.
|
|
747
|
+
* Handles a pooled child's per-job durable-report acknowledgement. A child
|
|
748
|
+
* runs jobs concurrently and reports one `job-outcome` per job id.
|
|
659
749
|
* @param {object} args - Message details.
|
|
660
750
|
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
661
751
|
* @param {?} args.message - IPC message.
|
|
@@ -665,23 +755,71 @@ export default class BackgroundJobsWorker {
|
|
|
665
755
|
if (!message || typeof message !== "object") return
|
|
666
756
|
const record = /** @type {{type?: ?, jobId?: ?, acknowledged?: ?, rssBytes?: ?, error?: ?}} */ (message)
|
|
667
757
|
const state = this.pooledChildStates.get(child)
|
|
668
|
-
if (record.type !== "job-outcome" || !state
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
state.payload = undefined
|
|
675
|
-
state.resolve = undefined
|
|
758
|
+
if (record.type !== "job-outcome" || !state || state.settling || typeof record.jobId !== "string") return
|
|
759
|
+
const entry = state.inflight.get(record.jobId)
|
|
760
|
+
if (!entry) return
|
|
761
|
+
|
|
762
|
+
if (entry.timeoutTimer) clearTimeout(entry.timeoutTimer)
|
|
763
|
+
state.inflight.delete(record.jobId)
|
|
676
764
|
state.jobsRun += 1
|
|
677
|
-
|
|
765
|
+
const resolve = entry.resolve
|
|
766
|
+
|
|
767
|
+
if (record.acknowledged === true) {
|
|
768
|
+
if (resolve) resolve(undefined)
|
|
769
|
+
} else {
|
|
770
|
+
// The child stayed alive but could not confirm this one job's terminal
|
|
771
|
+
// report; reclaim just this job — its concurrent siblings are unaffected.
|
|
772
|
+
void this._reportJobResult({
|
|
773
|
+
jobId: entry.payload.id,
|
|
774
|
+
status: "failed",
|
|
775
|
+
error: new Error(typeof record.error === "string" ? record.error : "Pooled runner terminal report was not acknowledged"),
|
|
776
|
+
handoffId: entry.payload.handoffId,
|
|
777
|
+
handedOffAtMs: entry.payload.handedOffAtMs,
|
|
778
|
+
workerId: entry.payload.workerId || this.workerId
|
|
779
|
+
}).finally(() => { if (resolve) resolve(undefined) })
|
|
780
|
+
}
|
|
781
|
+
|
|
678
782
|
const rssBytes = typeof record.rssBytes === "number" ? record.rssBytes : Number.POSITIVE_INFINITY
|
|
679
783
|
const runnerAgeMs = Date.now() - state.createdAtMs
|
|
680
|
-
if (state.jobsRun >= this.pooledRunnerMaxJobs || rssBytes >= this.pooledRunnerMaxRssBytes || runnerAgeMs >= this.pooledRunnerMaxLifetimeMs || this.shouldStop)
|
|
784
|
+
if (!state.retiring && (state.jobsRun >= this.pooledRunnerMaxJobs || rssBytes >= this.pooledRunnerMaxRssBytes || runnerAgeMs >= this.pooledRunnerMaxLifetimeMs || this.shouldStop)) {
|
|
785
|
+
this._beginRetirePooledChild(child)
|
|
786
|
+
}
|
|
787
|
+
this._terminateIfDrained(child)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Marks a pooled child for retirement and eagerly spawns a single replacement
|
|
792
|
+
* (1-for-1) so its capacity is restored immediately without waiting for it to
|
|
793
|
+
* finish draining. The retiring child stops receiving new jobs and is
|
|
794
|
+
* terminated only once its in-flight set drains, so a long-running job (e.g. a
|
|
795
|
+
* build) is never cut off.
|
|
796
|
+
* @param {import("node:child_process").ChildProcess} child - Child to retire.
|
|
797
|
+
* @returns {void}
|
|
798
|
+
*/
|
|
799
|
+
_beginRetirePooledChild(child) {
|
|
800
|
+
const state = this.pooledChildStates.get(child)
|
|
801
|
+
if (!state || state.retiring) return
|
|
802
|
+
|
|
803
|
+
state.retiring = true
|
|
804
|
+
// Best-effort pre-warm: skip when stopping (no new work) or before the
|
|
805
|
+
// worker is initialized (no configuration to fork a child from).
|
|
806
|
+
if (!this.shouldStop && this.configuration) this._createPooledChild()
|
|
681
807
|
}
|
|
682
808
|
|
|
683
809
|
/**
|
|
684
|
-
*
|
|
810
|
+
* Terminates a retiring pooled child once it has no in-flight jobs left.
|
|
811
|
+
* @param {import("node:child_process").ChildProcess} child - Child to check.
|
|
812
|
+
* @returns {void}
|
|
813
|
+
*/
|
|
814
|
+
_terminateIfDrained(child) {
|
|
815
|
+
const state = this.pooledChildStates.get(child)
|
|
816
|
+
if (!state || !state.retiring || state.inflight.size > 0) return
|
|
817
|
+
|
|
818
|
+
this._retirePooledChild(child)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Retires a drained pooled child (removes it from tracking, then SIGTERMs it).
|
|
685
823
|
* @param {import("node:child_process").ChildProcess} child - Child process to retire.
|
|
686
824
|
* @returns {void}
|
|
687
825
|
*/
|
|
@@ -693,7 +831,11 @@ export default class BackgroundJobsWorker {
|
|
|
693
831
|
}
|
|
694
832
|
|
|
695
833
|
/**
|
|
696
|
-
* Removes and reports
|
|
834
|
+
* Removes an exited/unhealthy pooled child and reports every job that was
|
|
835
|
+
* in-flight on it as failed — a process-level crash's blast radius is the
|
|
836
|
+
* child's whole in-flight set. Capacity is refilled lazily on the next
|
|
837
|
+
* dispatch (a spawnable slot is still advertised), avoiding a tight respawn
|
|
838
|
+
* loop when a child crashes on startup.
|
|
697
839
|
* @param {object} args - Failure details.
|
|
698
840
|
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
699
841
|
* @param {?} args.error - Failure.
|
|
@@ -702,24 +844,33 @@ export default class BackgroundJobsWorker {
|
|
|
702
844
|
async _handlePooledChildFailure({child, error}) {
|
|
703
845
|
const state = this.pooledChildStates.get(child)
|
|
704
846
|
if (state?.settling) return
|
|
705
|
-
if (state)
|
|
847
|
+
if (state) {
|
|
848
|
+
state.settling = true
|
|
849
|
+
// Cancel this child's pending timers before its in-flight set is reported —
|
|
850
|
+
// the SIGKILL grace from a timeout kill, and every armed per-job backstop.
|
|
851
|
+
if (state.timeoutSigkillTimer) clearTimeout(state.timeoutSigkillTimer)
|
|
852
|
+
for (const inflightEntry of state.inflight.values()) {
|
|
853
|
+
if (inflightEntry.timeoutTimer) clearTimeout(inflightEntry.timeoutTimer)
|
|
854
|
+
}
|
|
855
|
+
}
|
|
706
856
|
this.pooledChildren.delete(child)
|
|
707
857
|
this.inflightProcessChildren.delete(child)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
})
|
|
858
|
+
|
|
859
|
+
const entries = state ? [...state.inflight.values()] : []
|
|
860
|
+
if (state) state.inflight.clear()
|
|
721
861
|
this.pooledChildStates.delete(child)
|
|
722
|
-
|
|
862
|
+
|
|
863
|
+
await Promise.allSettled(entries.map(async (entry) => {
|
|
864
|
+
await this._reportJobResult({
|
|
865
|
+
jobId: entry.payload.id,
|
|
866
|
+
status: "failed",
|
|
867
|
+
error,
|
|
868
|
+
handoffId: entry.payload.handoffId,
|
|
869
|
+
handedOffAtMs: entry.payload.handedOffAtMs,
|
|
870
|
+
workerId: entry.payload.workerId || this.workerId
|
|
871
|
+
})
|
|
872
|
+
if (entry.resolve) entry.resolve(undefined)
|
|
873
|
+
}))
|
|
723
874
|
}
|
|
724
875
|
|
|
725
876
|
/**
|
|
@@ -820,7 +971,7 @@ export default class BackgroundJobsWorker {
|
|
|
820
971
|
* @returns {ForkedJobTimeoutState} - Timeout state.
|
|
821
972
|
*/
|
|
822
973
|
_armForkedJobTimeout({child}) {
|
|
823
|
-
const timeoutMs = this.
|
|
974
|
+
const timeoutMs = this._resolveJobTimeoutMs()
|
|
824
975
|
/** @type {ForkedJobTimeoutState} */
|
|
825
976
|
const state = {timedOut: false, timeoutMs, timer: null, sigkillTimer: null}
|
|
826
977
|
|
|
@@ -832,12 +983,12 @@ export default class BackgroundJobsWorker {
|
|
|
832
983
|
}
|
|
833
984
|
|
|
834
985
|
/**
|
|
835
|
-
* Resolves the effective
|
|
986
|
+
* Resolves the effective wall-clock job timeout in ms (shared by forked and pooled jobs), or null when disabled. The
|
|
836
987
|
* constructor override wins; otherwise the value comes from the background-jobs
|
|
837
988
|
* configuration. A non-positive value disables the backstop.
|
|
838
989
|
* @returns {number | null} - Timeout in ms, or null when disabled.
|
|
839
990
|
*/
|
|
840
|
-
|
|
991
|
+
_resolveJobTimeoutMs() {
|
|
841
992
|
const raw = typeof this.jobTimeoutMsOverride === "number"
|
|
842
993
|
? this.jobTimeoutMsOverride
|
|
843
994
|
: (this.configuration ? this.configuration.getBackgroundJobsConfig().jobTimeoutMs : null)
|
|
@@ -165,6 +165,7 @@
|
|
|
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
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} [pooledRunnerConcurrency] - Number of jobs each pooled child runs concurrently on its own event loop. Total per-worker pooled capacity is `pooledRunnerCount × pooledRunnerConcurrency`. `1` (default) keeps each child serial; raise it for I/O-bound jobs so a bounded set of isolated processes handles high concurrency (like the inline lane) without one process per concurrent job. Default: `1`.
|
|
168
169
|
* @property {number} [pooledRunnerMaxJobs] - Number of sequential jobs a pooled child runs before it is replaced, bounding process-level resource accumulation. Default: `100`.
|
|
169
170
|
* @property {number} [pooledRunnerMaxRssBytes] - RSS bytes after an acknowledged job at which a pooled child is replaced. Default: `536870912` (512 MiB).
|
|
170
171
|
* @property {number} [pooledRunnerMaxLifetimeMs] - Age after an acknowledged job at which a pooled child is replaced. Default: `3600000` (one hour).
|
package/build/configuration.js
CHANGED
|
@@ -192,6 +192,12 @@ export default class VelociousConfiguration {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
this._isInitialized = false
|
|
195
|
+
/**
|
|
196
|
+
* In-progress `initialize()` promise, memoized so concurrent callers await
|
|
197
|
+
* the same bootstrap. Reset to undefined if initialization fails.
|
|
198
|
+
* @type {Promise<void> | undefined}
|
|
199
|
+
*/
|
|
200
|
+
this._initializePromise = undefined
|
|
195
201
|
this.httpServer = httpServer || {}
|
|
196
202
|
/**
|
|
197
203
|
* Stores the http server instance value.
|
|
@@ -1269,6 +1275,7 @@ export default class VelociousConfiguration {
|
|
|
1269
1275
|
const envMaxConcurrentForkedRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_FORKED_JOBS
|
|
1270
1276
|
const envMaxConcurrentRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_MAX_CONCURRENT_INLINE_JOBS
|
|
1271
1277
|
const envPooledRunnerCountRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_COUNT
|
|
1278
|
+
const envPooledRunnerConcurrencyRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_CONCURRENCY
|
|
1272
1279
|
const envPooledRunnerMaxJobsRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_JOBS
|
|
1273
1280
|
const envPooledRunnerMaxRssBytesRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_RSS_BYTES
|
|
1274
1281
|
const envPooledRunnerMaxLifetimeMsRaw = process.env.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_LIFETIME_MS
|
|
@@ -1279,6 +1286,7 @@ export default class VelociousConfiguration {
|
|
|
1279
1286
|
const envMaxConcurrentForked = envMaxConcurrentForkedRaw ? Number(envMaxConcurrentForkedRaw) : undefined
|
|
1280
1287
|
const envMaxConcurrent = envMaxConcurrentRaw ? Number(envMaxConcurrentRaw) : undefined
|
|
1281
1288
|
const envPooledRunnerCount = envPooledRunnerCountRaw ? Number(envPooledRunnerCountRaw) : undefined
|
|
1289
|
+
const envPooledRunnerConcurrency = envPooledRunnerConcurrencyRaw ? Number(envPooledRunnerConcurrencyRaw) : undefined
|
|
1282
1290
|
const envPooledRunnerMaxJobs = envPooledRunnerMaxJobsRaw ? Number(envPooledRunnerMaxJobsRaw) : undefined
|
|
1283
1291
|
const envPooledRunnerMaxRssBytes = envPooledRunnerMaxRssBytesRaw ? Number(envPooledRunnerMaxRssBytesRaw) : undefined
|
|
1284
1292
|
const envPooledRunnerMaxLifetimeMs = envPooledRunnerMaxLifetimeMsRaw ? Number(envPooledRunnerMaxLifetimeMsRaw) : undefined
|
|
@@ -1299,6 +1307,9 @@ export default class VelociousConfiguration {
|
|
|
1299
1307
|
const pooledRunnerCount = typeof configured.pooledRunnerCount === "number" && Number.isFinite(configured.pooledRunnerCount) && Number.isInteger(configured.pooledRunnerCount) && configured.pooledRunnerCount >= 1
|
|
1300
1308
|
? configured.pooledRunnerCount
|
|
1301
1309
|
: (!("pooledRunnerCount" in configured) && typeof envPooledRunnerCount === "number" && Number.isFinite(envPooledRunnerCount) && Number.isInteger(envPooledRunnerCount) && envPooledRunnerCount >= 1 ? envPooledRunnerCount : 4)
|
|
1310
|
+
const pooledRunnerConcurrency = typeof configured.pooledRunnerConcurrency === "number" && Number.isFinite(configured.pooledRunnerConcurrency) && Number.isInteger(configured.pooledRunnerConcurrency) && configured.pooledRunnerConcurrency >= 1
|
|
1311
|
+
? configured.pooledRunnerConcurrency
|
|
1312
|
+
: (!("pooledRunnerConcurrency" in configured) && typeof envPooledRunnerConcurrency === "number" && Number.isFinite(envPooledRunnerConcurrency) && Number.isInteger(envPooledRunnerConcurrency) && envPooledRunnerConcurrency >= 1 ? envPooledRunnerConcurrency : 1)
|
|
1302
1313
|
const pooledRunnerMaxJobs = typeof configured.pooledRunnerMaxJobs === "number" && Number.isFinite(configured.pooledRunnerMaxJobs) && Number.isInteger(configured.pooledRunnerMaxJobs) && configured.pooledRunnerMaxJobs >= 1
|
|
1303
1314
|
? configured.pooledRunnerMaxJobs
|
|
1304
1315
|
: (!("pooledRunnerMaxJobs" in configured) && typeof envPooledRunnerMaxJobs === "number" && Number.isFinite(envPooledRunnerMaxJobs) && Number.isInteger(envPooledRunnerMaxJobs) && envPooledRunnerMaxJobs >= 1 ? envPooledRunnerMaxJobs : 100)
|
|
@@ -1336,7 +1347,7 @@ export default class VelociousConfiguration {
|
|
|
1336
1347
|
: 60 * 60 * 1000
|
|
1337
1348
|
}
|
|
1338
1349
|
|
|
1339
|
-
return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, dispatchStrategy, pollIntervalMs, queues, jobTimeoutMs, retention}
|
|
1350
|
+
return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, dispatchStrategy, pollIntervalMs, queues, jobTimeoutMs, retention}
|
|
1340
1351
|
}
|
|
1341
1352
|
|
|
1342
1353
|
/**
|
|
@@ -1934,9 +1945,16 @@ export default class VelociousConfiguration {
|
|
|
1934
1945
|
* @returns {Promise<void>} - Resolves when complete.
|
|
1935
1946
|
*/
|
|
1936
1947
|
async initialize({type} = {type: "undefined"}) {
|
|
1937
|
-
if (
|
|
1938
|
-
|
|
1939
|
-
|
|
1948
|
+
if (this._isInitialized) return
|
|
1949
|
+
// Memoize the in-progress initialization so concurrent callers await the same
|
|
1950
|
+
// bootstrap instead of racing. `_isInitialized` was previously set to `true`
|
|
1951
|
+
// up front, so a second caller (e.g. a pooled runner with
|
|
1952
|
+
// `pooledRunnerConcurrency > 1` starting several jobs on a cold child) could
|
|
1953
|
+
// skip initialization and load models / perform a job while the first call
|
|
1954
|
+
// was still awaiting model discovery and initializers. Mirrors connectBeacon.
|
|
1955
|
+
if (this._initializePromise) return await this._initializePromise
|
|
1956
|
+
|
|
1957
|
+
this._initializePromise = (async () => {
|
|
1940
1958
|
await this.initializeModels({type})
|
|
1941
1959
|
await this.getEnvironmentHandler().autoDiscoverResources(this)
|
|
1942
1960
|
this._mergeDiscoveredAbilityResources()
|
|
@@ -1957,6 +1975,17 @@ export default class VelociousConfiguration {
|
|
|
1957
1975
|
}
|
|
1958
1976
|
}
|
|
1959
1977
|
}
|
|
1978
|
+
|
|
1979
|
+
this._isInitialized = true
|
|
1980
|
+
})()
|
|
1981
|
+
|
|
1982
|
+
try {
|
|
1983
|
+
await this._initializePromise
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
// Let a later call retry a failed initialization instead of every future
|
|
1986
|
+
// caller awaiting the same cached rejection.
|
|
1987
|
+
this._initializePromise = undefined
|
|
1988
|
+
throw error
|
|
1960
1989
|
}
|
|
1961
1990
|
}
|
|
1962
1991
|
|