velocious 1.0.695 → 1.0.697

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 (62) hide show
  1. package/README.md +1 -1
  2. package/build/background-jobs/adapter.js +1 -1
  3. package/build/background-jobs/client.js +1 -1
  4. package/build/background-jobs/job-record.js +23 -0
  5. package/build/background-jobs/local-adapter.js +1 -1
  6. package/build/background-jobs/local-store.js +10 -5
  7. package/build/background-jobs/main.js +6 -1
  8. package/build/background-jobs/owner-liveness.js +92 -0
  9. package/build/background-jobs/pooled-runner-child.js +5 -1
  10. package/build/background-jobs/status-reporter.js +7 -4
  11. package/build/background-jobs/store.js +13 -5
  12. package/build/background-jobs/types.js +2 -1
  13. package/build/background-jobs/worker.js +4 -2
  14. package/build/database/migrations-ledger.js +96 -8
  15. package/build/src/background-jobs/adapter.d.ts +2 -1
  16. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  17. package/build/src/background-jobs/adapter.js +2 -2
  18. package/build/src/background-jobs/client.js +2 -2
  19. package/build/src/background-jobs/job-record.d.ts +16 -0
  20. package/build/src/background-jobs/job-record.d.ts.map +1 -1
  21. package/build/src/background-jobs/job-record.js +23 -1
  22. package/build/src/background-jobs/local-adapter.d.ts +2 -1
  23. package/build/src/background-jobs/local-adapter.d.ts.map +1 -1
  24. package/build/src/background-jobs/local-adapter.js +2 -2
  25. package/build/src/background-jobs/local-store.d.ts +5 -3
  26. package/build/src/background-jobs/local-store.d.ts.map +1 -1
  27. package/build/src/background-jobs/local-store.js +12 -6
  28. package/build/src/background-jobs/main.d.ts.map +1 -1
  29. package/build/src/background-jobs/main.js +7 -2
  30. package/build/src/background-jobs/owner-liveness.d.ts +71 -0
  31. package/build/src/background-jobs/owner-liveness.d.ts.map +1 -0
  32. package/build/src/background-jobs/owner-liveness.js +78 -0
  33. package/build/src/background-jobs/pooled-runner-child.js +6 -2
  34. package/build/src/background-jobs/status-reporter.d.ts +6 -2
  35. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  36. package/build/src/background-jobs/status-reporter.js +8 -5
  37. package/build/src/background-jobs/store.d.ts +5 -3
  38. package/build/src/background-jobs/store.d.ts.map +1 -1
  39. package/build/src/background-jobs/store.js +15 -6
  40. package/build/src/background-jobs/types.d.ts +7 -1
  41. package/build/src/background-jobs/types.d.ts.map +1 -1
  42. package/build/src/background-jobs/types.js +3 -2
  43. package/build/src/background-jobs/worker.d.ts +2 -1
  44. package/build/src/background-jobs/worker.d.ts.map +1 -1
  45. package/build/src/background-jobs/worker.js +6 -4
  46. package/build/src/database/migrations-ledger.d.ts +34 -1
  47. package/build/src/database/migrations-ledger.d.ts.map +1 -1
  48. package/build/src/database/migrations-ledger.js +82 -9
  49. package/package.json +1 -1
  50. package/src/background-jobs/adapter.js +1 -1
  51. package/src/background-jobs/client.js +1 -1
  52. package/src/background-jobs/job-record.js +23 -0
  53. package/src/background-jobs/local-adapter.js +1 -1
  54. package/src/background-jobs/local-store.js +10 -5
  55. package/src/background-jobs/main.js +6 -1
  56. package/src/background-jobs/owner-liveness.js +92 -0
  57. package/src/background-jobs/pooled-runner-child.js +5 -1
  58. package/src/background-jobs/status-reporter.js +7 -4
  59. package/src/background-jobs/store.js +13 -5
  60. package/src/background-jobs/types.js +2 -1
  61. package/src/background-jobs/worker.js +4 -2
  62. package/src/database/migrations-ledger.js +96 -8
@@ -223,6 +223,7 @@
223
223
  * @property {number | null} childStartedAtMs - Epoch ms when the job's perform started in the pooled child, or null when it never started.
224
224
  * @property {string | null} childInstanceId - Stable identity of the pooled child process that accepted the job, or null.
225
225
  * @property {number | null} childPid - OS pid of the pooled child process that accepted the job, or null.
226
+ * @property {string | null} runnerHostname - Hostname reported by the pooled child process that accepted the job, or null.
226
227
  */
227
228
  /**
228
229
  * @typedef {"queued" | "handed_off" | null} BackgroundJobReplacementPreviousStatus
@@ -292,7 +293,7 @@
292
293
  * @typedef {{type: "schedule-woken", jobId: string | null, outcome: BackgroundJobWakeOutcome}} BackgroundJobScheduleWokenMessage
293
294
  * @typedef {{type: "wake-scheduled-error", error?: string}} BackgroundJobWakeScheduledErrorMessage
294
295
  * @typedef {{type: "job", payload: BackgroundJobPayload}} BackgroundJobJobMessage
295
- * @typedef {{type: "job-accepted", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number}} BackgroundJobAcceptedMessage
296
+ * @typedef {{type: "job-accepted", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number, runnerHostname?: string}} BackgroundJobAcceptedMessage
296
297
  * @typedef {{type: "job-complete", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobCompleteMessage
297
298
  * @typedef {{type: "job-failed", jobId: string, error?: ReturnType<typeof JSON.parse>, handoffId?: string, workerId?: string, handedOffAtMs?: number, runnerFailure?: PooledRunnerFailure}} BackgroundJobFailedMessage
298
299
  * @typedef {{type: "job-reschedule", jobId: string, delayMs: number, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobRescheduleMessage
@@ -93,7 +93,7 @@ function positiveInteger(value) {
93
93
  * one job. The child carries its exact handoff lease so the worker can forward
94
94
  * the report without depending on its in-flight entry still existing.
95
95
  * @param {ReturnType<typeof JSON.parse>} message - IPC message.
96
- * @returns {message is {type: "job-received" | "job-started", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number}} - Whether this is a valid acceptance message.
96
+ * @returns {message is {type: "job-received" | "job-started", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number, runnerHostname?: string}} - Whether this is a valid acceptance message.
97
97
  */
98
98
  function isChildAcceptanceMessage(message) {
99
99
  if (!message || typeof message !== "object") return false
@@ -108,6 +108,7 @@ function isChildAcceptanceMessage(message) {
108
108
  && (record.startedAtMs === undefined || typeof record.startedAtMs === "number")
109
109
  && (record.childInstanceId === undefined || typeof record.childInstanceId === "string")
110
110
  && (record.childPid === undefined || Number.isInteger(record.childPid))
111
+ && (record.runnerHostname === undefined || typeof record.runnerHostname === "string")
111
112
  }
112
113
 
113
114
  /**
@@ -1510,7 +1511,7 @@ export default class BackgroundJobsWorker {
1510
1511
  * or outcome that already settled the worker's in-flight entry cannot lose
1511
1512
  * the fencing. A report that never lands degrades phase diagnostics for that
1512
1513
  * job only — it must never block or fail the job itself.
1513
- * @param {{type: "job-received" | "job-started", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number}} message - Validated child acceptance message.
1514
+ * @param {{type: "job-received" | "job-started", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number, receivedAtMs?: number, startedAtMs?: number, childInstanceId?: string, childPid?: number, runnerHostname?: string}} message - Validated child acceptance message.
1514
1515
  * @returns {void}
1515
1516
  */
1516
1517
  _reportChildAccepted(message) {
@@ -1525,6 +1526,7 @@ export default class BackgroundJobsWorker {
1525
1526
  startedAtMs: message.startedAtMs,
1526
1527
  childInstanceId: message.childInstanceId,
1527
1528
  childPid: message.childPid,
1529
+ runnerHostname: message.runnerHostname,
1528
1530
  maxDurationMs: CHILD_ACCEPTANCE_REPORT_MAX_DURATION_MS
1529
1531
  }).catch((error) => {
1530
1532
  console.error("Background job child-acceptance reporting failed:", error)
@@ -4,6 +4,11 @@ import {digg} from "diggerize"
4
4
  import TableData from "./table-data/index.js"
5
5
 
6
6
  const TABLE_NAME = "schema_migrations"
7
+ /** SQL Server "There is already an object named ..." — raised when two sessions race to create the same table. */
8
+ const MSSQL_OBJECT_ALREADY_EXISTS_ERROR_NUMBER = 2714
9
+ /** SQL Server deadlock victim — raised when concurrent sessions deadlock on the same catalog reads. */
10
+ const MSSQL_DEADLOCK_VICTIM_ERROR_NUMBER = 1205
11
+
7
12
 
8
13
  /**
9
14
  * Single owner of the `schema_migrations` ledger shape and the only place that reads
@@ -35,24 +40,107 @@ export default class MigrationsLedger {
35
40
  return Boolean(table)
36
41
  }
37
42
 
43
+ /**
44
+ * Whether the error means a concurrent session already created the ledger table.
45
+ *
46
+ * Several processes bootstrap the same database at the same time (for example every
47
+ * CI test shard), and the MSSQL `IF NOT EXISTS (...) BEGIN CREATE TABLE ... END` guard
48
+ * is NOT atomic: two sessions can both evaluate the check as false and then both
49
+ * create, so the loser is rejected. Walks the wrapped-error cause chain because
50
+ * `db.query` re-wraps the driver's `RequestError` in a plain `Error`, and matches the
51
+ * stable numeric error number instead of the localized message text.
52
+ * @param {unknown} error - Error thrown while creating the ledger table.
53
+ * @returns {boolean} - True if the table was created by a concurrent session.
54
+ */
55
+ static isConcurrentCreationError(error) {
56
+ let current = error
57
+
58
+ while (current instanceof Error) {
59
+ const errorNumber = /** @type {{number?: unknown}} */ (current).number
60
+
61
+ if (typeof errorNumber === "number" && errorNumber === MSSQL_OBJECT_ALREADY_EXISTS_ERROR_NUMBER) return true
62
+
63
+ current = current.cause
64
+ }
65
+
66
+ return false
67
+ }
68
+
69
+ /**
70
+ * Whether the error is a transient deadlock that should be retried.
71
+ *
72
+ * Concurrent boots also deadlock on the `INFORMATION_SCHEMA` metadata scan that
73
+ * `getTableByName` performs on SQL Server; the victim transaction rolled back cleanly,
74
+ * so retrying it is safe. Matched by numeric error number, never by message text.
75
+ * @param {unknown} error - Error thrown while inspecting or creating the ledger table.
76
+ * @returns {boolean} - True if the error is a retryable deadlock.
77
+ */
78
+ static isDeadlockError(error) {
79
+ let current = error
80
+
81
+ while (current instanceof Error) {
82
+ const errorNumber = /** @type {{number?: unknown}} */ (current).number
83
+
84
+ if (typeof errorNumber === "number" && errorNumber === MSSQL_DEADLOCK_VICTIM_ERROR_NUMBER) return true
85
+
86
+ current = current.cause
87
+ }
88
+
89
+ return false
90
+ }
91
+
38
92
  /**
39
93
  * Creates the ledger table if it does not exist. This is the single definition of
40
94
  * the `schema_migrations` table shape.
95
+ *
96
+ * Safe under concurrency: a session that loses the create race (or deadlocks on the
97
+ * catalog read) converges on the existing table instead of failing its caller. That
98
+ * matters because this runs on every boot of a database configured for migrations
99
+ * (see `Migrator#createMigrationsTableForDatabase` and
100
+ * `migrateRequireContextForDatabase`), so a crash here takes down a whole test shard
101
+ * or deployment over a table that provably already exists.
41
102
  * @param {import("./drivers/base.js").default} db - Database that should contain the ledger table.
103
+ * @param {{maxAttempts?: number}} [args] - Retry budget for transient catalog deadlocks.
42
104
  * @returns {Promise<void>}
43
105
  */
44
- static async ensureTable(db) {
45
- if (await MigrationsLedger.tableExists(db)) return
106
+ static async ensureTable(db, {maxAttempts = 3} = {}) {
107
+ let attempt = 0
46
108
 
47
- const tableData = new TableData(TABLE_NAME, {ifNotExists: true})
109
+ while (true) {
110
+ attempt++
48
111
 
49
- tableData.string("version", {null: false, primaryKey: true})
112
+ try {
113
+ if (await MigrationsLedger.tableExists(db)) return
50
114
 
51
- for (const sql of await db.createTableSql(tableData)) {
52
- await db.query(sql)
53
- }
115
+ const tableData = new TableData(TABLE_NAME, {ifNotExists: true})
116
+
117
+ tableData.string("version", {null: false, primaryKey: true})
54
118
 
55
- db.clearSchemaCache()
119
+ for (const sql of await db.createTableSql(tableData)) {
120
+ await db.query(sql)
121
+ }
122
+
123
+ db.clearSchemaCache()
124
+
125
+ return
126
+ } catch (error) {
127
+ if (MigrationsLedger.isConcurrentCreationError(error)) {
128
+ // Another session created the ledger between our check and our create. The
129
+ // table exists with exactly this shape, so the goal state is already reached.
130
+ db.clearSchemaCache()
131
+
132
+ return
133
+ }
134
+
135
+ if (MigrationsLedger.isDeadlockError(error) && attempt < maxAttempts) {
136
+ db.clearSchemaCache()
137
+
138
+ continue
139
+ }
140
+
141
+ throw error
142
+ }
143
+ }
56
144
  }
57
145
 
58
146
  /**