velocious 1.0.602 → 1.0.604
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 +38 -5
- package/build/configuration.js +19 -0
- package/build/database/drivers/base.js +296 -46
- package/build/database/drivers/mysql/deadlock-diagnostic-parser.js +227 -0
- package/build/database/drivers/mysql/index.js +42 -36
- package/build/database/pool/async-tracked-multi-connection.js +145 -42
- package/build/database/pool/base.js +20 -3
- package/build/environment-handlers/base.js +16 -0
- package/build/environment-handlers/node/cli/commands/test.js +220 -84
- package/build/environment-handlers/node.js +40 -1
- package/build/src/configuration.d.ts +9 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +17 -1
- package/build/src/database/drivers/base.d.ts +150 -4
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +271 -44
- package/build/src/database/drivers/mysql/deadlock-diagnostic-parser.d.ts +80 -0
- package/build/src/database/drivers/mysql/deadlock-diagnostic-parser.d.ts.map +1 -0
- package/build/src/database/drivers/mysql/deadlock-diagnostic-parser.js +196 -0
- package/build/src/database/drivers/mysql/index.d.ts +21 -2
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +43 -36
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +42 -2
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +127 -43
- package/build/src/database/pool/base.d.ts +18 -1
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +18 -3
- package/build/src/environment-handlers/base.d.ts +14 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +15 -1
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +22 -0
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +200 -82
- package/build/src/environment-handlers/node.d.ts +14 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +38 -2
- package/build/src/testing/test-filter-parser.d.ts +15 -0
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +57 -6
- package/build/src/testing/test-profile-activity.d.ts +7 -0
- package/build/src/testing/test-profile-activity.d.ts.map +1 -0
- package/build/src/testing/test-profile-activity.js +13 -0
- package/build/src/testing/test-profile-context.d.ts +15 -0
- package/build/src/testing/test-profile-context.d.ts.map +1 -0
- package/build/src/testing/test-profile-context.js +25 -0
- package/build/src/testing/test-profile-output.d.ts +30 -0
- package/build/src/testing/test-profile-output.d.ts.map +1 -0
- package/build/src/testing/test-profile-output.js +204 -0
- package/build/src/testing/test-profiler.d.ts +668 -0
- package/build/src/testing/test-profiler.d.ts.map +1 -0
- package/build/src/testing/test-profiler.js +847 -0
- package/build/src/testing/test-runner.d.ts +85 -2
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +182 -33
- package/build/testing/test-filter-parser.js +57 -5
- package/build/testing/test-profile-activity.js +14 -0
- package/build/testing/test-profile-context.js +28 -0
- package/build/testing/test-profile-output.js +240 -0
- package/build/testing/test-profiler.js +917 -0
- package/build/testing/test-runner.js +196 -33
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/configuration.js +19 -0
- package/src/database/drivers/base.js +296 -46
- package/src/database/drivers/mysql/deadlock-diagnostic-parser.js +227 -0
- package/src/database/drivers/mysql/index.js +42 -36
- package/src/database/pool/async-tracked-multi-connection.js +145 -42
- package/src/database/pool/base.js +20 -3
- package/src/environment-handlers/base.js +16 -0
- package/src/environment-handlers/node/cli/commands/test.js +220 -84
- package/src/environment-handlers/node.js +40 -1
- package/src/testing/test-filter-parser.js +57 -5
- package/src/testing/test-profile-activity.js +14 -0
- package/src/testing/test-profile-context.js +28 -0
- package/src/testing/test-profile-output.js +240 -0
- package/src/testing/test-profiler.js +917 -0
- package/src/testing/test-runner.js +196 -33
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
|
|
27
27
|
* Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
|
|
28
28
|
* Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, linting, and a process-global reload-retention budget that bounds cache-busted re-import memory (see [docs/factories.md](docs/factories.md))
|
|
29
|
+
* Opt-in Benchmark-style test profiling with privacy-safe rich JSON and directly reusable duration-aware shard manifests (see [docs/test-profiling.md](docs/test-profiling.md))
|
|
29
30
|
* Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters, safe batching of structurally identical aggregates, and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
|
|
30
31
|
* Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
|
|
31
32
|
* Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
|
|
@@ -227,6 +228,22 @@ Slowest 10 tests:
|
|
|
227
228
|
|
|
228
229
|
The report is skipped for single-test runs. See [docs/testing-guidelines.md](docs/testing-guidelines.md).
|
|
229
230
|
|
|
231
|
+
Add `--profile` for a compact Benchmark-style phase and pool summary. Use
|
|
232
|
+
`--profile-json <path>` for versioned, privacy-safe detail or
|
|
233
|
+
`--timing-manifest-output <path>` to generate a sorted per-file duration map for
|
|
234
|
+
the existing `--timing-manifest` shard input; either output flag implies
|
|
235
|
+
profiling.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npx velocious test --profile-json tmp/test-profile.json \
|
|
239
|
+
--timing-manifest-output tmp/test-timings.json
|
|
240
|
+
npx velocious test --groups=4 --group-number=1 \
|
|
241
|
+
--timing-manifest tmp/test-timings.json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
See [test profiling](docs/test-profiling.md) for lifecycle accounting, custom
|
|
245
|
+
activity spans, schema, and privacy guarantees.
|
|
246
|
+
|
|
230
247
|
Prefer waiting for a real signal or condition over sleeping a fixed duration. `waitForEvent(emitter, eventName, {timeoutMs, filter})` resolves the instant a matching event fires (a background job finishing, a model update, a websocket message) and rejects on timeout; for polling an arbitrary condition, use awaitery's `waitFor`.
|
|
231
248
|
|
|
232
249
|
```js
|
|
@@ -1769,7 +1786,12 @@ database: {
|
|
|
1769
1786
|
|
|
1770
1787
|
`pool.max` caps live async-tracked connections for that pool and defaults to `10` when omitted. When the cap is reached, new checkouts wait until a matching checked-in connection can be handed over or capacity is freed. Set `pool.max` to `null` only when a process is deliberately allowed to open an unbounded number of database connections. The built-in debug endpoint reports each in-use connection's `checkedOutForMs`, each idle connection's `idleForMs`, and queued `pendingCheckouts[].waitingForMs` so production diagnostics can distinguish long-held checkouts from pool-capacity waits.
|
|
1771
1788
|
|
|
1772
|
-
Debug snapshots also expose cumulative checkout-wait and
|
|
1789
|
+
Debug snapshots also expose cumulative connection-creation, checkout-wait and
|
|
1790
|
+
timeout, idle-reap, and peak-live-connection telemetry. Opt-in test profiles can
|
|
1791
|
+
attribute safe aggregate deltas to their current spans. The [MySQL idle-reaping
|
|
1792
|
+
benchmark and methodology](docs/mysql-pool-idle-reaping-research.md) compare the
|
|
1793
|
+
5-second default with 60 seconds and disabled reaping under a fixed cap; absent
|
|
1794
|
+
representative measured evidence, retain the 5-second default.
|
|
1773
1795
|
|
|
1774
1796
|
# Websockets
|
|
1775
1797
|
|
|
@@ -1921,10 +1943,21 @@ Genuinely unexpected frontend-model command failures reach this bus too. The fro
|
|
|
1921
1943
|
|
|
1922
1944
|
Unexpected inbound decoded WebSocket dispatch failures emit one `framework-error` and one matching `all-error`. Established expected client-flow errors remain excluded from both events.
|
|
1923
1945
|
|
|
1924
|
-
MySQL/MariaDB transaction
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1946
|
+
MySQL/MariaDB transaction contention that will run another outer attempt emits a structured
|
|
1947
|
+
`database-deadlock-retry` event through `configuration.getErrorEvents()` and mirrors it to `all-error` with
|
|
1948
|
+
`errorType: "database-deadlock-retry"`. Required context fields are `stage`, `driverType`,
|
|
1949
|
+
`contentionKind`, `attempt`, `maxAttempts`, `willRetry` (always `true` for this retry-only event), and
|
|
1950
|
+
`transactionAttemptDurationMs`. Exhausted/non-retried contention emits no retry event.
|
|
1951
|
+
Pool-owned connections optionally add logical `databaseIdentifier` and opaque
|
|
1952
|
+
`databaseIdentifierFingerprint`/`databaseIdentityFingerprint` fields; the logical identifier itself
|
|
1953
|
+
is always redacted. Named checkouts always redact `operationName` and add only its bounded opaque
|
|
1954
|
+
`operationNameFingerprint` for correlation. Query failures add
|
|
1955
|
+
`sqlOperation`/`sqlFingerprint`. True deadlocks may add `statusCapture` and the bounded structural
|
|
1956
|
+
`innodbDeadlockSummary`, including explicit bounded MariaDB counterparty conflict edges whose owner
|
|
1957
|
+
is intentionally not inferred; lock-wait timeouts report `statusCapture: "not-applicable"` and never attach a
|
|
1958
|
+
historical graph. Capture, parsing, and listeners remain detached from rollback and retry control
|
|
1959
|
+
flow. See the exact identity, redaction, structural bounds, failure-channel, and optional-field
|
|
1960
|
+
contract in [deadlock retry diagnostics](docs/logging.md#deadlock-retry-diagnostics).
|
|
1928
1961
|
|
|
1929
1962
|
## Use the Websocket client API (HTTP-like)
|
|
1930
1963
|
|
package/build/configuration.js
CHANGED
|
@@ -35,6 +35,7 @@ import {frontendModelApiManifest, frontendModelResourceClassFromDefinition, fron
|
|
|
35
35
|
import {currentOfflineGrantSigningKey, normalizeOfflineGrantSigningKey} from "./sync/offline-grant.js"
|
|
36
36
|
import PluginRoutes from "./routes/plugin-routes.js"
|
|
37
37
|
import restArgsError from "./utils/rest-args-error.js"
|
|
38
|
+
import { validateTestActivityName } from "./testing/test-profile-activity.js"
|
|
38
39
|
import {validateTimeZone} from "./time-zone.js"
|
|
39
40
|
import {withTrackedStack} from "./utils/with-tracked-stack.js"
|
|
40
41
|
import VelociousPackage from "./packages/velocious-package.js"
|
|
@@ -3150,6 +3151,24 @@ export default class VelociousConfiguration {
|
|
|
3150
3151
|
return await this.getEnvironmentHandler().runWithRequestTiming(requestTiming, callback)
|
|
3151
3152
|
}
|
|
3152
3153
|
|
|
3154
|
+
/**
|
|
3155
|
+
* Profiles an application-defined test activity when an opt-in test profile
|
|
3156
|
+
* context is active. The callback always runs, including outside profiling.
|
|
3157
|
+
* @template T
|
|
3158
|
+
* @param {string} name - Low-cardinality activity identifier.
|
|
3159
|
+
* @param {() => (T | Promise<T>)} callback - Activity callback.
|
|
3160
|
+
* @returns {Promise<T>} - Callback result.
|
|
3161
|
+
*/
|
|
3162
|
+
async profileTestActivity(name, callback) {
|
|
3163
|
+
const validatedName = validateTestActivityName(name)
|
|
3164
|
+
|
|
3165
|
+
const context = this.getEnvironmentHandler().getCurrentTestProfileContext()
|
|
3166
|
+
|
|
3167
|
+
if (!context) return await callback()
|
|
3168
|
+
|
|
3169
|
+
return await context.profiler.profileActivity(context, validatedName, callback)
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3153
3172
|
/**
|
|
3154
3173
|
* Runs run with timezone.
|
|
3155
3174
|
* @param {string} timeZone - IANA timezone identifier.
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
* @property {boolean} retry - Whether the error should be retried.
|
|
55
55
|
* @property {boolean} reconnect - Whether to reconnect before retrying.
|
|
56
56
|
* @property {boolean} [deadlock] - Whether the error is a transaction deadlock/lock-wait-timeout that should retry the whole transaction.
|
|
57
|
+
* @property {"deadlock" | "lock-wait-timeout"} [contentionKind] - Classified transaction contention kind.
|
|
57
58
|
* @property {number} [maxTries] - Override the max retry attempts.
|
|
58
59
|
* @property {number} [waitMs] - Wait time before retrying in milliseconds.
|
|
59
60
|
*/
|
|
@@ -71,6 +72,33 @@
|
|
|
71
72
|
* @property {symbol} [operationOwner] - Opaque owner for an operation-leased connection.
|
|
72
73
|
*/
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* DeadlockRetryDiagnosticSnapshot type.
|
|
77
|
+
* @typedef {object} DeadlockRetryDiagnosticSnapshot
|
|
78
|
+
* @property {number} attempt - One-based transaction attempt.
|
|
79
|
+
* @property {"deadlock" | "lock-wait-timeout"} contentionKind - Classified contention kind.
|
|
80
|
+
* @property {string} [databaseIdentifier] - Redacted logical database pool identifier marker.
|
|
81
|
+
* @property {string} [databaseIdentifierFingerprint] - Opaque logical database pool identity.
|
|
82
|
+
* @property {string} [databaseIdentityFingerprint] - Opaque physical database identity.
|
|
83
|
+
* @property {string} driverType - Driver type.
|
|
84
|
+
* @property {number} maxAttempts - Configured transaction attempt budget.
|
|
85
|
+
* @property {string} [operationName] - Redacted operation-name marker.
|
|
86
|
+
* @property {string} [operationNameFingerprint] - Opaque operation-name identity.
|
|
87
|
+
* @property {string} [sqlFingerprint] - Normalized SQL-shape fingerprint.
|
|
88
|
+
* @property {string} [sqlOperation] - SQL verb.
|
|
89
|
+
* @property {string} stage - Error-event stage.
|
|
90
|
+
* @property {number} transactionAttemptDurationMs - Duration of the failed outer attempt.
|
|
91
|
+
* @property {boolean} willRetry - Whether another outer transaction attempt will run.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* TestProfileQueryAttempt type.
|
|
96
|
+
* @typedef {object} TestProfileQueryAttempt
|
|
97
|
+
* @property {import("../../testing/test-profiler.js").TestProfileAsyncContext} context - Captured async attribution.
|
|
98
|
+
* @property {{sqlFingerprint: string, sqlOperation: string}} diagnostic - Redacted statement diagnostic.
|
|
99
|
+
* @property {number} startedAtMs - Physical attempt start time.
|
|
100
|
+
*/
|
|
101
|
+
|
|
74
102
|
/**
|
|
75
103
|
* ActiveQueryDebugSnapshot type.
|
|
76
104
|
* @typedef {object} ActiveQueryDebugSnapshot
|
|
@@ -144,19 +172,24 @@ import TableForeignKey from "../table-data/table-foreign-key.js"
|
|
|
144
172
|
import wait from "awaitery/build/wait.js"
|
|
145
173
|
import { optionalPositiveInteger } from "typanic"
|
|
146
174
|
import { coordinateSharedTransactionConnection } from "../../testing/shared-transaction-connection-coordinator.js"
|
|
175
|
+
import { currentTestProfileContext } from "../../testing/test-profile-context.js"
|
|
176
|
+
import sha256Hex from "../../utils/sha256-hex.js"
|
|
147
177
|
|
|
148
178
|
/** Maximum characters inspected when building the debug SQL preview. */
|
|
149
179
|
const SQL_PREVIEW_SCAN_LIMIT = 4096
|
|
150
180
|
/** Maximum characters inspected when deciding whether a statement invalidates schema metadata. */
|
|
151
181
|
const SCHEMA_INVALIDATION_SCAN_LIMIT = 8192
|
|
182
|
+
/** Maximum checkout-name characters inspected by retry diagnostics. */
|
|
183
|
+
const OPERATION_NAME_SCAN_LIMIT = 1024
|
|
184
|
+
const REDACTED_DIAGNOSTIC_LABEL = "[REDACTED]"
|
|
152
185
|
|
|
153
186
|
/**
|
|
154
187
|
* Builds a non-reversible, stable SQL fingerprint without retaining SQL text. Literal spelling is
|
|
155
188
|
* normalized first so the same statement shape produces the same fingerprint across values.
|
|
156
189
|
* @param {string} sql - SQL to fingerprint.
|
|
157
|
-
* @returns {string} - Bounded
|
|
190
|
+
* @returns {{sqlFingerprint: string, sqlOperation: string}} - Bounded query diagnostic.
|
|
158
191
|
*/
|
|
159
|
-
function
|
|
192
|
+
function sqlDiagnostic(sql) {
|
|
160
193
|
let fingerprintInput = ""
|
|
161
194
|
|
|
162
195
|
for (let index = 0; index < sql.length;) {
|
|
@@ -206,7 +239,12 @@ function sqlFingerprint(sql) {
|
|
|
206
239
|
hash = BigInt.asUintN(64, hash * 0x100000001b3n)
|
|
207
240
|
}
|
|
208
241
|
|
|
209
|
-
|
|
242
|
+
const operationMatch = /^([a-z]+)/.exec(normalized)
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
sqlFingerprint: `fnv1a64:${hash.toString(16).padStart(16, "0")}`,
|
|
246
|
+
sqlOperation: operationMatch ? operationMatch[1].toUpperCase() : "UNKNOWN"
|
|
247
|
+
}
|
|
210
248
|
}
|
|
211
249
|
|
|
212
250
|
/**
|
|
@@ -270,6 +308,10 @@ export default class VelociousDatabaseDriversBase {
|
|
|
270
308
|
* Narrows the runtime value to the documented type.
|
|
271
309
|
* @type {string | undefined} */
|
|
272
310
|
_connectionCheckoutName
|
|
311
|
+
/** @type {string | undefined} */
|
|
312
|
+
_databaseIdentifier
|
|
313
|
+
/** @type {string | undefined} */
|
|
314
|
+
_databaseIdentityFingerprint
|
|
273
315
|
/**
|
|
274
316
|
* Active query.
|
|
275
317
|
* @type {ActiveQueryState | null} */
|
|
@@ -465,6 +507,16 @@ export default class VelociousDatabaseDriversBase {
|
|
|
465
507
|
this._connectionCheckedOutAtUnixMs = undefined
|
|
466
508
|
}
|
|
467
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Sets the pool-owned identity used by safe database diagnostics.
|
|
512
|
+
* @param {{databaseIdentifier: string, databaseIdentityFingerprint: string}} identity - Pool-stamped identity redacted at diagnostic snapshot time.
|
|
513
|
+
* @returns {void}
|
|
514
|
+
*/
|
|
515
|
+
setPoolDiagnosticIdentity({databaseIdentifier, databaseIdentityFingerprint}) {
|
|
516
|
+
this._databaseIdentifier = databaseIdentifier
|
|
517
|
+
this._databaseIdentityFingerprint = databaseIdentityFingerprint
|
|
518
|
+
}
|
|
519
|
+
|
|
468
520
|
/**
|
|
469
521
|
* Runs reconnect.
|
|
470
522
|
* @returns {Promise<void>} - Resolves when complete.
|
|
@@ -1304,6 +1356,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1304
1356
|
|
|
1305
1357
|
while (true) {
|
|
1306
1358
|
attempt++
|
|
1359
|
+
const attemptStartedAtMs = this._nowMs()
|
|
1307
1360
|
|
|
1308
1361
|
try {
|
|
1309
1362
|
return await this._runTransactionAttempt(callback, options)
|
|
@@ -1312,9 +1365,17 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1312
1365
|
if (!(error instanceof Error)) throw error
|
|
1313
1366
|
|
|
1314
1367
|
const retryInfo = this.retryableDatabaseError(error)
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1368
|
+
const willRetry = Boolean(retryInfo.deadlock && attempt < maxAttempts && this._transactionsCount == 0)
|
|
1369
|
+
|
|
1370
|
+
if (willRetry) {
|
|
1371
|
+
this._reportDeadlockRetryDiagnostic({
|
|
1372
|
+
attempt,
|
|
1373
|
+
contentionKind: retryInfo.contentionKind || "deadlock",
|
|
1374
|
+
error,
|
|
1375
|
+
maxAttempts,
|
|
1376
|
+
transactionAttemptDurationMs: Math.max(0, this._nowMs() - attemptStartedAtMs),
|
|
1377
|
+
willRetry
|
|
1378
|
+
})
|
|
1318
1379
|
|
|
1319
1380
|
// An explicitly-configured base wins so the tuning knob is effective even on drivers
|
|
1320
1381
|
// whose classifier supplies its own `waitMs` (MySQL/MariaDB return a fixed 50ms for
|
|
@@ -1330,7 +1391,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1330
1391
|
const ceilingWaitMs = Math.min(baseWaitMs * (2 ** (attempt - 1)), deadlockMaxWaitMs)
|
|
1331
1392
|
const jitteredWaitMs = Math.floor(Math.random() * (ceilingWaitMs + 1))
|
|
1332
1393
|
|
|
1333
|
-
|
|
1394
|
+
const loggedContentionKind = retryInfo.contentionKind || "transaction contention"
|
|
1395
|
+
|
|
1396
|
+
this.logger.warn(`Retrying transaction after ${loggedContentionKind} (attempt ${attempt}/${maxAttempts})`)
|
|
1334
1397
|
await this._waitMs(jitteredWaitMs)
|
|
1335
1398
|
continue
|
|
1336
1399
|
}
|
|
@@ -1350,29 +1413,67 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1350
1413
|
await wait(ms)
|
|
1351
1414
|
}
|
|
1352
1415
|
|
|
1416
|
+
/**
|
|
1417
|
+
* Returns the clock used for transaction-attempt diagnostics.
|
|
1418
|
+
* @returns {number} - Monotonic milliseconds where available.
|
|
1419
|
+
*/
|
|
1420
|
+
_nowMs() {
|
|
1421
|
+
return nowMs()
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1353
1424
|
/**
|
|
1354
1425
|
* Starts best-effort deadlock diagnostics without joining the retry control flow. Subclasses may
|
|
1355
1426
|
* add bounded driver-specific context; capture and event-listener failures cannot affect retry.
|
|
1356
|
-
* @param {{attempt: number, error: Error, maxAttempts: number}} args - Retry metadata.
|
|
1427
|
+
* @param {{attempt: number, contentionKind: "deadlock" | "lock-wait-timeout", error: Error, maxAttempts: number, transactionAttemptDurationMs: number, willRetry: boolean}} args - Retry metadata.
|
|
1357
1428
|
* @returns {void}
|
|
1358
1429
|
*/
|
|
1359
|
-
_reportDeadlockRetryDiagnostic({attempt, error, maxAttempts}) {
|
|
1360
|
-
|
|
1430
|
+
_reportDeadlockRetryDiagnostic({attempt, contentionKind, error, maxAttempts, transactionAttemptDurationMs, willRetry}) {
|
|
1431
|
+
let snapshot
|
|
1432
|
+
|
|
1433
|
+
try {
|
|
1434
|
+
const queryDiagnostic = this._failedQueryDiagnostics.get(error)
|
|
1435
|
+
|
|
1436
|
+
snapshot = Object.freeze({
|
|
1437
|
+
attempt,
|
|
1438
|
+
contentionKind,
|
|
1439
|
+
driverType: this.getType(),
|
|
1440
|
+
maxAttempts,
|
|
1441
|
+
stage: "database-deadlock-retry",
|
|
1442
|
+
transactionAttemptDurationMs,
|
|
1443
|
+
willRetry,
|
|
1444
|
+
...this._poolDiagnosticIdentityContext(),
|
|
1445
|
+
...this._operationDiagnosticContext(),
|
|
1446
|
+
...queryDiagnostic
|
|
1447
|
+
})
|
|
1448
|
+
} catch (diagnosticError) {
|
|
1449
|
+
this._reportDeadlockDiagnosticPipelineFailure(diagnosticError)
|
|
1450
|
+
return
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
let driverContextResult
|
|
1454
|
+
|
|
1455
|
+
try {
|
|
1456
|
+
driverContextResult = this._deadlockDiagnosticContext(snapshot)
|
|
1457
|
+
} catch (diagnosticError) {
|
|
1458
|
+
this._reportDeadlockDiagnosticPipelineFailure(diagnosticError)
|
|
1459
|
+
return
|
|
1460
|
+
}
|
|
1361
1461
|
|
|
1362
|
-
|
|
1462
|
+
const hasPromiseContract = driverContextResult instanceof Promise
|
|
1463
|
+
|
|
1464
|
+
void Promise.resolve(driverContextResult)
|
|
1363
1465
|
.then((driverContext) => {
|
|
1466
|
+
if (!hasPromiseContract) throw new Error("Database deadlock diagnostic context must return a Promise")
|
|
1467
|
+
|
|
1364
1468
|
const context = {
|
|
1365
|
-
|
|
1366
|
-
driverType: this.getType(),
|
|
1367
|
-
maxAttempts,
|
|
1368
|
-
stage: "database-deadlock-retry",
|
|
1369
|
-
willRetry: true,
|
|
1370
|
-
...queryDiagnostic,
|
|
1469
|
+
...snapshot,
|
|
1371
1470
|
...driverContext
|
|
1372
1471
|
}
|
|
1373
1472
|
const payload = {
|
|
1374
1473
|
context,
|
|
1375
|
-
error: new Error(
|
|
1474
|
+
error: new Error(willRetry
|
|
1475
|
+
? `Database transaction ${contentionKind} will be retried`
|
|
1476
|
+
: `Database transaction ${contentionKind} exhausted its retry budget`)
|
|
1376
1477
|
}
|
|
1377
1478
|
const errorEvents = this.configuration.getErrorEvents()
|
|
1378
1479
|
|
|
@@ -1388,30 +1489,93 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1388
1489
|
this.logger.warn("Database deadlock retry all-error listener failed", {error: eventError})
|
|
1389
1490
|
}
|
|
1390
1491
|
})
|
|
1391
|
-
.catch((diagnosticError) =>
|
|
1392
|
-
|
|
1393
|
-
? diagnosticError
|
|
1394
|
-
: new Error("Database deadlock retry diagnostic failed", {cause: diagnosticError})
|
|
1395
|
-
const payload = {
|
|
1396
|
-
context: {stage: "database-deadlock-retry-diagnostic"},
|
|
1397
|
-
error: normalizedError
|
|
1398
|
-
}
|
|
1492
|
+
.catch((diagnosticError) => this._reportDeadlockDiagnosticPipelineFailure(diagnosticError))
|
|
1493
|
+
}
|
|
1399
1494
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1495
|
+
/**
|
|
1496
|
+
* Returns pool identity only when this driver was stamped by a pool.
|
|
1497
|
+
* @returns {{databaseIdentifier?: string, databaseIdentifierFingerprint?: string, databaseIdentityFingerprint?: string}} - Safe pool identity.
|
|
1498
|
+
*/
|
|
1499
|
+
_poolDiagnosticIdentityContext() {
|
|
1500
|
+
if (this._databaseIdentifier === undefined || !this._databaseIdentityFingerprint) return {}
|
|
1501
|
+
|
|
1502
|
+
const identifierFingerprintInput = typeof this._databaseIdentifier === "string"
|
|
1503
|
+
? this._databaseIdentifier
|
|
1504
|
+
: `invalid:${typeof this._databaseIdentifier}`
|
|
1505
|
+
const databaseIdentifierFingerprint = `sha256:${sha256Hex(`database-logical-identifier:v1\0${identifierFingerprintInput}`)}`
|
|
1506
|
+
|
|
1507
|
+
return {
|
|
1508
|
+
databaseIdentifier: REDACTED_DIAGNOSTIC_LABEL,
|
|
1509
|
+
databaseIdentifierFingerprint,
|
|
1510
|
+
databaseIdentityFingerprint: this._databaseIdentityFingerprint
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Builds the bounded operation portion of an immutable retry snapshot.
|
|
1516
|
+
* @returns {{operationName?: string, operationNameFingerprint?: string}} - Safe operation fields.
|
|
1517
|
+
*/
|
|
1518
|
+
_operationDiagnosticContext() {
|
|
1519
|
+
const rawOperationName = this._connectionCheckoutName
|
|
1520
|
+
|
|
1521
|
+
if (rawOperationName === undefined) return {}
|
|
1522
|
+
if (typeof rawOperationName !== "string") {
|
|
1523
|
+
return {
|
|
1524
|
+
operationName: REDACTED_DIAGNOSTIC_LABEL,
|
|
1525
|
+
operationNameFingerprint: `sha256:${sha256Hex(`database-operation:v1\0invalid:${typeof rawOperationName}`)}`
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const scannedOperationName = rawOperationName.slice(0, OPERATION_NAME_SCAN_LIMIT)
|
|
1530
|
+
const operationNameFingerprint = `sha256:${sha256Hex(`database-operation:v1\0${scannedOperationName}\0length:${rawOperationName.length}`)}`
|
|
1531
|
+
|
|
1532
|
+
return {
|
|
1533
|
+
operationName: REDACTED_DIAGNOSTIC_LABEL,
|
|
1534
|
+
operationNameFingerprint
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* Reports an unexpected detached diagnostics failure without changing transaction control flow.
|
|
1540
|
+
* @param {ReturnType<typeof JSON.parse>} diagnosticError - Diagnostics failure.
|
|
1541
|
+
* @returns {void}
|
|
1542
|
+
*/
|
|
1543
|
+
_reportDeadlockDiagnosticPipelineFailure(diagnosticError) {
|
|
1544
|
+
const normalizedError = diagnosticError instanceof Error
|
|
1545
|
+
? diagnosticError
|
|
1546
|
+
: new Error("Database deadlock retry diagnostic failed", {cause: diagnosticError})
|
|
1547
|
+
const payload = {
|
|
1548
|
+
context: {stage: "database-deadlock-retry-diagnostic"},
|
|
1549
|
+
error: normalizedError
|
|
1550
|
+
}
|
|
1551
|
+
let errorEvents
|
|
1552
|
+
|
|
1553
|
+
try {
|
|
1554
|
+
errorEvents = this.configuration.getErrorEvents()
|
|
1555
|
+
} catch (reportingError) {
|
|
1556
|
+
this.logger.warn("Database deadlock retry diagnostic pipeline reporting failed", {error: normalizedError, reportingError})
|
|
1557
|
+
return
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
try {
|
|
1561
|
+
errorEvents.emit("framework-error", payload)
|
|
1562
|
+
} catch (reportingError) {
|
|
1563
|
+
this.logger.warn("Database deadlock retry framework-error listener failed", {error: normalizedError, reportingError})
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
try {
|
|
1567
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
1568
|
+
} catch (reportingError) {
|
|
1569
|
+
this.logger.warn("Database deadlock retry all-error listener failed", {error: normalizedError, reportingError})
|
|
1570
|
+
}
|
|
1408
1571
|
}
|
|
1409
1572
|
|
|
1410
1573
|
/**
|
|
1411
1574
|
* Builds driver-specific deadlock context. The base driver has no server diagnostic source.
|
|
1575
|
+
* @param {DeadlockRetryDiagnosticSnapshot} _snapshot - Immutable retry snapshot.
|
|
1412
1576
|
* @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>>} - Safe context fields.
|
|
1413
1577
|
*/
|
|
1414
|
-
async _deadlockDiagnosticContext() {
|
|
1578
|
+
async _deadlockDiagnosticContext(_snapshot) {
|
|
1415
1579
|
return {}
|
|
1416
1580
|
}
|
|
1417
1581
|
|
|
@@ -1585,7 +1749,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1585
1749
|
return
|
|
1586
1750
|
}
|
|
1587
1751
|
|
|
1588
|
-
await this.
|
|
1752
|
+
await this._runProfiledTransactionAction("start", async () => {
|
|
1753
|
+
await this._startTransactionAction(options)
|
|
1754
|
+
})
|
|
1589
1755
|
this._transactionsCount++
|
|
1590
1756
|
|
|
1591
1757
|
if (this._transactionsCount === 1) {
|
|
@@ -1617,7 +1783,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1617
1783
|
*/
|
|
1618
1784
|
async commitTransaction(options = {}) {
|
|
1619
1785
|
await this._transactionsActionsMutex.sync(async () => {
|
|
1620
|
-
await this.
|
|
1786
|
+
await this._runProfiledTransactionAction("commit", async () => {
|
|
1787
|
+
await this._commitTransactionAction(options)
|
|
1788
|
+
})
|
|
1621
1789
|
this._transactionsCount--
|
|
1622
1790
|
this._resolveCompletedTransaction()
|
|
1623
1791
|
})
|
|
@@ -1642,6 +1810,68 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1642
1810
|
await this.query("COMMIT", options)
|
|
1643
1811
|
}
|
|
1644
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* Times a physical transaction action only when test profiling is active.
|
|
1815
|
+
* @template T
|
|
1816
|
+
* @param {"start" | "commit" | "rollback"} action - Transaction action.
|
|
1817
|
+
* @param {() => Promise<T>} callback - Physical action callback.
|
|
1818
|
+
* @returns {Promise<T>} - Callback result.
|
|
1819
|
+
*/
|
|
1820
|
+
async _runProfiledTransactionAction(action, callback) {
|
|
1821
|
+
const profileContext = currentTestProfileContext(this.configuration)
|
|
1822
|
+
|
|
1823
|
+
if (!profileContext) return await callback()
|
|
1824
|
+
|
|
1825
|
+
const startedAtMs = nowMs()
|
|
1826
|
+
let failed = true
|
|
1827
|
+
|
|
1828
|
+
try {
|
|
1829
|
+
const result = await callback()
|
|
1830
|
+
|
|
1831
|
+
failed = false
|
|
1832
|
+
return result
|
|
1833
|
+
} finally {
|
|
1834
|
+
profileContext.profiler.recordDatabaseTransaction(profileContext, {
|
|
1835
|
+
action,
|
|
1836
|
+
durationMs: nowMs() - startedAtMs,
|
|
1837
|
+
failed
|
|
1838
|
+
})
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Starts an optional physical-query profile attempt without retaining SQL.
|
|
1844
|
+
* @param {string} sql - Original SQL used only to derive its redacted diagnostic.
|
|
1845
|
+
* @returns {TestProfileQueryAttempt | undefined} - Active profile handle.
|
|
1846
|
+
*/
|
|
1847
|
+
_startProfiledQueryAttempt(sql) {
|
|
1848
|
+
const context = currentTestProfileContext(this.configuration)
|
|
1849
|
+
|
|
1850
|
+
if (!context) return undefined
|
|
1851
|
+
|
|
1852
|
+
return {
|
|
1853
|
+
context,
|
|
1854
|
+
diagnostic: sqlDiagnostic(sql),
|
|
1855
|
+
startedAtMs: nowMs()
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Completes an optional physical-query profile attempt.
|
|
1861
|
+
* @param {TestProfileQueryAttempt | undefined} attempt - Profile handle.
|
|
1862
|
+
* @param {boolean} failed - Whether the physical driver call failed.
|
|
1863
|
+
* @returns {void}
|
|
1864
|
+
*/
|
|
1865
|
+
_finishProfiledQueryAttempt(attempt, failed) {
|
|
1866
|
+
if (!attempt) return
|
|
1867
|
+
|
|
1868
|
+
attempt.context.profiler.recordDatabaseQuery(attempt.context, {
|
|
1869
|
+
durationMs: nowMs() - attempt.startedAtMs,
|
|
1870
|
+
failed,
|
|
1871
|
+
...attempt.diagnostic
|
|
1872
|
+
})
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1645
1875
|
/**
|
|
1646
1876
|
* Runs every guard registered to the transaction frame.
|
|
1647
1877
|
* @param {TransactionCallbackFrame} callbackFrame - Frame whose guards are completing.
|
|
@@ -1716,10 +1946,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1716
1946
|
} catch (error) {
|
|
1717
1947
|
if (!(error instanceof Error)) throw error
|
|
1718
1948
|
|
|
1719
|
-
this._failedQueryDiagnostics.set(error,
|
|
1720
|
-
sqlFingerprint: sqlFingerprint(sql),
|
|
1721
|
-
sqlOperation: sql.trim().split(/\s+/, 1)[0]?.toUpperCase() || "UNKNOWN"
|
|
1722
|
-
})
|
|
1949
|
+
this._failedQueryDiagnostics.set(error, sqlDiagnostic(sql))
|
|
1723
1950
|
|
|
1724
1951
|
// A deliberately-aborted query must never be silently re-run — its
|
|
1725
1952
|
// connection was destroyed on purpose, so treat it as terminal.
|
|
@@ -1764,7 +1991,17 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1764
1991
|
await this.beforeQuery(sql, options)
|
|
1765
1992
|
|
|
1766
1993
|
try {
|
|
1767
|
-
|
|
1994
|
+
const profileAttempt = this._startProfiledQueryAttempt(sql)
|
|
1995
|
+
let failed = true
|
|
1996
|
+
|
|
1997
|
+
try {
|
|
1998
|
+
const affectedRows = await this._affectedRowsActual(sql)
|
|
1999
|
+
|
|
2000
|
+
failed = false
|
|
2001
|
+
return affectedRows
|
|
2002
|
+
} finally {
|
|
2003
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
2004
|
+
}
|
|
1768
2005
|
} finally {
|
|
1769
2006
|
await this.afterQuery(sql, options)
|
|
1770
2007
|
}
|
|
@@ -1793,7 +2030,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1793
2030
|
let result
|
|
1794
2031
|
|
|
1795
2032
|
try {
|
|
1796
|
-
const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options)
|
|
2033
|
+
const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options, originalSql)
|
|
1797
2034
|
|
|
1798
2035
|
if (requestTiming && tries === 1) {
|
|
1799
2036
|
result = await requestTiming.measureDbQuery(runQueryActualWithHooks)
|
|
@@ -1828,14 +2065,25 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1828
2065
|
* Runs query actual with before/after hooks.
|
|
1829
2066
|
* @param {string} sql - SQL string.
|
|
1830
2067
|
* @param {QueryOptions} options - Query options.
|
|
2068
|
+
* @param {string} originalSql - SQL before process-list comments.
|
|
1831
2069
|
* @returns {Promise<QueryResultType>} - Resolves with the query.
|
|
1832
2070
|
*/
|
|
1833
|
-
async _queryActualWithHooks(sql, options) {
|
|
2071
|
+
async _queryActualWithHooks(sql, options, originalSql) {
|
|
1834
2072
|
return await coordinateSharedTransactionConnection(this, async () => {
|
|
1835
2073
|
await this.beforeQuery(sql, options)
|
|
1836
2074
|
|
|
1837
2075
|
try {
|
|
1838
|
-
|
|
2076
|
+
const profileAttempt = this._startProfiledQueryAttempt(originalSql)
|
|
2077
|
+
let failed = true
|
|
2078
|
+
|
|
2079
|
+
try {
|
|
2080
|
+
const result = await this._queryActual(sql, options)
|
|
2081
|
+
|
|
2082
|
+
failed = false
|
|
2083
|
+
return result
|
|
2084
|
+
} finally {
|
|
2085
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
2086
|
+
}
|
|
1839
2087
|
} finally {
|
|
1840
2088
|
await this.afterQuery(sql, options)
|
|
1841
2089
|
}
|
|
@@ -2251,7 +2499,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2251
2499
|
async rollbackTransaction(options = {}) {
|
|
2252
2500
|
await this._transactionsActionsMutex.sync(async () => {
|
|
2253
2501
|
try {
|
|
2254
|
-
await this.
|
|
2502
|
+
await this._runProfiledTransactionAction("rollback", async () => {
|
|
2503
|
+
await this._rollbackTransactionAction(options)
|
|
2504
|
+
})
|
|
2255
2505
|
} finally {
|
|
2256
2506
|
this._transactionsCount--
|
|
2257
2507
|
this._resolveCompletedTransaction()
|