velocious 1.0.611 → 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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/build/background-jobs/adapter.js +3 -1
  3. package/build/background-jobs/json-socket.js +4 -0
  4. package/build/background-jobs/local-adapter.js +1 -1
  5. package/build/background-jobs/local-store.js +2 -3
  6. package/build/background-jobs/main.js +166 -12
  7. package/build/background-jobs/pooled-runner-child.js +1 -0
  8. package/build/background-jobs/store.js +2 -3
  9. package/build/background-jobs/types.js +6 -0
  10. package/build/background-jobs/worker.js +65 -27
  11. package/build/src/background-jobs/adapter.d.ts +4 -5
  12. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  13. package/build/src/background-jobs/adapter.js +4 -2
  14. package/build/src/background-jobs/json-socket.d.ts +4 -0
  15. package/build/src/background-jobs/json-socket.d.ts.map +1 -1
  16. package/build/src/background-jobs/json-socket.js +5 -1
  17. package/build/src/background-jobs/local-adapter.d.ts +2 -5
  18. package/build/src/background-jobs/local-adapter.d.ts.map +1 -1
  19. package/build/src/background-jobs/local-adapter.js +2 -2
  20. package/build/src/background-jobs/local-store.d.ts +2 -5
  21. package/build/src/background-jobs/local-store.d.ts.map +1 -1
  22. package/build/src/background-jobs/local-store.js +3 -4
  23. package/build/src/background-jobs/main.d.ts +90 -1
  24. package/build/src/background-jobs/main.d.ts.map +1 -1
  25. package/build/src/background-jobs/main.js +161 -15
  26. package/build/src/background-jobs/pooled-runner-child.js +3 -1
  27. package/build/src/background-jobs/store.d.ts +3 -1
  28. package/build/src/background-jobs/store.d.ts.map +1 -1
  29. package/build/src/background-jobs/store.js +3 -3
  30. package/build/src/background-jobs/types.d.ts +20 -0
  31. package/build/src/background-jobs/types.d.ts.map +1 -1
  32. package/build/src/background-jobs/types.js +7 -1
  33. package/build/src/background-jobs/worker.d.ts +23 -10
  34. package/build/src/background-jobs/worker.d.ts.map +1 -1
  35. package/build/src/background-jobs/worker.js +67 -29
  36. package/package.json +1 -1
  37. package/src/background-jobs/adapter.js +3 -1
  38. package/src/background-jobs/json-socket.js +4 -0
  39. package/src/background-jobs/local-adapter.js +1 -1
  40. package/src/background-jobs/local-store.js +2 -3
  41. package/src/background-jobs/main.js +166 -12
  42. package/src/background-jobs/pooled-runner-child.js +1 -0
  43. package/src/background-jobs/store.js +2 -3
  44. package/src/background-jobs/types.js +6 -0
  45. package/src/background-jobs/worker.js +65 -27
package/README.md CHANGED
@@ -2396,7 +2396,7 @@ VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS=5400000
2396
2396
 
2397
2397
  `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; select it with `executionMode: "forked"`.
2398
2398
 
2399
- 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. Workers advertise that exact free-slot count and the main consumes one slot per durable handoff, allowing one readiness notification to fill the pool. `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). `execution_mode` is the single source of truth for a job's runtime — pooled rows persist as `execution_mode = "pooled"` directly. See [execution modes and pooled runners](docs/background-jobs.md#execution-modes-and-pooled-runners).
2399
+ 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. Workers advertise that exact free-slot count and the main consumes one slot per durable handoff, allowing one readiness notification to fill the pool. If an initialized child exits unexpectedly, the worker immediately advertises the freed capacity while failure reports retry; the replacement is spawned lazily by the next dispatch, and a pre-startup crash does not trigger a respawn loop. `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). `execution_mode` is the single source of truth for a job's runtime — pooled rows persist as `execution_mode = "pooled"` directly. See [execution modes and pooled runners](docs/background-jobs.md#execution-modes-and-pooled-runners).
2400
2400
 
2401
2401
  Cold pooled jobs share one atomic model-bootstrap phase. If that phase fails, all
2402
2402
  waiting jobs receive the failure; a later job in the surviving child cannot run
@@ -2595,7 +2595,7 @@ Each job must define exactly one of `every` or `cron`. Cron times are evaluated
2595
2595
 
2596
2596
  ## Persistence and retries
2597
2597
 
2598
- Jobs are persisted in the configured database (`backgroundJobs.databaseIdentifier`) in an internal `background_jobs` table. When a worker picks a job, the job is marked as handed off with a unique lease id and the worker reports completion or failure back to the main process. If that worker socket disconnects unexpectedly, only the leases handed to that exact socket are immediately returned to the queue; late reports are fenced by lease id so they cannot mutate a newer attempt. This recovery is at-least-once and may repeat application side effects if the disconnected attempt had already started them. Gracefully draining workers keep their leases while they finish. When the **main** itself restarts (every deploy), a reconnecting worker's still-active handoffs are adopted into its new socket on `hello` so a later disconnect releases them instead of leaving them stuck until the orphan sweep; the main never time-reclaims a disconnected worker's jobs, so a gracefully-draining old-release worker is not double-run. For rolling upgrades, upgrade workers before the main process: a lease-aware main keeps legacy workers connected for old reports but dispatches new jobs only to workers advertising lease-reporting support. See [docs/background-jobs.md](docs/background-jobs.md#worker-disconnect-recovery).
2598
+ Jobs are persisted in the configured database (`backgroundJobs.databaseIdentifier`) in an internal `background_jobs` table. When a worker picks a job, the main generates a unique lease id before asking the adapter to mark the job handed off, and the worker reports completion or failure back to the main process. If that persistence call has an ambiguous result, only the exact caller-generated lease is conditionally returned; failed recovery is retained for the dispatch error-retry path, so worker admission and concurrency do not remain stranded and a newer lease is never reclaimed. Custom adapters must persist a supplied `markHandedOff({handoffId})` exactly; built-in adapters continue generating one for legacy direct callers that omit it. If a worker socket disconnects unexpectedly, only the leases handed to that exact socket are immediately returned to the queue; late reports are fenced by lease id so they cannot mutate a newer attempt. This recovery is at-least-once and may repeat application side effects if the disconnected attempt had already started them. Gracefully draining workers keep their leases while they finish. When the **main** itself restarts (every deploy), a reconnecting worker's still-active handoffs are adopted into its new socket on `hello` so a later disconnect releases them instead of leaving them stuck until the orphan sweep; the main never time-reclaims a disconnected worker's jobs, so a gracefully-draining old-release worker is not double-run. For rolling upgrades, upgrade workers before the main process: a lease-aware main keeps legacy workers connected for old reports but dispatches new jobs only to workers advertising lease-reporting support. See [docs/background-jobs.md](docs/background-jobs.md#worker-disconnect-recovery).
2599
2599
 
2600
2600
  Failed jobs are re-queued with backoff and retried up to 10 times by default (10s, 1m, 10m, 1h, then +1h per retry). You can override the retry limit per job:
2601
2601
 
@@ -83,7 +83,9 @@ export default class BackgroundJobsAdapter {
83
83
 
84
84
  /**
85
85
  * Starts a job by claiming its durable handoff.
86
- * @param {{jobId: string, workerId?: string}} _args - Handoff request.
86
+ * When `handoffId` is supplied, the adapter must persist and return that exact
87
+ * id so the caller can fence an ambiguous commit acknowledgement.
88
+ * @param {import("./types.js").BackgroundJobHandoffRequest} _args - Handoff request.
87
89
  * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Claimed handoff.
88
90
  */
89
91
  async markHandedOff(_args) { throw new Error("BackgroundJobsAdapter#markHandedOff is not implemented") }
@@ -32,6 +32,10 @@ export default class JsonSocket extends EventEmitter {
32
32
  this.availablePooledSlots = 0
33
33
  /** Whether the worker/main pair uses consumable pooled-capacity credits. */
34
34
  this.usesPooledCapacityCredits = false
35
+ /** Whether this worker has permanently stopped accepting new handoffs. */
36
+ this.isDraining = false
37
+ /** Monotonic generation of the worker's latest readiness advertisement. */
38
+ this.readinessVersion = 0
35
39
  /**
36
40
  * Narrows the runtime value to the documented type.
37
41
  * @type {boolean} */
@@ -107,7 +107,7 @@ export default class LocalBackgroundJobsAdapter extends BackgroundJobsAdapter {
107
107
 
108
108
  /**
109
109
  * Claims one queued local job.
110
- * @param {{jobId: string, workerId?: string}} args - Claim request.
110
+ * @param {import("./types.js").BackgroundJobHandoffRequest} args - Claim request. A supplied handoff id is persisted exactly.
111
111
  * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Handoff.
112
112
  */
113
113
  async markHandedOff(args) { return await this.store.markHandedOff(args) }
@@ -693,10 +693,10 @@ export default class LocalBackgroundJobsStore {
693
693
 
694
694
  /**
695
695
  * Atomically reserves concurrency and claims one queued job.
696
- * @param {{jobId: string, workerId?: string}} args - Claim request.
696
+ * @param {import("./types.js").BackgroundJobHandoffRequest} args - Claim request. A supplied handoff id is persisted exactly.
697
697
  * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Fenced claim.
698
698
  */
699
- async markHandedOff({jobId, workerId}) {
699
+ async markHandedOff({jobId, handoffId = new UUID(4).format(), workerId}) {
700
700
  await this.ensureReady()
701
701
 
702
702
  return await this._withDb(async (connection) => await this._mutate(connection, async (db) => {
@@ -706,7 +706,6 @@ export default class LocalBackgroundJobsStore {
706
706
  if (job.concurrencyKey && !(await this._reserveConcurrency(db, job.concurrencyKey))) return null
707
707
 
708
708
  const handedOffAtMs = this.clock.now()
709
- const handoffId = new UUID(4).format()
710
709
  const affectedRows = await this._updateAffectedRows(db, {
711
710
  conditions: {id: jobId, status: "queued"},
712
711
  data: {handed_off_at_ms: handedOffAtMs, handoff_id: handoffId, status: "handed_off", worker_id: workerId || "local"},
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
2
 
3
+ import { randomUUID } from "crypto"
3
4
  import net from "net"
4
5
  import JsonSocket from "./json-socket.js"
5
6
  import BackgroundJobsScheduler from "./scheduler.js"
@@ -92,6 +93,12 @@ export default class BackgroundJobsMain {
92
93
  * Active durable handoffs keyed by the exact worker socket that received them.
93
94
  * @type {Map<JsonSocket, Map<string, string>>} */
94
95
  this.workerHandoffs = new Map()
96
+ /**
97
+ * Exact caller-generated leases whose claim outcome was ambiguous or whose
98
+ * pre-dispatch release has not yet been acknowledged. Retained until a
99
+ * fenced return succeeds (including an exact no-op).
100
+ * @type {Map<string, string>} */
101
+ this.pendingHandoffRecoveries = new Map()
95
102
  /**
96
103
  * Handoff-adoption queries started by worker hello messages. Shutdown must
97
104
  * wait for these before closing the configuration's database pools.
@@ -667,6 +674,7 @@ export default class BackgroundJobsMain {
667
674
  * @returns {void}
668
675
  */
669
676
  _handleWorkerReady({jsonSocket, message}) {
677
+ jsonSocket.readinessVersion += 1
670
678
  jsonSocket.acceptsSpawnedJobs = message.acceptsSpawned !== false && message.acceptsForked !== false
671
679
  jsonSocket.acceptsForkedJobs = message.acceptsForked !== false
672
680
  jsonSocket.acceptsPooledJobs = message.acceptsPooled === true
@@ -676,7 +684,7 @@ export default class BackgroundJobsMain {
676
684
  ? availablePooledSlots
677
685
  : 0
678
686
  jsonSocket.acceptsInlineJobs = message.acceptsInline !== false
679
- if (jsonSocket.supportsHandoffIdReporting) {
687
+ if (jsonSocket.supportsHandoffIdReporting && !jsonSocket.isDraining) {
680
688
  this.readyWorkers.add(jsonSocket)
681
689
  } else {
682
690
  this.readyWorkers.delete(jsonSocket)
@@ -694,6 +702,7 @@ export default class BackgroundJobsMain {
694
702
  // The worker is shutting down gracefully. Stop dispatching new jobs
695
703
  // to it but keep the connection in `workers` so any in-flight job
696
704
  // it's still draining can report its result.
705
+ jsonSocket.isDraining = true
697
706
  this.readyWorkers.delete(jsonSocket)
698
707
  }
699
708
 
@@ -1230,6 +1239,8 @@ export default class BackgroundJobsMain {
1230
1239
  * Runs clear error retry timer.
1231
1240
  * @returns {void} */
1232
1241
  _clearErrorRetryTimer() {
1242
+ if (this.pendingHandoffRecoveries.size > 0) return
1243
+
1233
1244
  for (const worker of this.workerHandoffs.keys()) {
1234
1245
  if (!this.workers.has(worker)) return
1235
1246
  }
@@ -1296,12 +1307,20 @@ export default class BackgroundJobsMain {
1296
1307
  }
1297
1308
 
1298
1309
  /**
1299
- * Retries failed disconnected-socket releases before draining queued work.
1310
+ * Retries failed pre-dispatch and disconnected-socket releases before
1311
+ * draining queued work.
1300
1312
  * @returns {Promise<void>} - Resolves after retry work.
1301
1313
  */
1302
1314
  async _retryAfterError() {
1303
1315
  if (this._stopped) return
1304
1316
 
1317
+ try {
1318
+ await this._retryPendingHandoffRecoveries()
1319
+ } catch {
1320
+ this._scheduleErrorRetry()
1321
+ return
1322
+ }
1323
+
1305
1324
  try {
1306
1325
  for (const worker of this.workerHandoffs.keys()) {
1307
1326
  if (!this.workers.has(worker)) await this._releaseWorkerHandoffs(worker)
@@ -1328,30 +1347,46 @@ export default class BackgroundJobsMain {
1328
1347
  const worker = this.readyWorkerForJob(job)
1329
1348
  if (!worker) return
1330
1349
 
1331
- this.readyWorkers.delete(worker)
1350
+ const admission = this._consumeWorkerAdmission({job, worker})
1351
+ const requestedHandoffId = randomUUID()
1352
+ let handoff
1332
1353
 
1333
- if (job.executionMode === "pooled" && worker.usesPooledCapacityCredits && worker.availablePooledSlots > 0) {
1334
- worker.availablePooledSlots -= 1
1335
- if (worker.availablePooledSlots > 0) this.readyWorkers.add(worker)
1336
- }
1354
+ try {
1355
+ handoff = await this.store.markHandedOff({handoffId: requestedHandoffId, jobId: job.id, workerId: worker.workerId})
1356
+ } catch (error) {
1357
+ this._rememberHandoffRecovery({handoffId: requestedHandoffId, jobId: job.id})
1358
+ this._restoreWorkerAdmission({...admission, worker})
1337
1359
 
1338
- const handoff = await this.store.markHandedOff({jobId: job.id, workerId: worker.workerId})
1360
+ try {
1361
+ await this._recoverHandoff({handoffId: requestedHandoffId, jobId: job.id})
1362
+ } catch (recoveryError) {
1363
+ this._reportHandoffRecoveryError({error: recoveryError, handoffId: requestedHandoffId, jobId: job.id})
1364
+ }
1365
+
1366
+ throw error
1367
+ }
1339
1368
 
1340
1369
  if (!handoff) {
1341
- if (job.executionMode === "pooled" && worker.usesPooledCapacityCredits) worker.availablePooledSlots += 1
1342
- if (this.workers.has(worker)) this.readyWorkers.add(worker)
1370
+ this._restoreWorkerAdmission({...admission, worker})
1343
1371
  continue
1344
1372
  }
1345
1373
 
1346
1374
  const handoffs = this.workerHandoffs.get(worker)
1347
1375
 
1348
- if (!handoffs || !this.workers.has(worker)) {
1349
- await this.store.markReturnedToQueue({handoffId: handoff.handoffId, jobId: job.id})
1376
+ if (!handoffs || !this.workers.has(worker) || worker.isDraining) {
1377
+ this._rememberHandoffRecovery({handoffId: handoff.handoffId, jobId: job.id})
1378
+ try {
1379
+ await this._recoverHandoff({handoffId: handoff.handoffId, jobId: job.id})
1380
+ } catch (recoveryError) {
1381
+ this._reportHandoffRecoveryError({error: recoveryError, handoffId: handoff.handoffId, jobId: job.id})
1382
+ throw recoveryError
1383
+ }
1350
1384
  this._notifyEnqueued()
1351
1385
  this._redrainQueued = true
1352
1386
  continue
1353
1387
  }
1354
1388
 
1389
+ this._finalizeWorkerAdmission({...admission, job, worker})
1355
1390
  handoffs.set(job.id, handoff.handoffId)
1356
1391
 
1357
1392
  try {
@@ -1382,6 +1417,125 @@ export default class BackgroundJobsMain {
1382
1417
  }
1383
1418
  }
1384
1419
 
1420
+ /**
1421
+ * Consumes one advertised worker admission while persistence is in flight.
1422
+ * @param {object} args - Admission details.
1423
+ * @param {import("./types.js").BackgroundJobRow} args.job - Selected job.
1424
+ * @param {JsonSocket} args.worker - Selected worker socket.
1425
+ * @returns {{pooledCreditConsumed: boolean, readinessVersion: number}} - Reversible admission debit.
1426
+ */
1427
+ _consumeWorkerAdmission({job, worker}) {
1428
+ let pooledCreditConsumed = false
1429
+
1430
+ this.readyWorkers.delete(worker)
1431
+
1432
+ if (job.executionMode === "pooled" && worker.usesPooledCapacityCredits && worker.availablePooledSlots > 0) {
1433
+ pooledCreditConsumed = true
1434
+ worker.availablePooledSlots -= 1
1435
+ if (worker.availablePooledSlots > 0) this.readyWorkers.add(worker)
1436
+ }
1437
+
1438
+ return {pooledCreditConsumed, readinessVersion: worker.readinessVersion}
1439
+ }
1440
+
1441
+ /**
1442
+ * Restores an admission that never reached a worker. A newer readiness
1443
+ * advertisement is already authoritative, so its pooled count is not changed.
1444
+ * @param {object} args - Admission details.
1445
+ * @param {boolean} args.pooledCreditConsumed - Whether a pooled credit was debited.
1446
+ * @param {number} args.readinessVersion - Readiness generation at debit time.
1447
+ * @param {JsonSocket} args.worker - Selected worker socket.
1448
+ * @returns {void}
1449
+ */
1450
+ _restoreWorkerAdmission({pooledCreditConsumed, readinessVersion, worker}) {
1451
+ if (this._stopped || !this.workers.has(worker) || worker.isDraining) return
1452
+
1453
+ if (pooledCreditConsumed && worker.readinessVersion === readinessVersion) {
1454
+ worker.availablePooledSlots += 1
1455
+ }
1456
+
1457
+ if (worker.supportsHandoffIdReporting) this.readyWorkers.add(worker)
1458
+ }
1459
+
1460
+ /**
1461
+ * Applies a successful pooled admission to a readiness advertisement that
1462
+ * arrived while persistence was in flight and replaced the earlier debit.
1463
+ * @param {object} args - Admission details.
1464
+ * @param {import("./types.js").BackgroundJobRow} args.job - Selected job.
1465
+ * @param {boolean} args.pooledCreditConsumed - Whether a pooled credit was debited.
1466
+ * @param {number} args.readinessVersion - Readiness generation at debit time.
1467
+ * @param {JsonSocket} args.worker - Selected worker socket.
1468
+ * @returns {void}
1469
+ */
1470
+ _finalizeWorkerAdmission({job, pooledCreditConsumed, readinessVersion, worker}) {
1471
+ if (!pooledCreditConsumed || job.executionMode !== "pooled") return
1472
+ if (worker.readinessVersion === readinessVersion || !worker.usesPooledCapacityCredits) return
1473
+ if (worker.availablePooledSlots <= 0) return
1474
+
1475
+ worker.availablePooledSlots -= 1
1476
+ if (worker.availablePooledSlots === 0) this.readyWorkers.delete(worker)
1477
+ }
1478
+
1479
+ /**
1480
+ * Retains an exact lease for idempotent pre-dispatch recovery.
1481
+ * @param {{handoffId: string, jobId: string}} args - Exact recovery fence.
1482
+ * @returns {void}
1483
+ */
1484
+ _rememberHandoffRecovery({handoffId, jobId}) {
1485
+ this.pendingHandoffRecoveries.set(handoffId, jobId)
1486
+ }
1487
+
1488
+ /**
1489
+ * Returns one exact lease and forgets it only after the adapter acknowledges
1490
+ * the fenced transition or confirms it was already absent.
1491
+ * @param {{handoffId: string, jobId: string}} args - Exact recovery fence.
1492
+ * @returns {Promise<void>} - Resolves after durable recovery settles.
1493
+ */
1494
+ async _recoverHandoff({handoffId, jobId}) {
1495
+ await this.store.markReturnedToQueue({handoffId, jobId})
1496
+
1497
+ if (this.pendingHandoffRecoveries.get(handoffId) === jobId) {
1498
+ this.pendingHandoffRecoveries.delete(handoffId)
1499
+ }
1500
+ }
1501
+
1502
+ /**
1503
+ * Replays retained exact-ID recoveries through the dispatcher's existing
1504
+ * transient-error retry lifecycle.
1505
+ * @returns {Promise<void>} - Resolves after every retained recovery settles.
1506
+ */
1507
+ async _retryPendingHandoffRecoveries() {
1508
+ for (const [handoffId, jobId] of [...this.pendingHandoffRecoveries]) {
1509
+ try {
1510
+ await this._recoverHandoff({handoffId, jobId})
1511
+ } catch (error) {
1512
+ this._reportHandoffRecoveryError({error, handoffId, jobId})
1513
+ throw error
1514
+ }
1515
+ }
1516
+ }
1517
+
1518
+ /**
1519
+ * Surfaces a failed exact-ID recovery without dropping its retry ledger entry.
1520
+ * @param {object} args - Recovery failure.
1521
+ * @param {ReturnType<typeof JSON.parse>} args.error - Adapter failure.
1522
+ * @param {string} args.handoffId - Exact lease fence.
1523
+ * @param {string} args.jobId - Job id.
1524
+ * @returns {void}
1525
+ */
1526
+ _reportHandoffRecoveryError({error, handoffId, jobId}) {
1527
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
1528
+ const payload = {
1529
+ context: {handoffId, jobId, stage: "background-job-handoff-admission-recovery"},
1530
+ error: normalizedError
1531
+ }
1532
+ const errorEvents = this.configuration.getErrorEvents()
1533
+
1534
+ this.logger.error(() => ["Failed to recover an ambiguous background job handoff:", normalizedError])
1535
+ errorEvents.emit("framework-error", payload)
1536
+ errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
1537
+ }
1538
+
1385
1539
  /**
1386
1540
  * Runs next available job for ready workers.
1387
1541
  * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Next queued job matching ready worker capacity.
@@ -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"})
@@ -916,10 +916,11 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
916
916
  * Runs mark handed off.
917
917
  * @param {object} args - Options.
918
918
  * @param {string} args.jobId - Job id.
919
+ * @param {string} [args.handoffId] - Caller-selected exact lease id. Generated for legacy direct callers when omitted.
919
920
  * @param {string} [args.workerId] - Worker id.
920
921
  * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Claimed handoff lease, or null when no longer queued.
921
922
  */
922
- async markHandedOff({jobId, workerId}) {
923
+ async markHandedOff({jobId, handoffId = randomUUID(), workerId}) {
923
924
  await this.ensureReady()
924
925
 
925
926
  const handedOffAtMs = Date.now()
@@ -928,8 +929,6 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
928
929
  const queuedJob = await this._getJobRowById(db, jobId)
929
930
  if (!queuedJob || queuedJob.status !== "queued") return null
930
931
  if (queuedJob.concurrencyKey && !(await this._reserveConcurrency(db, queuedJob.concurrencyKey))) return null
931
- const handoffId = randomUUID()
932
-
933
932
  const affectedRows = await this._updateAffectedRows(db, {
934
933
  tableName: JOBS_TABLE,
935
934
  data: {
@@ -43,6 +43,12 @@
43
43
  * @property {string} handoffId - Unique handoff lease id.
44
44
  * @property {number} handedOffAtMs - Time handed to a worker in ms.
45
45
  */
46
+ /**
47
+ * @typedef {object} BackgroundJobHandoffRequest
48
+ * @property {string} jobId - Job to claim.
49
+ * @property {string} [handoffId] - Exact caller-selected lease id. Adapters must persist and return this id when supplied; built-in adapters generate one when omitted for legacy direct callers.
50
+ * @property {string} [workerId] - Worker claiming the job.
51
+ */
46
52
  /**
47
53
  * @typedef {object} BackgroundJobOptions
48
54
  * @property {BackgroundJobExecutionMode} [executionMode] - How the job should run. Node defaults to `"pooled"` (a warm, reused local runner process). Browser/Expo local dispatch defaults to and only accepts `"inline"`. `"forked"` runs a Node job in a fresh `child_process.fork()` child, and `"spawned"` in a detached CLI runner.
@@ -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
- * Tells main we're ready for the next job — but only if we haven't been
604
- * asked to drain. Once we've sent `draining` we don't want to take more
605
- * work.
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 acceptsPooled = this._availablePooledSlots() > 0
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: this._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
- return new Promise((resolve) => {
693
- const timeoutTimer = this._armPooledJobTimeout({child, payload})
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
- state.inflight.set(payload.id, {payload, resolve, timeoutTimer})
696
- try {
697
- child.send({type: "job", payload, sharedTransactionBroker: this._pooledJobSharedTransactionBrokerConfig()})
698
- } catch (error) {
699
- void this._handlePooledChildFailure({child, error})
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; capacity is refilled on the next dispatch.
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. Capacity is refilled lazily on the next
911
- * dispatch (a spawnable slot is still advertised), avoiding a tight respawn
912
- * loop when a child crashes on startup.
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
- await Promise.allSettled(entries.map(async (entry) => {
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
  /**
@@ -81,13 +81,12 @@ export default class BackgroundJobsAdapter {
81
81
  getJob(_jobId: string): Promise<import("./types.js").BackgroundJobRow | null>;
82
82
  /**
83
83
  * Starts a job by claiming its durable handoff.
84
- * @param {{jobId: string, workerId?: string}} _args - Handoff request.
84
+ * When `handoffId` is supplied, the adapter must persist and return that exact
85
+ * id so the caller can fence an ambiguous commit acknowledgement.
86
+ * @param {import("./types.js").BackgroundJobHandoffRequest} _args - Handoff request.
85
87
  * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Claimed handoff.
86
88
  */
87
- markHandedOff(_args: {
88
- jobId: string;
89
- workerId?: string;
90
- }): Promise<import("./types.js").BackgroundJobHandoff | null>;
89
+ markHandedOff(_args: import("./types.js").BackgroundJobHandoffRequest): Promise<import("./types.js").BackgroundJobHandoff | null>;
91
90
  /**
92
91
  * Marks a handed-off job successful.
93
92
  * @param {{jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number}} _args - Completion report.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/adapter.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/adapter.js"],"names":[],"mappings":"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"}