velocious 1.0.641 → 1.0.642
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 +9 -0
- package/build/background-jobs/main.js +21 -6
- package/build/background-jobs/store.js +116 -7
- package/build/src/background-jobs/main.d.ts +3 -2
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +26 -7
- package/build/src/background-jobs/store.d.ts +16 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +103 -7
- package/package.json +1 -1
- package/src/background-jobs/main.js +21 -6
- package/src/background-jobs/store.js +116 -7
package/package.json
CHANGED
|
@@ -325,7 +325,7 @@ export default class BackgroundJobsMain {
|
|
|
325
325
|
}, this.workerLivenessSweepMs)
|
|
326
326
|
|
|
327
327
|
if (this.lifecycleState === "active") {
|
|
328
|
-
await this._startActiveOwnership()
|
|
328
|
+
await this._startActiveOwnership("active")
|
|
329
329
|
} else if (this.lifecycleState === "retired") {
|
|
330
330
|
this._startGenerationRecoveryOwnership()
|
|
331
331
|
}
|
|
@@ -519,17 +519,27 @@ export default class BackgroundJobsMain {
|
|
|
519
519
|
|
|
520
520
|
/**
|
|
521
521
|
* Acquires scheduling and dispatch ownership for an active generation.
|
|
522
|
-
* @
|
|
522
|
+
* @param {"active" | "candidate"} expectedLifecycleState - State that still owns activation.
|
|
523
|
+
* @returns {Promise<boolean>} - Whether active ownership was established.
|
|
523
524
|
*/
|
|
524
|
-
async _startActiveOwnership() {
|
|
525
|
+
async _startActiveOwnership(expectedLifecycleState) {
|
|
525
526
|
await this.store.reconcileQueueConcurrency()
|
|
527
|
+
if (this.lifecycleState !== expectedLifecycleState) return false
|
|
526
528
|
this._setupDispatchTriggers()
|
|
527
529
|
this._setupStartupHandoffReclaim()
|
|
528
530
|
this._startOrphanSweep()
|
|
529
531
|
await this._startScheduler()
|
|
532
|
+
if (this.lifecycleState !== expectedLifecycleState) {
|
|
533
|
+
if (this.scheduler) await this.scheduler.stop()
|
|
534
|
+
this.scheduler = undefined
|
|
535
|
+
this._clearDispatchTimers()
|
|
536
|
+
this._disconnectBeaconHandlers()
|
|
537
|
+
return false
|
|
538
|
+
}
|
|
530
539
|
this._activeOwnershipReady = true
|
|
531
540
|
this._creditReadyWorkers()
|
|
532
541
|
await this._drain()
|
|
542
|
+
return this.lifecycleState === expectedLifecycleState
|
|
533
543
|
}
|
|
534
544
|
|
|
535
545
|
/** Starts exact recovery duties without acquiring global dispatch ownership. */
|
|
@@ -603,7 +613,10 @@ export default class BackgroundJobsMain {
|
|
|
603
613
|
*/
|
|
604
614
|
async _activate() {
|
|
605
615
|
this.logger.info(() => ["Background jobs generation activation starting", {generationId: this.generationId}])
|
|
606
|
-
await this._startActiveOwnership()
|
|
616
|
+
const ownershipStarted = await this._startActiveOwnership("candidate")
|
|
617
|
+
if (!ownershipStarted || this.lifecycleState !== "candidate") {
|
|
618
|
+
throw new Error("Background jobs generation retirement started before activation acquired ownership")
|
|
619
|
+
}
|
|
607
620
|
this.lifecycleState = "active"
|
|
608
621
|
this._creditReadyWorkers()
|
|
609
622
|
this.logger.info(() => ["Background jobs generation activation acknowledged", {generationId: this.generationId}])
|
|
@@ -619,7 +632,8 @@ export default class BackgroundJobsMain {
|
|
|
619
632
|
retire() {
|
|
620
633
|
if (!this.generationId) throw new Error("Background jobs generation retirement requires generation mode")
|
|
621
634
|
if (this.lifecycleState === "retiring" || this.lifecycleState === "retired") return Promise.resolve()
|
|
622
|
-
|
|
635
|
+
const activationInProgress = this.lifecycleState === "candidate" && Boolean(this._activationPromise)
|
|
636
|
+
if (this.lifecycleState !== "active" && !activationInProgress) throw new Error(`Cannot retire background jobs generation from ${this.lifecycleState}`)
|
|
623
637
|
|
|
624
638
|
this.lifecycleState = "retiring"
|
|
625
639
|
this._activeOwnershipReady = false
|
|
@@ -638,7 +652,8 @@ export default class BackgroundJobsMain {
|
|
|
638
652
|
* @returns {Promise<void>} - Retirement fence completion.
|
|
639
653
|
*/
|
|
640
654
|
async _retire() {
|
|
641
|
-
await this.
|
|
655
|
+
if (this._activationPromise) await Promise.allSettled([this._activationPromise])
|
|
656
|
+
if (this.scheduler) await this.scheduler.stop()
|
|
642
657
|
this.scheduler = undefined
|
|
643
658
|
if (this._drainPromise) await this._drainPromise
|
|
644
659
|
if (this._stopped) return
|
|
@@ -66,12 +66,24 @@ const EXECUTION_MODE_BACKFILL_MIGRATION_VERSION = "20260607131010"
|
|
|
66
66
|
// handoff-marker workaround), leaving `execution_mode` as the single source of
|
|
67
67
|
// truth for a job's runtime.
|
|
68
68
|
const DROP_FORKED_COLUMN_MIGRATION_VERSION = "20260719000000"
|
|
69
|
+
const JOBS_INDEX_REPAIR_MIGRATION_VERSION = "20260903120000"
|
|
69
70
|
// Legacy marker prefix used by rows written before this migration: pooled jobs
|
|
70
71
|
// used to persist as `execution_mode = "forked"` plus a `velocious-pooled:*`
|
|
71
72
|
// handoff id. Retained only to detect and convert those rows in the migration.
|
|
72
73
|
const LEGACY_POOLED_HANDOFF_ID_PREFIX = "velocious-pooled:"
|
|
73
74
|
const LEGACY_POOLED_QUEUED_HANDOFF_ID = `${LEGACY_POOLED_HANDOFF_ID_PREFIX}queued`
|
|
74
75
|
const JOBS_TABLE = "background_jobs"
|
|
76
|
+
const JOBS_INDEX_COLUMN_NAMES = [
|
|
77
|
+
"job_name",
|
|
78
|
+
"queue",
|
|
79
|
+
"status",
|
|
80
|
+
"scheduled_at_ms",
|
|
81
|
+
"created_at_ms",
|
|
82
|
+
"schedule_key",
|
|
83
|
+
"handed_off_at_ms",
|
|
84
|
+
"orphaned_at_ms",
|
|
85
|
+
"concurrency_key"
|
|
86
|
+
]
|
|
75
87
|
const IDEMPOTENCY_KEYS_TABLE = "background_job_idempotency_keys"
|
|
76
88
|
const SCHEDULE_KEYS_TABLE = "background_job_schedule_keys"
|
|
77
89
|
const CONCURRENCY_TABLE = "background_job_concurrency"
|
|
@@ -1838,6 +1850,50 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
1838
1850
|
await this._ensureQueueColumn(db)
|
|
1839
1851
|
await this._ensureScheduleKeyColumn(db)
|
|
1840
1852
|
await this._ensureJobTimeoutColumn(db)
|
|
1853
|
+
await this._ensureJobsTableIndexesOnce(db)
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Repairs secondary indexes that older add-column upgrades declared but did
|
|
1858
|
+
* not create on every SQL driver. The migration ledger keeps routine store
|
|
1859
|
+
* readiness from repeatedly introspecting the full index set.
|
|
1860
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
1861
|
+
* @returns {Promise<void>} - Resolves when all expected indexes exist.
|
|
1862
|
+
*/
|
|
1863
|
+
async _ensureJobsTableIndexesOnce(db) {
|
|
1864
|
+
const migrationVersion = JOBS_INDEX_REPAIR_MIGRATION_VERSION
|
|
1865
|
+
const migrationKey = this._migrationKey(migrationVersion)
|
|
1866
|
+
|
|
1867
|
+
if (await this._hasMigration(db, migrationVersion)) return
|
|
1868
|
+
|
|
1869
|
+
const acquired = await db.acquireAdvisoryLock(migrationKey)
|
|
1870
|
+
|
|
1871
|
+
if (!acquired) throw new Error("Failed to acquire background jobs index repair lock")
|
|
1872
|
+
|
|
1873
|
+
try {
|
|
1874
|
+
if (await this._hasMigration(db, migrationVersion)) return
|
|
1875
|
+
|
|
1876
|
+
db.clearSchemaCache()
|
|
1877
|
+
const table = await db.getTableByNameOrFail(JOBS_TABLE)
|
|
1878
|
+
const indexedColumnNames = new Set(
|
|
1879
|
+
(await table.getIndexes())
|
|
1880
|
+
.filter((index) => !index.isPrimaryKey() && index.getColumnNames().length === 1)
|
|
1881
|
+
.map((index) => index.getColumnNames()[0])
|
|
1882
|
+
)
|
|
1883
|
+
|
|
1884
|
+
for (const columnName of JOBS_INDEX_COLUMN_NAMES) {
|
|
1885
|
+
if (indexedColumnNames.has(columnName)) continue
|
|
1886
|
+
|
|
1887
|
+
for (const sql of await db.createIndexSQLs({columns: [columnName], ifNotExists: db.getType() === "sqlite", tableName: JOBS_TABLE})) {
|
|
1888
|
+
await db.query(sql)
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
db.clearSchemaCache()
|
|
1893
|
+
await this._recordMigration(db, migrationVersion)
|
|
1894
|
+
} finally {
|
|
1895
|
+
await db.releaseAdvisoryLock(migrationKey)
|
|
1896
|
+
}
|
|
1841
1897
|
}
|
|
1842
1898
|
|
|
1843
1899
|
/**
|
|
@@ -2711,13 +2767,61 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2711
2767
|
*/
|
|
2712
2768
|
async _reconcileConcurrency(db) {
|
|
2713
2769
|
if (!(await db.tableExists(CONCURRENCY_TABLE))) return
|
|
2714
|
-
const
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
`${
|
|
2770
|
+
const activeRows = await db
|
|
2771
|
+
.newQuery()
|
|
2772
|
+
.from(JOBS_TABLE)
|
|
2773
|
+
.select("concurrency_key")
|
|
2774
|
+
.where({status: "handed_off"})
|
|
2775
|
+
.where(`${db.quoteColumn("concurrency_key")} IS NOT NULL`)
|
|
2776
|
+
.group("concurrency_key")
|
|
2777
|
+
.results()
|
|
2778
|
+
const staleRows = await db
|
|
2779
|
+
.newQuery()
|
|
2780
|
+
.from(CONCURRENCY_TABLE)
|
|
2781
|
+
.select("concurrency_key")
|
|
2782
|
+
.where(`${db.quoteColumn("active_count")} != 0`)
|
|
2783
|
+
.results()
|
|
2784
|
+
const concurrencyKeys = new Set(
|
|
2785
|
+
[...activeRows, ...staleRows].map((row) =>
|
|
2786
|
+
String(/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row).concurrency_key)
|
|
2787
|
+
)
|
|
2720
2788
|
)
|
|
2789
|
+
|
|
2790
|
+
for (const concurrencyKey of [...concurrencyKeys].sort()) {
|
|
2791
|
+
await this._transactionResult(db, async () => {
|
|
2792
|
+
await this._reconcileConcurrencyKey(db, concurrencyKey)
|
|
2793
|
+
})
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
/**
|
|
2798
|
+
* Rebuilds one counter after locking it ahead of the job rows, matching the
|
|
2799
|
+
* lock order used by handoff and completion transitions.
|
|
2800
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
2801
|
+
* @param {string} concurrencyKey - Counter key.
|
|
2802
|
+
* @returns {Promise<void>} - Resolves when reconciled.
|
|
2803
|
+
*/
|
|
2804
|
+
async _reconcileConcurrencyKey(db, concurrencyKey) {
|
|
2805
|
+
await this._lockConcurrencyRow(db, concurrencyKey)
|
|
2806
|
+
const rows = await db
|
|
2807
|
+
.newQuery()
|
|
2808
|
+
.from(JOBS_TABLE)
|
|
2809
|
+
.select("COUNT(*) AS active_count")
|
|
2810
|
+
.where({concurrency_key: concurrencyKey, status: "handed_off"})
|
|
2811
|
+
.results()
|
|
2812
|
+
const activeCount = this._normalizeNumber(
|
|
2813
|
+
/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (rows[0] || {}).active_count
|
|
2814
|
+
)
|
|
2815
|
+
|
|
2816
|
+
if (activeCount === null || !Number.isSafeInteger(activeCount) || activeCount < 0) {
|
|
2817
|
+
throw new Error(`Invalid reconciled background job concurrency count for ${concurrencyKey}: ${activeCount}`)
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
await db.update({
|
|
2821
|
+
tableName: CONCURRENCY_TABLE,
|
|
2822
|
+
data: {active_count: activeCount},
|
|
2823
|
+
conditions: {concurrency_key: concurrencyKey}
|
|
2824
|
+
})
|
|
2721
2825
|
}
|
|
2722
2826
|
|
|
2723
2827
|
/**
|
|
@@ -2768,7 +2872,12 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
|
|
|
2768
2872
|
)
|
|
2769
2873
|
}
|
|
2770
2874
|
|
|
2771
|
-
const concurrencyRows = await db
|
|
2875
|
+
const concurrencyRows = await db
|
|
2876
|
+
.newQuery()
|
|
2877
|
+
.from(CONCURRENCY_TABLE)
|
|
2878
|
+
.select("concurrency_key")
|
|
2879
|
+
.where(`${db.quoteColumn("concurrency_key")} LIKE ${db.quote(`${QUEUE_CONCURRENCY_KEY_PREFIX}%`)}`)
|
|
2880
|
+
.results()
|
|
2772
2881
|
|
|
2773
2882
|
for (const row of concurrencyRows) {
|
|
2774
2883
|
const concurrencyKey = String(/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row).concurrency_key)
|