velocious 1.0.669 → 1.0.670

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.669",
6
+ "version": "1.0.670",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -59,6 +59,16 @@ import {
59
59
  * @property {{failureMessage: string, name: string}} [advisoryLock] - Session lock held around the transaction.
60
60
  */
61
61
 
62
+ /**
63
+ * BackgroundJobPruneCandidateMetadata type. Immutable metadata for one selected
64
+ * retention candidate batch, exposed to the optional prune barrier hook.
65
+ * @typedef {object} BackgroundJobPruneCandidateMetadata
66
+ * @property {Readonly<Array<string>>} candidates - Job ids selected for this batch.
67
+ * @property {string} status - Terminal status the candidates were selected by.
68
+ * @property {string} column - Terminal timestamp column compared against the cutoff.
69
+ * @property {number} cutoff - Cutoff timestamp the candidates were selected against.
70
+ */
71
+
62
72
  /**
63
73
  * BackgroundJobConcurrencyCountRow type.
64
74
  * @typedef {object} BackgroundJobConcurrencyCountRow
@@ -158,13 +168,15 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
158
168
  * @param {string} [args.databaseIdentifier] - Database identifier.
159
169
  * @param {{now: () => number}} [args.clock] - Injectable persistence clock.
160
170
  * @param {(producerProof: import("./types.js").BackgroundJobProducerProof) => void | Promise<void>} [args.afterOwnedProducerValidation] - Exact owned-enqueue validation hook.
171
+ * @param {(metadata: BackgroundJobPruneCandidateMetadata) => void | Promise<void>} [args.afterPruneCandidatesSelected] - Optional barrier invoked after one retention batch's candidate discovery and before its serialized delete transaction.
161
172
  */
162
- constructor({configuration, databaseIdentifier, clock, afterOwnedProducerValidation}) {
173
+ constructor({configuration, databaseIdentifier, clock, afterOwnedProducerValidation, afterPruneCandidatesSelected}) {
163
174
  super()
164
175
  this.configuration = configuration
165
176
  this.databaseIdentifier = databaseIdentifier
166
177
  this.clock = clock || {now: () => Date.now()}
167
178
  this.afterOwnedProducerValidation = afterOwnedProducerValidation
179
+ this.afterPruneCandidatesSelected = afterPruneCandidatesSelected
168
180
  this.logger = new Logger(this)
169
181
  this._readyPromise = null
170
182
  this._queueConcurrencyReconciled = false
@@ -1790,7 +1802,14 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1790
1802
 
1791
1803
  /**
1792
1804
  * Deletes rows of one terminal status older than a cutoff, batch by batch,
1793
- * until a page returns fewer than `batchSize` rows.
1805
+ * until a candidate page returns fewer than `batchSize` rows. Candidate
1806
+ * discovery runs on a plain connection — never inside the serialized count
1807
+ * mutation — so a long scan cannot hold the count-revision lock and starve
1808
+ * enqueue acknowledgements; only the short delete transaction is serialized.
1809
+ * The delete revalidates status and cutoff for the selected ids, publishes
1810
+ * the delta from the actual affected-row count, and a page whose candidates
1811
+ * were already removed by a concurrent pruner still ends the pass only when
1812
+ * the page itself is short.
1794
1813
  * @param {object} args - Options.
1795
1814
  * @param {string} args.status - Terminal status to prune.
1796
1815
  * @param {string} args.column - Timestamp column compared against the cutoff.
@@ -1802,22 +1821,37 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1802
1821
  let deleted = 0
1803
1822
 
1804
1823
  for (;;) {
1805
- const removed = await this._serializedCountMutation(async (db) => {
1806
- const rows = await db
1824
+ const candidates = await this._withDb(async (db) =>
1825
+ await db
1807
1826
  .newQuery()
1808
1827
  .from(JOBS_TABLE)
1809
1828
  .select("id")
1810
1829
  .where({status})
1811
1830
  .where(`${db.quoteColumn(column)} <= ${db.quote(cutoff)}`)
1831
+ .order({column, direction: "ASC"})
1832
+ .order({column: "id", direction: "ASC"})
1812
1833
  .limit(batchSize)
1813
1834
  .results()
1835
+ )
1814
1836
 
1815
- if (rows.length === 0) return 0
1837
+ if (candidates.length === 0) break
1816
1838
 
1817
- const ids = rows.map((/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ row) => db.quote(String(row.id))).join(", ")
1839
+ const candidateIds = Object.freeze(candidates.map((/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ row) => String(row.id)))
1840
+
1841
+ if (this.afterPruneCandidatesSelected) {
1842
+ await this.afterPruneCandidatesSelected(Object.freeze({
1843
+ candidates: candidateIds,
1844
+ column,
1845
+ cutoff,
1846
+ status
1847
+ }))
1848
+ }
1849
+
1850
+ const removed = await this._serializedCountMutation(async (db) => {
1851
+ const ids = candidateIds.map((id) => db.quote(id)).join(", ")
1818
1852
 
1819
1853
  const removed = await db.affectedRows(
1820
- `DELETE FROM ${db.quoteTable(JOBS_TABLE)} WHERE ${db.quoteColumn("id")} IN (${ids})`
1854
+ `DELETE FROM ${db.quoteTable(JOBS_TABLE)} WHERE ${db.quoteColumn("id")} IN (${ids}) AND ${db.quoteColumn("status")} = ${db.quote(status)} AND ${db.quoteColumn(column)} <= ${db.quote(cutoff)}`
1821
1855
  )
1822
1856
 
1823
1857
  await this._recordCountDelta(db, {all: -removed, [status]: -removed})
@@ -1826,7 +1860,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1826
1860
  })
1827
1861
 
1828
1862
  deleted += removed
1829
- if (removed < batchSize) break
1863
+ if (candidates.length < batchSize) break
1830
1864
  }
1831
1865
 
1832
1866
  return deleted