velocious 1.0.612 → 1.0.613
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 +1 -1
- package/build/background-jobs/pooled-runner-child.js +1 -0
- package/build/background-jobs/worker.js +65 -27
- package/build/src/background-jobs/pooled-runner-child.js +3 -1
- package/build/src/background-jobs/worker.d.ts +23 -10
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +67 -29
- package/package.json +1 -1
- package/src/background-jobs/pooled-runner-child.js +1 -0
- package/src/background-jobs/worker.js +65 -27
package/package.json
CHANGED
|
@@ -141,3 +141,4 @@ function handleMessage(message) {
|
|
|
141
141
|
process.on("message", (message) => handleMessage(message))
|
|
142
142
|
process.once("disconnect", () => void shutdownRunner(0))
|
|
143
143
|
for (const signal of ["SIGTERM", "SIGINT"]) process.once(signal, () => void shutdownRunner(1))
|
|
144
|
+
if (process.send) process.send({type: "ready"})
|
|
@@ -194,8 +194,10 @@ export default class BackgroundJobsWorker {
|
|
|
194
194
|
this.inflightPooledJobs = new Set()
|
|
195
195
|
/** @type {Set<import("node:child_process").ChildProcess>} */
|
|
196
196
|
this.pooledChildren = new Set()
|
|
197
|
-
/** @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}>, lastDispatchSeq: number, retiring: boolean, settling?: boolean, timeoutSigkillTimer?: ReturnType<typeof setTimeout> | null}>} */
|
|
197
|
+
/** @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, pooledJob?: Promise<void>, timeoutTimer?: ReturnType<typeof setTimeout> | null}>, lastDispatchSeq: number, retiring: boolean, started?: boolean, settling?: boolean, timeoutSigkillTimer?: ReturnType<typeof setTimeout> | null}>} */
|
|
198
198
|
this.pooledChildStates = new Map()
|
|
199
|
+
/** @type {WeakSet<Promise<void>>} */
|
|
200
|
+
this._pooledStartupFailureJobs = new WeakSet()
|
|
199
201
|
// Monotonic dispatch counter for round-robin child selection: each dispatch stamps
|
|
200
202
|
// the chosen child, and selection prefers the child dispatched least recently.
|
|
201
203
|
this._pooledDispatchSeq = 0
|
|
@@ -600,16 +602,16 @@ export default class BackgroundJobsWorker {
|
|
|
600
602
|
}
|
|
601
603
|
|
|
602
604
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
605
|
+
* Advertises current worker capacity unless the worker is draining.
|
|
606
|
+
* @param {object} [options] - Advertisement options.
|
|
607
|
+
* @param {boolean} [options.revokePooledAdmission] - Revoke pooled credits while preserving other execution modes.
|
|
606
608
|
* @returns {void}
|
|
607
609
|
*/
|
|
608
|
-
_sendReadyIfRunning() {
|
|
610
|
+
_sendReadyIfRunning({revokePooledAdmission = false} = {}) {
|
|
609
611
|
if (this.shouldStop) return
|
|
610
612
|
if (!this.jsonSocket) return
|
|
611
613
|
|
|
612
|
-
const readyMessage = this._readyMessage()
|
|
614
|
+
const readyMessage = this._readyMessage({revokePooledAdmission})
|
|
613
615
|
|
|
614
616
|
if (!readyMessage) return
|
|
615
617
|
this.jsonSocket.send(readyMessage)
|
|
@@ -617,21 +619,24 @@ export default class BackgroundJobsWorker {
|
|
|
617
619
|
|
|
618
620
|
/**
|
|
619
621
|
* Runs ready message.
|
|
622
|
+
* @param {object} [options] - Advertisement options.
|
|
623
|
+
* @param {boolean} [options.revokePooledAdmission] - Revoke pooled credits while preserving other execution modes.
|
|
620
624
|
* @returns {import("./types.js").BackgroundJobSocketMessage | null} - Ready message or null when the worker has no capacity.
|
|
621
625
|
*/
|
|
622
|
-
_readyMessage() {
|
|
626
|
+
_readyMessage({revokePooledAdmission = false} = {}) {
|
|
623
627
|
const acceptsProcessJob = this.inflightProcessJobs.size < this.maxConcurrentForkedJobs
|
|
624
628
|
const acceptsInline = this.inflightInlineJobs.size < this.maxConcurrentInlineJobs
|
|
625
|
-
const
|
|
629
|
+
const availablePooledSlots = revokePooledAdmission ? 0 : this._availablePooledSlots()
|
|
630
|
+
const acceptsPooled = availablePooledSlots > 0
|
|
626
631
|
|
|
627
|
-
if (!acceptsProcessJob && !acceptsInline && !acceptsPooled) return null
|
|
632
|
+
if (!revokePooledAdmission && !acceptsProcessJob && !acceptsInline && !acceptsPooled) return null
|
|
628
633
|
|
|
629
634
|
return {
|
|
630
635
|
type: "ready",
|
|
631
636
|
acceptsForked: acceptsProcessJob,
|
|
632
637
|
acceptsInline,
|
|
633
638
|
acceptsPooled,
|
|
634
|
-
availablePooledSlots
|
|
639
|
+
availablePooledSlots,
|
|
635
640
|
acceptsSpawned: acceptsProcessJob
|
|
636
641
|
}
|
|
637
642
|
}
|
|
@@ -646,7 +651,7 @@ export default class BackgroundJobsWorker {
|
|
|
646
651
|
let inflight
|
|
647
652
|
inflight = pooledJob.finally(() => {
|
|
648
653
|
this.inflightPooledJobs.delete(inflight)
|
|
649
|
-
if (!this.shouldStop) this._sendReadyIfRunning()
|
|
654
|
+
if (!this.shouldStop && !this._pooledStartupFailureJobs.has(pooledJob)) this._sendReadyIfRunning()
|
|
650
655
|
})
|
|
651
656
|
this.inflightPooledJobs.add(inflight)
|
|
652
657
|
}
|
|
@@ -689,16 +694,22 @@ export default class BackgroundJobsWorker {
|
|
|
689
694
|
// Stamp the round-robin cursor so the next dispatch prefers a different child.
|
|
690
695
|
state.lastDispatchSeq = ++this._pooledDispatchSeq
|
|
691
696
|
|
|
692
|
-
|
|
693
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Resolves the pooled job promise.
|
|
699
|
+
* @type {(value: void) => void}
|
|
700
|
+
*/
|
|
701
|
+
let resolvePooledJob = () => {}
|
|
702
|
+
const pooledJob = new Promise((resolve) => { resolvePooledJob = resolve })
|
|
703
|
+
const timeoutTimer = this._armPooledJobTimeout({child, payload})
|
|
694
704
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
705
|
+
state.inflight.set(payload.id, {payload, resolve: resolvePooledJob, pooledJob, timeoutTimer})
|
|
706
|
+
try {
|
|
707
|
+
child.send({type: "job", payload, sharedTransactionBroker: this._pooledJobSharedTransactionBrokerConfig()})
|
|
708
|
+
} catch (error) {
|
|
709
|
+
void this._handlePooledChildFailure({child, error})
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return pooledJob
|
|
702
713
|
}
|
|
703
714
|
|
|
704
715
|
/**
|
|
@@ -769,7 +780,8 @@ export default class BackgroundJobsWorker {
|
|
|
769
780
|
* (SIGTERM, then SIGKILL after the grace) — a hung JS job cannot be cancelled
|
|
770
781
|
* any other way. The non-clean exit flows through `_handlePooledChildFailure`,
|
|
771
782
|
* which reports every in-flight job on the child failed (so they requeue) and
|
|
772
|
-
* drops it from tracking;
|
|
783
|
+
* drops it from tracking; the failure path immediately re-advertises the
|
|
784
|
+
* resulting capacity once the runner has completed startup.
|
|
773
785
|
* @param {object} args - Options.
|
|
774
786
|
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
775
787
|
* @param {string} args.jobId - Job id that overran.
|
|
@@ -810,7 +822,7 @@ export default class BackgroundJobsWorker {
|
|
|
810
822
|
})
|
|
811
823
|
this.pooledChildren.add(child)
|
|
812
824
|
this.inflightProcessChildren.add(child)
|
|
813
|
-
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false})
|
|
825
|
+
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false, started: false})
|
|
814
826
|
child.on("message", (message) => this._handlePooledChildMessage({child, message}))
|
|
815
827
|
child.once("exit", (code, signal) => this._handlePooledChildFailure({child, error: new Error(`Pooled background job runner exited: code=${code} signal=${signal || "none"}`)}))
|
|
816
828
|
child.once("error", (error) => this._handlePooledChildFailure({child, error}))
|
|
@@ -829,7 +841,12 @@ export default class BackgroundJobsWorker {
|
|
|
829
841
|
if (!message || typeof message !== "object") return
|
|
830
842
|
const record = /** @type {{type?: ReturnType<typeof JSON.parse>, jobId?: ReturnType<typeof JSON.parse>, acknowledged?: ReturnType<typeof JSON.parse>, rssBytes?: ReturnType<typeof JSON.parse>, error?: ReturnType<typeof JSON.parse>}} */ (message)
|
|
831
843
|
const state = this.pooledChildStates.get(child)
|
|
844
|
+
if (record.type === "ready") {
|
|
845
|
+
if (state) state.started = true
|
|
846
|
+
return
|
|
847
|
+
}
|
|
832
848
|
if (record.type !== "job-outcome" || !state || state.settling || typeof record.jobId !== "string") return
|
|
849
|
+
state.started = true
|
|
833
850
|
const entry = state.inflight.get(record.jobId)
|
|
834
851
|
if (!entry) return
|
|
835
852
|
|
|
@@ -907,9 +924,11 @@ export default class BackgroundJobsWorker {
|
|
|
907
924
|
/**
|
|
908
925
|
* Removes an exited/unhealthy pooled child and reports every job that was
|
|
909
926
|
* in-flight on it as failed — a process-level crash's blast radius is the
|
|
910
|
-
* child's whole in-flight set.
|
|
911
|
-
*
|
|
912
|
-
*
|
|
927
|
+
* child's whole in-flight set. Once the child has completed startup, its
|
|
928
|
+
* freed capacity is advertised immediately; the replacement itself is still
|
|
929
|
+
* spawned lazily by the next dispatch. A child that exits before its startup
|
|
930
|
+
* handshake does not re-announce, avoiding a tight respawn loop on startup
|
|
931
|
+
* failure.
|
|
913
932
|
* @param {object} args - Failure details.
|
|
914
933
|
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
915
934
|
* @param {ReturnType<typeof JSON.parse>} args.error - Failure.
|
|
@@ -934,7 +953,7 @@ export default class BackgroundJobsWorker {
|
|
|
934
953
|
if (state) state.inflight.clear()
|
|
935
954
|
this.pooledChildStates.delete(child)
|
|
936
955
|
|
|
937
|
-
|
|
956
|
+
const failureReports = entries.map(async (entry) => {
|
|
938
957
|
await this._reportJobResult({
|
|
939
958
|
jobId: entry.payload.id,
|
|
940
959
|
status: "failed",
|
|
@@ -944,7 +963,26 @@ export default class BackgroundJobsWorker {
|
|
|
944
963
|
workerId: entry.payload.workerId || this.workerId
|
|
945
964
|
})
|
|
946
965
|
if (entry.resolve) entry.resolve(undefined)
|
|
947
|
-
})
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
// Start every fallback report before announcing capacity so the main cannot
|
|
969
|
+
// observe a replacement slot before the failed jobs' reports are in flight.
|
|
970
|
+
// The report promises remain tracked below; a slow retry must not hold the
|
|
971
|
+
// newly freed runner capacity hostage.
|
|
972
|
+
if (state && state.started !== false) {
|
|
973
|
+
this._sendReadyIfRunning()
|
|
974
|
+
} else if (state) {
|
|
975
|
+
for (const entry of entries) {
|
|
976
|
+
if (entry.pooledJob) this._pooledStartupFailureJobs.add(entry.pooledJob)
|
|
977
|
+
}
|
|
978
|
+
// A previous ready message may still have unconsumed pooled credits at the
|
|
979
|
+
// main. Revoke them authoritatively without suppressing valid inline or
|
|
980
|
+
// process-runner readiness; otherwise queued jobs can trigger a startup
|
|
981
|
+
// crash loop using the stale credits.
|
|
982
|
+
this._sendReadyIfRunning({revokePooledAdmission: true})
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
await Promise.allSettled(failureReports)
|
|
948
986
|
}
|
|
949
987
|
|
|
950
988
|
/**
|