velocious 1.0.601 → 1.0.603
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 +18 -7
- package/build/database/drivers/base.js +191 -39
- package/build/database/drivers/mysql/deadlock-diagnostic-parser.js +227 -0
- package/build/database/drivers/mysql/index.js +21 -30
- package/build/database/live-query.js +4 -1
- package/build/database/operation.js +9 -0
- package/build/database/pool/async-tracked-multi-connection.js +10 -15
- package/build/database/pool/base.js +19 -2
- package/build/database/record/index.js +50 -2
- package/build/database/record-changes.js +18 -4
- package/build/src/database/drivers/base.d.ts +110 -3
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +175 -37
- 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 +19 -28
- package/build/src/database/live-query.d.ts +5 -0
- package/build/src/database/live-query.d.ts.map +1 -1
- package/build/src/database/live-query.js +4 -2
- package/build/src/database/operation.d.ts +5 -0
- package/build/src/database/operation.d.ts.map +1 -1
- package/build/src/database/operation.js +9 -1
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +11 -16
- package/build/src/database/pool/base.d.ts +7 -0
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +17 -2
- package/build/src/database/record/index.d.ts +36 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +43 -3
- package/build/src/database/record-changes.d.ts +9 -1
- package/build/src/database/record-changes.d.ts.map +1 -1
- package/build/src/database/record-changes.js +18 -5
- package/build/src/sync/local-mutation-log.d.ts +7 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +17 -1
- package/build/src/sync/sync-api-client-types.d.ts +8 -4
- package/build/src/sync/sync-api-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-api-client-types.js +3 -3
- package/build/src/sync/sync-client-types.d.ts +25 -0
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +6 -1
- package/build/src/sync/sync-client.d.ts +43 -1
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +187 -37
- package/build/src/sync/sync-realtime-bridge.js +2 -2
- package/build/src/sync/sync-scope-store.d.ts +15 -1
- package/build/src/sync/sync-scope-store.d.ts.map +1 -1
- package/build/src/sync/sync-scope-store.js +31 -4
- package/build/src/tenants/tenant-handle.d.ts +81 -0
- package/build/src/tenants/tenant-handle.d.ts.map +1 -1
- package/build/src/tenants/tenant-handle.js +143 -1
- package/build/sync/local-mutation-log.js +17 -0
- package/build/sync/sync-api-client-types.js +2 -2
- package/build/sync/sync-client-types.js +5 -0
- package/build/sync/sync-client.js +198 -38
- package/build/sync/sync-realtime-bridge.js +1 -1
- package/build/sync/sync-scope-store.js +32 -3
- package/build/tenants/tenant-handle.js +165 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/database/drivers/base.js +191 -39
- package/src/database/drivers/mysql/deadlock-diagnostic-parser.js +227 -0
- package/src/database/drivers/mysql/index.js +21 -30
- package/src/database/live-query.js +4 -1
- package/src/database/operation.js +9 -0
- package/src/database/pool/async-tracked-multi-connection.js +10 -15
- package/src/database/pool/base.js +19 -2
- package/src/database/record/index.js +50 -2
- package/src/database/record-changes.js +18 -4
- package/src/sync/local-mutation-log.js +17 -0
- package/src/sync/sync-api-client-types.js +2 -2
- package/src/sync/sync-client-types.js +5 -0
- package/src/sync/sync-client.js +198 -38
- package/src/sync/sync-realtime-bridge.js +1 -1
- package/src/sync/sync-scope-store.js +32 -3
- package/src/tenants/tenant-handle.js +165 -0
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Controllers and views for HTTP endpoints
|
|
16
16
|
* Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses and one-budget WebSocket startup controls (see [docs/frontend-models.md](docs/frontend-models.md) and [docs/websocket-channels.md](docs/websocket-channels.md))
|
|
17
17
|
* Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see the [shared-resource sync developer guide](docs/shared-resource-sync-guide.md) and [offline sync architecture](docs/offline-sync.md))
|
|
18
|
-
* Declarative client sync scopes with per-scope cursors, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery
|
|
19
|
-
* Reactive `useLiveQuery(Model.where(...))` queries
|
|
18
|
+
* Declarative client sync scopes with per-scope cursors, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery, and immutable-handle project clients whose queues, scopes, cursors, receipts, conflicts, pulls, and reconnect catch-up stay in one physical tenant database (see [docs/sync-client.md](docs/sync-client.md) and [docs/offline-sync.md](docs/offline-sync.md))
|
|
19
|
+
* Reactive `useLiveQuery(Model.where(...))` queries for default databases plus immutable-handle tenant live-query sources whose committed events and refreshes stay on the captured physical tenant (see [docs/live-queries.md](docs/live-queries.md))
|
|
20
20
|
* Server-side sync envelope replay orchestration for app-owned sync receivers, including allowlisted authoritative values for conflict resolution (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
|
|
21
21
|
* Self-sustaining sync feeds: upstream imports triggered by the changes pull itself, with framework-owned coalescing and throttling (see [docs/sync-upstream-imports.md](docs/sync-upstream-imports.md))
|
|
22
22
|
* AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see the [developer guide](docs/shared-resource-sync-guide.md) and [proof](docs/awesome-tasks-offline-sync-proof.md))
|
|
@@ -1921,10 +1921,21 @@ Genuinely unexpected frontend-model command failures reach this bus too. The fro
|
|
|
1921
1921
|
|
|
1922
1922
|
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
1923
|
|
|
1924
|
-
MySQL/MariaDB transaction
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1924
|
+
MySQL/MariaDB transaction contention that will run another outer attempt emits a structured
|
|
1925
|
+
`database-deadlock-retry` event through `configuration.getErrorEvents()` and mirrors it to `all-error` with
|
|
1926
|
+
`errorType: "database-deadlock-retry"`. Required context fields are `stage`, `driverType`,
|
|
1927
|
+
`contentionKind`, `attempt`, `maxAttempts`, `willRetry` (always `true` for this retry-only event), and
|
|
1928
|
+
`transactionAttemptDurationMs`. Exhausted/non-retried contention emits no retry event.
|
|
1929
|
+
Pool-owned connections optionally add logical `databaseIdentifier` and opaque
|
|
1930
|
+
`databaseIdentifierFingerprint`/`databaseIdentityFingerprint` fields; the logical identifier itself
|
|
1931
|
+
is always redacted. Named checkouts always redact `operationName` and add only its bounded opaque
|
|
1932
|
+
`operationNameFingerprint` for correlation. Query failures add
|
|
1933
|
+
`sqlOperation`/`sqlFingerprint`. True deadlocks may add `statusCapture` and the bounded structural
|
|
1934
|
+
`innodbDeadlockSummary`, including explicit bounded MariaDB counterparty conflict edges whose owner
|
|
1935
|
+
is intentionally not inferred; lock-wait timeouts report `statusCapture: "not-applicable"` and never attach a
|
|
1936
|
+
historical graph. Capture, parsing, and listeners remain detached from rollback and retry control
|
|
1937
|
+
flow. See the exact identity, redaction, structural bounds, failure-channel, and optional-field
|
|
1938
|
+
contract in [deadlock retry diagnostics](docs/logging.md#deadlock-retry-diagnostics).
|
|
1928
1939
|
|
|
1929
1940
|
## Use the Websocket client API (HTTP-like)
|
|
1930
1941
|
|
|
@@ -2687,7 +2698,7 @@ Tenant lifecycle commands print start and final counts, report each completed te
|
|
|
2687
2698
|
|
|
2688
2699
|
`afterMigrateTenant` hooks run inside the active default and tenant database connection scope for the tenant being migrated.
|
|
2689
2700
|
|
|
2690
|
-
At runtime, the apartment-style `Tenant` façade (`velocious/build/src/tenants/tenant.js`) is the single entry point: `Tenant.with(tenant, callback)` / `Tenant.current()` to switch into and read a Node async context, `Tenant.handle(tenant)` to deeply capture immutable application and physical database identity for overlapping browser/native work, `Tenant.each({identifier, callback, parallel?, filter?})` to run a callback within every provider-listed tenant, and `Tenant.drop({identifier, tenant})` (plus the `db:tenants:drop` CLI command) to drop a tenant's database through the provider's `dropDatabase` hook. SQLite handles additionally expose framework-owned `open`, `flush`, `close`, `delete`, `inspect`, and `withPin` lifecycle methods; `frontendTenantSqlite.maxOpenHandles` bounds resident identities and clean, idle, unpinned handles are evicted least-recently-used. `Tenant.handle(...).databaseOperation(...)` and `.transaction(...)` use bounded pool-owned checkouts and pin model/query/write/association/preload/audit/attachment/raw work to the captured database even if a later UI project switch changes ambient tenant state. `Model.usingTenant(tenant)` uses the same safe core and adds eager helpers plus general `databaseOperation`/`transaction` model callbacks; eager records preserve legacy ambient Node follow-up semantics, while browser/native follow-up database work belongs inside the callback APIs. Inactive identifiers, mixed physical tenants, unsupported/cyclic descriptors,
|
|
2701
|
+
At runtime, the apartment-style `Tenant` façade (`velocious/build/src/tenants/tenant.js`) is the single entry point: `Tenant.with(tenant, callback)` / `Tenant.current()` to switch into and read a Node async context, `Tenant.handle(tenant)` to deeply capture immutable application and physical database identity for overlapping browser/native work, `Tenant.each({identifier, callback, parallel?, filter?})` to run a callback within every provider-listed tenant, and `Tenant.drop({identifier, tenant})` (plus the `db:tenants:drop` CLI command) to drop a tenant's database through the provider's `dropDatabase` hook. SQLite handles additionally expose framework-owned `open`, `flush`, `close`, `delete`, `inspect`, and `withPin` lifecycle methods; `frontendTenantSqlite.maxOpenHandles` bounds resident identities and clean, idle, unpinned handles are evicted least-recently-used. `Tenant.handle(...).databaseOperation(...)` and `.transaction(...)` use bounded pool-owned checkouts and pin model/query/write/association/preload/audit/attachment/raw work to the captured database even if a later UI project switch changes ambient tenant state. The handle also builds tenant-bound live-query sources, exposes an opaque physical identity for filtered record-change subscriptions, and binds project `SyncClient` instances. `Model.usingTenant(tenant)` uses the same safe core and adds eager helpers plus general `databaseOperation`/`transaction` model callbacks; eager records preserve legacy ambient Node follow-up semantics, while browser/native follow-up database work belongs inside the callback APIs. Inactive identifiers, mixed physical tenants, unsupported/cyclic descriptors, expired operations, unscoped tenant event subscriptions, and stale/cross-tenant sync state fail closed. `Tenant.with` and `Tenant.each` retain their connection-establishing and model-initializing ambient behavior for Node request/job flows. `Tenant.aggregateAcross({identifier, aggregates, keyColumns, subquery, tenants?, filter?})` runs one aggregate over the same table across many tenant databases and returns the merged result — grouping tenants by server and using a single cross-database `UNION ALL` where the driver supports two-part `` `database`.`table` `` references (MySQL/MariaDB) or one query per tenant otherwise (PostgreSQL/SQLite/MSSQL).
|
|
2691
2702
|
|
|
2692
2703
|
`SchemaCloner` adds a missing auto-increment column and its separate source unique index in one schema alteration, including on MySQL/MariaDB where an auto-increment column must be keyed when it is created.
|
|
2693
2704
|
|
|
@@ -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,25 @@
|
|
|
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
|
+
|
|
74
94
|
/**
|
|
75
95
|
* ActiveQueryDebugSnapshot type.
|
|
76
96
|
* @typedef {object} ActiveQueryDebugSnapshot
|
|
@@ -144,19 +164,23 @@ import TableForeignKey from "../table-data/table-foreign-key.js"
|
|
|
144
164
|
import wait from "awaitery/build/wait.js"
|
|
145
165
|
import { optionalPositiveInteger } from "typanic"
|
|
146
166
|
import { coordinateSharedTransactionConnection } from "../../testing/shared-transaction-connection-coordinator.js"
|
|
167
|
+
import sha256Hex from "../../utils/sha256-hex.js"
|
|
147
168
|
|
|
148
169
|
/** Maximum characters inspected when building the debug SQL preview. */
|
|
149
170
|
const SQL_PREVIEW_SCAN_LIMIT = 4096
|
|
150
171
|
/** Maximum characters inspected when deciding whether a statement invalidates schema metadata. */
|
|
151
172
|
const SCHEMA_INVALIDATION_SCAN_LIMIT = 8192
|
|
173
|
+
/** Maximum checkout-name characters inspected by retry diagnostics. */
|
|
174
|
+
const OPERATION_NAME_SCAN_LIMIT = 1024
|
|
175
|
+
const REDACTED_DIAGNOSTIC_LABEL = "[REDACTED]"
|
|
152
176
|
|
|
153
177
|
/**
|
|
154
178
|
* Builds a non-reversible, stable SQL fingerprint without retaining SQL text. Literal spelling is
|
|
155
179
|
* normalized first so the same statement shape produces the same fingerprint across values.
|
|
156
180
|
* @param {string} sql - SQL to fingerprint.
|
|
157
|
-
* @returns {string} - Bounded
|
|
181
|
+
* @returns {{sqlFingerprint: string, sqlOperation: string}} - Bounded query diagnostic.
|
|
158
182
|
*/
|
|
159
|
-
function
|
|
183
|
+
function sqlDiagnostic(sql) {
|
|
160
184
|
let fingerprintInput = ""
|
|
161
185
|
|
|
162
186
|
for (let index = 0; index < sql.length;) {
|
|
@@ -206,7 +230,12 @@ function sqlFingerprint(sql) {
|
|
|
206
230
|
hash = BigInt.asUintN(64, hash * 0x100000001b3n)
|
|
207
231
|
}
|
|
208
232
|
|
|
209
|
-
|
|
233
|
+
const operationMatch = /^([a-z]+)/.exec(normalized)
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
sqlFingerprint: `fnv1a64:${hash.toString(16).padStart(16, "0")}`,
|
|
237
|
+
sqlOperation: operationMatch ? operationMatch[1].toUpperCase() : "UNKNOWN"
|
|
238
|
+
}
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
/**
|
|
@@ -270,6 +299,10 @@ export default class VelociousDatabaseDriversBase {
|
|
|
270
299
|
* Narrows the runtime value to the documented type.
|
|
271
300
|
* @type {string | undefined} */
|
|
272
301
|
_connectionCheckoutName
|
|
302
|
+
/** @type {string | undefined} */
|
|
303
|
+
_databaseIdentifier
|
|
304
|
+
/** @type {string | undefined} */
|
|
305
|
+
_databaseIdentityFingerprint
|
|
273
306
|
/**
|
|
274
307
|
* Active query.
|
|
275
308
|
* @type {ActiveQueryState | null} */
|
|
@@ -465,6 +498,16 @@ export default class VelociousDatabaseDriversBase {
|
|
|
465
498
|
this._connectionCheckedOutAtUnixMs = undefined
|
|
466
499
|
}
|
|
467
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Sets the pool-owned identity used by safe database diagnostics.
|
|
503
|
+
* @param {{databaseIdentifier: string, databaseIdentityFingerprint: string}} identity - Pool-stamped identity redacted at diagnostic snapshot time.
|
|
504
|
+
* @returns {void}
|
|
505
|
+
*/
|
|
506
|
+
setPoolDiagnosticIdentity({databaseIdentifier, databaseIdentityFingerprint}) {
|
|
507
|
+
this._databaseIdentifier = databaseIdentifier
|
|
508
|
+
this._databaseIdentityFingerprint = databaseIdentityFingerprint
|
|
509
|
+
}
|
|
510
|
+
|
|
468
511
|
/**
|
|
469
512
|
* Runs reconnect.
|
|
470
513
|
* @returns {Promise<void>} - Resolves when complete.
|
|
@@ -1304,6 +1347,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1304
1347
|
|
|
1305
1348
|
while (true) {
|
|
1306
1349
|
attempt++
|
|
1350
|
+
const attemptStartedAtMs = this._nowMs()
|
|
1307
1351
|
|
|
1308
1352
|
try {
|
|
1309
1353
|
return await this._runTransactionAttempt(callback, options)
|
|
@@ -1312,9 +1356,17 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1312
1356
|
if (!(error instanceof Error)) throw error
|
|
1313
1357
|
|
|
1314
1358
|
const retryInfo = this.retryableDatabaseError(error)
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1359
|
+
const willRetry = Boolean(retryInfo.deadlock && attempt < maxAttempts && this._transactionsCount == 0)
|
|
1360
|
+
|
|
1361
|
+
if (willRetry) {
|
|
1362
|
+
this._reportDeadlockRetryDiagnostic({
|
|
1363
|
+
attempt,
|
|
1364
|
+
contentionKind: retryInfo.contentionKind || "deadlock",
|
|
1365
|
+
error,
|
|
1366
|
+
maxAttempts,
|
|
1367
|
+
transactionAttemptDurationMs: Math.max(0, this._nowMs() - attemptStartedAtMs),
|
|
1368
|
+
willRetry
|
|
1369
|
+
})
|
|
1318
1370
|
|
|
1319
1371
|
// An explicitly-configured base wins so the tuning knob is effective even on drivers
|
|
1320
1372
|
// whose classifier supplies its own `waitMs` (MySQL/MariaDB return a fixed 50ms for
|
|
@@ -1330,7 +1382,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1330
1382
|
const ceilingWaitMs = Math.min(baseWaitMs * (2 ** (attempt - 1)), deadlockMaxWaitMs)
|
|
1331
1383
|
const jitteredWaitMs = Math.floor(Math.random() * (ceilingWaitMs + 1))
|
|
1332
1384
|
|
|
1333
|
-
|
|
1385
|
+
const loggedContentionKind = retryInfo.contentionKind || "transaction contention"
|
|
1386
|
+
|
|
1387
|
+
this.logger.warn(`Retrying transaction after ${loggedContentionKind} (attempt ${attempt}/${maxAttempts})`)
|
|
1334
1388
|
await this._waitMs(jitteredWaitMs)
|
|
1335
1389
|
continue
|
|
1336
1390
|
}
|
|
@@ -1350,29 +1404,67 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1350
1404
|
await wait(ms)
|
|
1351
1405
|
}
|
|
1352
1406
|
|
|
1407
|
+
/**
|
|
1408
|
+
* Returns the clock used for transaction-attempt diagnostics.
|
|
1409
|
+
* @returns {number} - Monotonic milliseconds where available.
|
|
1410
|
+
*/
|
|
1411
|
+
_nowMs() {
|
|
1412
|
+
return nowMs()
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1353
1415
|
/**
|
|
1354
1416
|
* Starts best-effort deadlock diagnostics without joining the retry control flow. Subclasses may
|
|
1355
1417
|
* add bounded driver-specific context; capture and event-listener failures cannot affect retry.
|
|
1356
|
-
* @param {{attempt: number, error: Error, maxAttempts: number}} args - Retry metadata.
|
|
1418
|
+
* @param {{attempt: number, contentionKind: "deadlock" | "lock-wait-timeout", error: Error, maxAttempts: number, transactionAttemptDurationMs: number, willRetry: boolean}} args - Retry metadata.
|
|
1357
1419
|
* @returns {void}
|
|
1358
1420
|
*/
|
|
1359
|
-
_reportDeadlockRetryDiagnostic({attempt, error, maxAttempts}) {
|
|
1360
|
-
|
|
1421
|
+
_reportDeadlockRetryDiagnostic({attempt, contentionKind, error, maxAttempts, transactionAttemptDurationMs, willRetry}) {
|
|
1422
|
+
let snapshot
|
|
1361
1423
|
|
|
1362
|
-
|
|
1424
|
+
try {
|
|
1425
|
+
const queryDiagnostic = this._failedQueryDiagnostics.get(error)
|
|
1426
|
+
|
|
1427
|
+
snapshot = Object.freeze({
|
|
1428
|
+
attempt,
|
|
1429
|
+
contentionKind,
|
|
1430
|
+
driverType: this.getType(),
|
|
1431
|
+
maxAttempts,
|
|
1432
|
+
stage: "database-deadlock-retry",
|
|
1433
|
+
transactionAttemptDurationMs,
|
|
1434
|
+
willRetry,
|
|
1435
|
+
...this._poolDiagnosticIdentityContext(),
|
|
1436
|
+
...this._operationDiagnosticContext(),
|
|
1437
|
+
...queryDiagnostic
|
|
1438
|
+
})
|
|
1439
|
+
} catch (diagnosticError) {
|
|
1440
|
+
this._reportDeadlockDiagnosticPipelineFailure(diagnosticError)
|
|
1441
|
+
return
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
let driverContextResult
|
|
1445
|
+
|
|
1446
|
+
try {
|
|
1447
|
+
driverContextResult = this._deadlockDiagnosticContext(snapshot)
|
|
1448
|
+
} catch (diagnosticError) {
|
|
1449
|
+
this._reportDeadlockDiagnosticPipelineFailure(diagnosticError)
|
|
1450
|
+
return
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const hasPromiseContract = driverContextResult instanceof Promise
|
|
1454
|
+
|
|
1455
|
+
void Promise.resolve(driverContextResult)
|
|
1363
1456
|
.then((driverContext) => {
|
|
1457
|
+
if (!hasPromiseContract) throw new Error("Database deadlock diagnostic context must return a Promise")
|
|
1458
|
+
|
|
1364
1459
|
const context = {
|
|
1365
|
-
|
|
1366
|
-
driverType: this.getType(),
|
|
1367
|
-
maxAttempts,
|
|
1368
|
-
stage: "database-deadlock-retry",
|
|
1369
|
-
willRetry: true,
|
|
1370
|
-
...queryDiagnostic,
|
|
1460
|
+
...snapshot,
|
|
1371
1461
|
...driverContext
|
|
1372
1462
|
}
|
|
1373
1463
|
const payload = {
|
|
1374
1464
|
context,
|
|
1375
|
-
error: new Error(
|
|
1465
|
+
error: new Error(willRetry
|
|
1466
|
+
? `Database transaction ${contentionKind} will be retried`
|
|
1467
|
+
: `Database transaction ${contentionKind} exhausted its retry budget`)
|
|
1376
1468
|
}
|
|
1377
1469
|
const errorEvents = this.configuration.getErrorEvents()
|
|
1378
1470
|
|
|
@@ -1388,30 +1480,93 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1388
1480
|
this.logger.warn("Database deadlock retry all-error listener failed", {error: eventError})
|
|
1389
1481
|
}
|
|
1390
1482
|
})
|
|
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
|
-
}
|
|
1483
|
+
.catch((diagnosticError) => this._reportDeadlockDiagnosticPipelineFailure(diagnosticError))
|
|
1484
|
+
}
|
|
1399
1485
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1486
|
+
/**
|
|
1487
|
+
* Returns pool identity only when this driver was stamped by a pool.
|
|
1488
|
+
* @returns {{databaseIdentifier?: string, databaseIdentifierFingerprint?: string, databaseIdentityFingerprint?: string}} - Safe pool identity.
|
|
1489
|
+
*/
|
|
1490
|
+
_poolDiagnosticIdentityContext() {
|
|
1491
|
+
if (this._databaseIdentifier === undefined || !this._databaseIdentityFingerprint) return {}
|
|
1492
|
+
|
|
1493
|
+
const identifierFingerprintInput = typeof this._databaseIdentifier === "string"
|
|
1494
|
+
? this._databaseIdentifier
|
|
1495
|
+
: `invalid:${typeof this._databaseIdentifier}`
|
|
1496
|
+
const databaseIdentifierFingerprint = `sha256:${sha256Hex(`database-logical-identifier:v1\0${identifierFingerprintInput}`)}`
|
|
1497
|
+
|
|
1498
|
+
return {
|
|
1499
|
+
databaseIdentifier: REDACTED_DIAGNOSTIC_LABEL,
|
|
1500
|
+
databaseIdentifierFingerprint,
|
|
1501
|
+
databaseIdentityFingerprint: this._databaseIdentityFingerprint
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Builds the bounded operation portion of an immutable retry snapshot.
|
|
1507
|
+
* @returns {{operationName?: string, operationNameFingerprint?: string}} - Safe operation fields.
|
|
1508
|
+
*/
|
|
1509
|
+
_operationDiagnosticContext() {
|
|
1510
|
+
const rawOperationName = this._connectionCheckoutName
|
|
1511
|
+
|
|
1512
|
+
if (rawOperationName === undefined) return {}
|
|
1513
|
+
if (typeof rawOperationName !== "string") {
|
|
1514
|
+
return {
|
|
1515
|
+
operationName: REDACTED_DIAGNOSTIC_LABEL,
|
|
1516
|
+
operationNameFingerprint: `sha256:${sha256Hex(`database-operation:v1\0invalid:${typeof rawOperationName}`)}`
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const scannedOperationName = rawOperationName.slice(0, OPERATION_NAME_SCAN_LIMIT)
|
|
1521
|
+
const operationNameFingerprint = `sha256:${sha256Hex(`database-operation:v1\0${scannedOperationName}\0length:${rawOperationName.length}`)}`
|
|
1522
|
+
|
|
1523
|
+
return {
|
|
1524
|
+
operationName: REDACTED_DIAGNOSTIC_LABEL,
|
|
1525
|
+
operationNameFingerprint
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Reports an unexpected detached diagnostics failure without changing transaction control flow.
|
|
1531
|
+
* @param {ReturnType<typeof JSON.parse>} diagnosticError - Diagnostics failure.
|
|
1532
|
+
* @returns {void}
|
|
1533
|
+
*/
|
|
1534
|
+
_reportDeadlockDiagnosticPipelineFailure(diagnosticError) {
|
|
1535
|
+
const normalizedError = diagnosticError instanceof Error
|
|
1536
|
+
? diagnosticError
|
|
1537
|
+
: new Error("Database deadlock retry diagnostic failed", {cause: diagnosticError})
|
|
1538
|
+
const payload = {
|
|
1539
|
+
context: {stage: "database-deadlock-retry-diagnostic"},
|
|
1540
|
+
error: normalizedError
|
|
1541
|
+
}
|
|
1542
|
+
let errorEvents
|
|
1543
|
+
|
|
1544
|
+
try {
|
|
1545
|
+
errorEvents = this.configuration.getErrorEvents()
|
|
1546
|
+
} catch (reportingError) {
|
|
1547
|
+
this.logger.warn("Database deadlock retry diagnostic pipeline reporting failed", {error: normalizedError, reportingError})
|
|
1548
|
+
return
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
try {
|
|
1552
|
+
errorEvents.emit("framework-error", payload)
|
|
1553
|
+
} catch (reportingError) {
|
|
1554
|
+
this.logger.warn("Database deadlock retry framework-error listener failed", {error: normalizedError, reportingError})
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
try {
|
|
1558
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
1559
|
+
} catch (reportingError) {
|
|
1560
|
+
this.logger.warn("Database deadlock retry all-error listener failed", {error: normalizedError, reportingError})
|
|
1561
|
+
}
|
|
1408
1562
|
}
|
|
1409
1563
|
|
|
1410
1564
|
/**
|
|
1411
1565
|
* Builds driver-specific deadlock context. The base driver has no server diagnostic source.
|
|
1566
|
+
* @param {DeadlockRetryDiagnosticSnapshot} _snapshot - Immutable retry snapshot.
|
|
1412
1567
|
* @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>>} - Safe context fields.
|
|
1413
1568
|
*/
|
|
1414
|
-
async _deadlockDiagnosticContext() {
|
|
1569
|
+
async _deadlockDiagnosticContext(_snapshot) {
|
|
1415
1570
|
return {}
|
|
1416
1571
|
}
|
|
1417
1572
|
|
|
@@ -1716,10 +1871,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1716
1871
|
} catch (error) {
|
|
1717
1872
|
if (!(error instanceof Error)) throw error
|
|
1718
1873
|
|
|
1719
|
-
this._failedQueryDiagnostics.set(error,
|
|
1720
|
-
sqlFingerprint: sqlFingerprint(sql),
|
|
1721
|
-
sqlOperation: sql.trim().split(/\s+/, 1)[0]?.toUpperCase() || "UNKNOWN"
|
|
1722
|
-
})
|
|
1874
|
+
this._failedQueryDiagnostics.set(error, sqlDiagnostic(sql))
|
|
1723
1875
|
|
|
1724
1876
|
// A deliberately-aborted query must never be silently re-run — its
|
|
1725
1877
|
// connection was destroyed on purpose, so treat it as terminal.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* InnodbDeadlockLockNode type.
|
|
5
|
+
* @typedef {object} InnodbDeadlockLockNode
|
|
6
|
+
* @property {string} indexFingerprint - Opaque index identity.
|
|
7
|
+
* @property {string} lockMode - Allowlisted lock mode.
|
|
8
|
+
* @property {"conflicting" | "held" | "waiting"} state - Lock relationship to its transaction.
|
|
9
|
+
* @property {string} tableFingerprint - Opaque table identity.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* InnodbDeadlockTransactionNode type.
|
|
14
|
+
* @typedef {object} InnodbDeadlockTransactionNode
|
|
15
|
+
* @property {InnodbDeadlockLockNode[]} conflictingLocks - Bounded counterparty conflict edges whose owner is unavailable.
|
|
16
|
+
* @property {InnodbDeadlockLockNode[]} locks - Bounded locks owned or awaited by this transaction.
|
|
17
|
+
* @property {number} ordinal - Report-local transaction ordinal.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* InnodbDeadlockParserState type.
|
|
22
|
+
* @typedef {object} InnodbDeadlockParserState
|
|
23
|
+
* @property {InnodbDeadlockTransactionNode | undefined} currentTransaction - Current bounded transaction node.
|
|
24
|
+
* @property {"conflicting" | "held" | "waiting" | undefined} currentLockState - Current lock section state.
|
|
25
|
+
* @property {number} lockRecordCount - Total emitted lock nodes.
|
|
26
|
+
* @property {boolean} lockRecordsTruncated - Whether a lock-node bound was reached.
|
|
27
|
+
* @property {InnodbDeadlockTransactionNode[]} transactionNodes - Bounded transaction nodes.
|
|
28
|
+
* @property {boolean} transactionNodesTruncated - Whether the transaction-node bound was reached.
|
|
29
|
+
* @property {number} transactions - Transaction headers observed in the bounded section.
|
|
30
|
+
* @property {number | null} victimTransaction - Victim ordinal.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import sha256Hex from "../../../utils/sha256-hex.js"
|
|
34
|
+
|
|
35
|
+
const INNODB_STATUS_SCAN_MAX_CHARS = 65536
|
|
36
|
+
const INNODB_DEADLOCK_SECTION_MAX_CHARS = 16384
|
|
37
|
+
const INNODB_DEADLOCK_TRANSACTION_MAX = 8
|
|
38
|
+
const INNODB_DEADLOCK_LOCKS_PER_TRANSACTION_MAX = 8
|
|
39
|
+
const INNODB_DEADLOCK_LOCK_RECORD_MAX = 32
|
|
40
|
+
const INNODB_DEADLOCK_LINE_MAX_CHARS = 1024
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parses a bounded InnoDB latest-deadlock section into safe structural context.
|
|
44
|
+
* @param {string} status - SHOW ENGINE INNODB STATUS text.
|
|
45
|
+
* @returns {{lockRecordsTruncated: boolean, sectionTruncated: boolean, transactionNodes: InnodbDeadlockTransactionNode[], transactionNodesTruncated: boolean, transactions: number, victimTransaction: number | null}} - Structural deadlock summary.
|
|
46
|
+
*/
|
|
47
|
+
export default function parseInnodbDeadlockSummary(status) {
|
|
48
|
+
const {candidate, sectionTruncated} = boundedDeadlockSection(status)
|
|
49
|
+
/** @type {InnodbDeadlockParserState} */
|
|
50
|
+
const state = {
|
|
51
|
+
currentTransaction: undefined,
|
|
52
|
+
currentLockState: undefined,
|
|
53
|
+
lockRecordCount: 0,
|
|
54
|
+
lockRecordsTruncated: false,
|
|
55
|
+
transactionNodes: [],
|
|
56
|
+
transactionNodesTruncated: false,
|
|
57
|
+
transactions: 0,
|
|
58
|
+
victimTransaction: null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const line of candidate.split(/\r?\n/)) {
|
|
62
|
+
const trimmed = line.slice(0, INNODB_DEADLOCK_LINE_MAX_CHARS).trim()
|
|
63
|
+
const transactionOrdinal = transactionHeaderOrdinal(trimmed)
|
|
64
|
+
|
|
65
|
+
if (transactionOrdinal !== undefined) {
|
|
66
|
+
startTransactionNode(state, transactionOrdinal)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lockStateMarker = transactionLockStateMarker(trimmed, state.currentTransaction)
|
|
71
|
+
|
|
72
|
+
if (lockStateMarker.matched) {
|
|
73
|
+
state.currentLockState = lockStateMarker.state
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const victimOrdinal = victimTransactionOrdinal(trimmed)
|
|
78
|
+
|
|
79
|
+
if (victimOrdinal !== undefined) state.victimTransaction = victimOrdinal
|
|
80
|
+
appendLockNode(state, trimmed)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
lockRecordsTruncated: state.lockRecordsTruncated,
|
|
85
|
+
sectionTruncated,
|
|
86
|
+
transactionNodes: state.transactionNodes,
|
|
87
|
+
transactionNodesTruncated: state.transactionNodesTruncated,
|
|
88
|
+
transactions: state.transactions,
|
|
89
|
+
victimTransaction: state.victimTransaction
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extracts and caps the latest-deadlock section.
|
|
95
|
+
* @param {string} status - Raw server status.
|
|
96
|
+
* @returns {{candidate: string, sectionTruncated: boolean}} - Bounded candidate and truncation state.
|
|
97
|
+
*/
|
|
98
|
+
function boundedDeadlockSection(status) {
|
|
99
|
+
const scannedStatus = status.slice(0, INNODB_STATUS_SCAN_MAX_CHARS)
|
|
100
|
+
const deadlockStart = scannedStatus.indexOf("LATEST DETECTED DEADLOCK")
|
|
101
|
+
const availableSection = deadlockStart >= 0 ? scannedStatus.slice(deadlockStart) : ""
|
|
102
|
+
const boundedSection = availableSection.slice(0, INNODB_DEADLOCK_SECTION_MAX_CHARS)
|
|
103
|
+
const sectionEndMatch = /\n-{10,}\r?\nTRANSACTIONS\r?\n-{10,}/.exec(boundedSection)
|
|
104
|
+
const scannedStatusTruncated = status.length > scannedStatus.length
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
candidate: sectionEndMatch ? boundedSection.slice(0, sectionEndMatch.index) : boundedSection,
|
|
108
|
+
sectionTruncated: !sectionEndMatch && deadlockStart >= 0 && (
|
|
109
|
+
availableSection.length > boundedSection.length || scannedStatusTruncated
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns a fixed-format transaction header ordinal.
|
|
116
|
+
* @param {string} line - Bounded status line.
|
|
117
|
+
* @returns {number | undefined} - Transaction ordinal.
|
|
118
|
+
*/
|
|
119
|
+
function transactionHeaderOrdinal(line) {
|
|
120
|
+
const match = /^\*\*\* \((\d{1,6})\) TRANSACTION:$/.exec(line)
|
|
121
|
+
|
|
122
|
+
return match ? Number(match[1]) : undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Starts a bounded transaction node.
|
|
127
|
+
* @param {InnodbDeadlockParserState} state - Parser state.
|
|
128
|
+
* @param {number} ordinal - Transaction ordinal.
|
|
129
|
+
* @returns {void}
|
|
130
|
+
*/
|
|
131
|
+
function startTransactionNode(state, ordinal) {
|
|
132
|
+
state.transactions++
|
|
133
|
+
state.currentLockState = undefined
|
|
134
|
+
|
|
135
|
+
if (state.transactionNodes.length >= INNODB_DEADLOCK_TRANSACTION_MAX) {
|
|
136
|
+
state.currentTransaction = undefined
|
|
137
|
+
state.transactionNodesTruncated = true
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
state.currentTransaction = {conflictingLocks: [], locks: [], ordinal}
|
|
142
|
+
state.transactionNodes.push(state.currentTransaction)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parses a fixed-format held/waiting marker for the current transaction.
|
|
147
|
+
* @param {string} line - Bounded status line.
|
|
148
|
+
* @param {InnodbDeadlockTransactionNode | undefined} currentTransaction - Current transaction.
|
|
149
|
+
* @returns {{matched: boolean, state: "conflicting" | "held" | "waiting" | undefined}} - Marker result.
|
|
150
|
+
*/
|
|
151
|
+
function transactionLockStateMarker(line, currentTransaction) {
|
|
152
|
+
const numberedMatch = /^\*\*\* \((\d{1,6})\) (HOLDS THE LOCK\(S\)|WAITING FOR THIS LOCK TO BE GRANTED):$/.exec(line)
|
|
153
|
+
|
|
154
|
+
if (numberedMatch) {
|
|
155
|
+
if (!currentTransaction || currentTransaction.ordinal != Number(numberedMatch[1])) return {matched: true, state: undefined}
|
|
156
|
+
|
|
157
|
+
return {matched: true, state: numberedMatch[2] == "HOLDS THE LOCK(S)" ? "held" : "waiting"}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const unnumberedMatch = /^\*\*\* (WAITING FOR THIS LOCK TO BE GRANTED|CONFLICTING WITH):$/.exec(line)
|
|
161
|
+
|
|
162
|
+
if (!unnumberedMatch) return {matched: false, state: undefined}
|
|
163
|
+
if (!currentTransaction) return {matched: true, state: undefined}
|
|
164
|
+
|
|
165
|
+
return {matched: true, state: unnumberedMatch[1] == "CONFLICTING WITH" ? "conflicting" : "waiting"}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns a fixed-format victim ordinal.
|
|
170
|
+
* @param {string} line - Bounded status line.
|
|
171
|
+
* @returns {number | undefined} - Victim ordinal.
|
|
172
|
+
*/
|
|
173
|
+
function victimTransactionOrdinal(line) {
|
|
174
|
+
const match = /^\*\*\* WE ROLL BACK TRANSACTION \((\d{1,6})\)$/.exec(line)
|
|
175
|
+
|
|
176
|
+
return match ? Number(match[1]) : undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Appends one bounded, fixed-format lock node when the line is eligible.
|
|
181
|
+
* @param {InnodbDeadlockParserState} state - Parser state.
|
|
182
|
+
* @param {string} line - Bounded status line.
|
|
183
|
+
* @returns {void}
|
|
184
|
+
*/
|
|
185
|
+
function appendLockNode(state, line) {
|
|
186
|
+
if (!state.currentTransaction || !state.currentLockState || !line.startsWith("RECORD LOCKS ")) return
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
state.lockRecordCount >= INNODB_DEADLOCK_LOCK_RECORD_MAX ||
|
|
190
|
+
state.currentTransaction.locks.length + state.currentTransaction.conflictingLocks.length >= INNODB_DEADLOCK_LOCKS_PER_TRANSACTION_MAX
|
|
191
|
+
) {
|
|
192
|
+
state.lockRecordsTruncated = true
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const lock = deadlockLockNode(line, state.currentLockState)
|
|
197
|
+
|
|
198
|
+
if (!lock) return
|
|
199
|
+
|
|
200
|
+
if (state.currentLockState == "conflicting") {
|
|
201
|
+
state.currentTransaction.conflictingLocks.push(lock)
|
|
202
|
+
} else {
|
|
203
|
+
state.currentTransaction.locks.push(lock)
|
|
204
|
+
}
|
|
205
|
+
state.lockRecordCount++
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parses one fixed-format RECORD LOCKS line into safe structural fields.
|
|
210
|
+
* @param {string} line - One bounded InnoDB status line.
|
|
211
|
+
* @param {"conflicting" | "held" | "waiting"} state - Lock relationship to its transaction.
|
|
212
|
+
* @returns {InnodbDeadlockLockNode | undefined} - Safe lock node.
|
|
213
|
+
*/
|
|
214
|
+
function deadlockLockNode(line, state) {
|
|
215
|
+
const identifier = "(?:`(?:``|[^`\\r\\n]){1,128}`|[A-Za-z0-9_$-]{1,128})"
|
|
216
|
+
const tableIdentifier = `(?:${identifier}\\.)?${identifier}`
|
|
217
|
+
const lockMatch = new RegExp(`^RECORD LOCKS .{1,512}? index (${identifier}) of table (${tableIdentifier}) trx id \\S{1,64}(?: \\S{1,64})? lock_mode (X|S|IX|IS|AUTO_INC)(?:\\s|$)`).exec(line)
|
|
218
|
+
|
|
219
|
+
if (!lockMatch) return undefined
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
indexFingerprint: `sha256:${sha256Hex(`innodb-index:v1\0${lockMatch[1]}`)}`,
|
|
223
|
+
lockMode: lockMatch[3],
|
|
224
|
+
state,
|
|
225
|
+
tableFingerprint: `sha256:${sha256Hex(`innodb-table:v1\0${lockMatch[2]}`)}`
|
|
226
|
+
}
|
|
227
|
+
}
|