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
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,7 @@ import Table from "./table.js"
|
|
|
21
21
|
import StructureSql from "./structure-sql.js"
|
|
22
22
|
import Upsert from "./sql/upsert.js"
|
|
23
23
|
import Update from "./sql/update.js"
|
|
24
|
+
import parseInnodbDeadlockSummary from "./deadlock-diagnostic-parser.js"
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Sentinel timeout (in seconds) used as the "block forever" value when a
|
|
@@ -365,14 +366,12 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
365
366
|
// A deadlock or lock-wait-timeout aborts the whole transaction; it must be retried at the
|
|
366
367
|
// transaction level (re-running the callback), not the query level, so flag it as such and
|
|
367
368
|
// keep `retry` false so an in-transaction query does not retry against the dead transaction.
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
) {
|
|
375
|
-
return {retry: false, reconnect: false, deadlock: true, waitMs: 50}
|
|
369
|
+
if (errorCode == "ER_LOCK_DEADLOCK" || message.includes("ER_LOCK_DEADLOCK") || message.includes("Deadlock found")) {
|
|
370
|
+
return {retry: false, reconnect: false, deadlock: true, contentionKind: "deadlock", waitMs: 50}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (errorCode == "ER_LOCK_WAIT_TIMEOUT" || message.includes("Lock wait timeout exceeded")) {
|
|
374
|
+
return {retry: false, reconnect: false, deadlock: true, contentionKind: "lock-wait-timeout", waitMs: 50}
|
|
376
375
|
}
|
|
377
376
|
|
|
378
377
|
shouldReconnect ||= (
|
|
@@ -397,19 +396,24 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
397
396
|
* Adds a redacted, bounded excerpt from MySQL's latest InnoDB deadlock report. Capture uses a
|
|
398
397
|
* separate short-lived connection so it cannot queue ahead of rollback or the next retry on this
|
|
399
398
|
* driver's single-connection pool.
|
|
399
|
+
* @param {import("../base.js").DeadlockRetryDiagnosticSnapshot} snapshot - Immutable retry snapshot.
|
|
400
400
|
* @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>>} - Safe diagnostic context.
|
|
401
401
|
*/
|
|
402
|
-
async _deadlockDiagnosticContext() {
|
|
403
|
-
|
|
404
|
-
const status = await this._captureInnodbDeadlockStatus()
|
|
402
|
+
async _deadlockDiagnosticContext(snapshot) {
|
|
403
|
+
if (snapshot.contentionKind == "lock-wait-timeout") return {statusCapture: "not-applicable"}
|
|
405
404
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
405
|
+
let status
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
status = await this._captureInnodbDeadlockStatus()
|
|
410
409
|
} catch {
|
|
411
410
|
return {statusCapture: "failed"}
|
|
412
411
|
}
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
innodbDeadlockSummary: this._innodbDeadlockSummary(status),
|
|
415
|
+
statusCapture: "captured"
|
|
416
|
+
}
|
|
413
417
|
}
|
|
414
418
|
|
|
415
419
|
/**
|
|
@@ -465,23 +469,10 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
465
469
|
* Extracts only fixed-format deadlock counters. The server report contains raw SQL, identifiers,
|
|
466
470
|
* and physical record data, so no source text is ever included in an application diagnostic.
|
|
467
471
|
* @param {string} status - SHOW ENGINE INNODB STATUS text.
|
|
468
|
-
* @returns {{transactions: number, victimTransaction: number | null}} - Structural deadlock summary.
|
|
472
|
+
* @returns {{lockRecordsTruncated: boolean, sectionTruncated: boolean, transactionNodes: Array<{conflictingLocks: Array<{indexFingerprint: string, lockMode: string, state: string, tableFingerprint: string}>, locks: Array<{indexFingerprint: string, lockMode: string, state: string, tableFingerprint: string}>, ordinal: number}>, transactionNodesTruncated: boolean, transactions: number, victimTransaction: number | null}} - Structural deadlock summary.
|
|
469
473
|
*/
|
|
470
474
|
_innodbDeadlockSummary(status) {
|
|
471
|
-
|
|
472
|
-
const candidate = deadlockStart >= 0 ? status.slice(deadlockStart) : status
|
|
473
|
-
let transactions = 0
|
|
474
|
-
/** @type {number | null} */
|
|
475
|
-
let victimTransaction = null
|
|
476
|
-
|
|
477
|
-
for (const line of candidate.split(/\r?\n/)) {
|
|
478
|
-
const trimmed = line.trim()
|
|
479
|
-
if (/^\*\*\* \(\d+\) TRANSACTION:$/.test(trimmed)) transactions++
|
|
480
|
-
const victimMatch = /^\*\*\* WE ROLL BACK TRANSACTION \((\d+)\)$/.exec(trimmed)
|
|
481
|
-
if (victimMatch) victimTransaction = Number(victimMatch[1])
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return {transactions, victimTransaction}
|
|
475
|
+
return parseInnodbDeadlockSummary(status)
|
|
485
476
|
}
|
|
486
477
|
|
|
487
478
|
/**
|
|
@@ -527,7 +518,15 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
527
518
|
if (!this.pool) await this.connect()
|
|
528
519
|
if (!this.pool) throw new Error("MySQL pool failed to initialize")
|
|
529
520
|
|
|
530
|
-
|
|
521
|
+
const profileAttempt = this._startProfiledQueryAttempt(sql)
|
|
522
|
+
let failed = true
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
yield* streamQuery(this.pool, sql)
|
|
526
|
+
failed = false
|
|
527
|
+
} finally {
|
|
528
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
529
|
+
}
|
|
531
530
|
}
|
|
532
531
|
|
|
533
532
|
/**
|
|
@@ -567,13 +566,20 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
567
566
|
if (!this.pool) throw new Error("MySQL pool failed to initialize")
|
|
568
567
|
|
|
569
568
|
const pool = this.pool
|
|
569
|
+
const profileAttempt = this._startProfiledQueryAttempt(structureSql)
|
|
570
|
+
let failed = true
|
|
570
571
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
572
|
+
try {
|
|
573
|
+
await new Promise((resolve, reject) => {
|
|
574
|
+
pool.query(structureSql, (error) => {
|
|
575
|
+
if (error) reject(error)
|
|
576
|
+
else resolve(undefined)
|
|
577
|
+
})
|
|
575
578
|
})
|
|
576
|
-
|
|
579
|
+
failed = false
|
|
580
|
+
} finally {
|
|
581
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
582
|
+
}
|
|
577
583
|
|
|
578
584
|
return true
|
|
579
585
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {AsyncLocalStorage} from "async_hooks"
|
|
4
|
-
import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks"
|
|
4
|
+
import BasePool, { POOL_CONFIGURATION_KEY } from "./base.js"
|
|
5
|
+
import { currentTestProfileContext } from "../../testing/test-profile-context.js"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* PendingCheckout type.
|
|
@@ -15,6 +16,7 @@ import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
|
|
|
15
16
|
* @property {number | null} timeoutAt - Timestamp when the checkout will time out, or null when disabled.
|
|
16
17
|
* @property {number | null} timeoutMillis - Milliseconds to wait before rejecting, or null when disabled.
|
|
17
18
|
* @property {ReturnType<typeof setTimeout> | undefined} timeoutTimer - Timer that rejects the pending checkout.
|
|
19
|
+
* @property {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} [testProfileContext] - Async-safe profile attribution captured at enqueue.
|
|
18
20
|
*/
|
|
19
21
|
export const CLOSED_CONNECTION = Symbol("velociousClosedConnection")
|
|
20
22
|
const IDLE_CONNECTION_CHECKED_IN_AT = Symbol("velociousIdleConnectionCheckedInAt")
|
|
@@ -115,10 +117,20 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
115
117
|
|
|
116
118
|
/** Cumulative low-cardinality pool telemetry. */
|
|
117
119
|
telemetry = {
|
|
120
|
+
connectionCreationCount: 0,
|
|
121
|
+
connectionCreationFailureCount: 0,
|
|
122
|
+
connectionCreationMaxMs: 0,
|
|
123
|
+
connectionCreationTotalMs: 0,
|
|
124
|
+
checkoutTimeoutCount: 0,
|
|
118
125
|
checkoutWaitCount: 0,
|
|
119
126
|
checkoutWaitMaxMs: 0,
|
|
120
127
|
checkoutWaitTotalMs: 0,
|
|
121
|
-
|
|
128
|
+
idleReapCount: 0,
|
|
129
|
+
idleReapDisposalCount: 0,
|
|
130
|
+
idleReapFailureCount: 0,
|
|
131
|
+
idleReapMaxMs: 0,
|
|
132
|
+
idleReapTotalMs: 0,
|
|
133
|
+
peakLiveConnections: 0
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
idSeq = 0
|
|
@@ -139,6 +151,58 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
139
151
|
this._withoutCurrentConnectionContext = withoutCurrentConnectionContext
|
|
140
152
|
}
|
|
141
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Returns the pool telemetry clock.
|
|
156
|
+
* @returns {number} - Current time in milliseconds.
|
|
157
|
+
*/
|
|
158
|
+
nowMs() { return Date.now() }
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Records a pool metric in the active async-safe test profile context.
|
|
162
|
+
* @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} context - Captured profile context.
|
|
163
|
+
* @param {"connectionCreation" | "checkoutWait" | "checkoutTimeout" | "idleReap" | "idleReapDisposal" | "peakLiveConnections"} metric - Metric name.
|
|
164
|
+
* @param {{durationMs?: number, failed?: boolean, value?: number}} [values] - Aggregate values.
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
167
|
+
recordTestProfilePoolMetric(context, metric, values = {}) {
|
|
168
|
+
if (!context) return
|
|
169
|
+
|
|
170
|
+
context.profiler.recordPoolMetric(context, this.identifier, metric, values)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Spawns and times a physical connection without retaining its configuration.
|
|
175
|
+
* @param {import("../../configuration-types.js").DatabaseConfigurationType} config - Resolved database configuration.
|
|
176
|
+
* @returns {Promise<import("../drivers/base.js").default>} - Connected driver.
|
|
177
|
+
*/
|
|
178
|
+
async spawnConnectionWithConfiguration(config) {
|
|
179
|
+
const startedAt = this.nowMs()
|
|
180
|
+
const profileContext = currentTestProfileContext(this.configuration)
|
|
181
|
+
let failed = true
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const connection = await super.spawnConnectionWithConfiguration(config)
|
|
185
|
+
|
|
186
|
+
failed = false
|
|
187
|
+
const liveConnectionCount = this.liveConnectionCount() - this.connectionsBeingSpawned + 1
|
|
188
|
+
|
|
189
|
+
if (liveConnectionCount > this.telemetry.peakLiveConnections) {
|
|
190
|
+
this.telemetry.peakLiveConnections = liveConnectionCount
|
|
191
|
+
this.recordTestProfilePoolMetric(profileContext, "peakLiveConnections", {value: liveConnectionCount})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return connection
|
|
195
|
+
} finally {
|
|
196
|
+
const durationMs = Math.max(0, this.nowMs() - startedAt)
|
|
197
|
+
|
|
198
|
+
this.telemetry.connectionCreationCount++
|
|
199
|
+
if (failed) this.telemetry.connectionCreationFailureCount++
|
|
200
|
+
this.telemetry.connectionCreationTotalMs += durationMs
|
|
201
|
+
this.telemetry.connectionCreationMaxMs = Math.max(this.telemetry.connectionCreationMaxMs, durationMs)
|
|
202
|
+
this.recordTestProfilePoolMetric(profileContext, "connectionCreation", {durationMs, failed})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
142
206
|
/**
|
|
143
207
|
* Runs checkin.
|
|
144
208
|
* @param {import("../drivers/base.js").default} connection - Database connection instance.
|
|
@@ -241,24 +305,28 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
241
305
|
* @returns {Promise<import("../drivers/base.js").default>} - Resolves with the checkout.
|
|
242
306
|
*/
|
|
243
307
|
async checkout(options = {}) {
|
|
244
|
-
|
|
245
|
-
|
|
308
|
+
let databaseConfig = this.getConfiguration()
|
|
309
|
+
let reuseKey = this.getConfigurationReuseKey(databaseConfig)
|
|
246
310
|
let connection = this.takeIdleConnectionForReuseKey(reuseKey)
|
|
247
311
|
|
|
248
312
|
if (connection) return await this.activateConnection(connection, options)
|
|
249
313
|
|
|
250
314
|
await this.reapIdleConnections()
|
|
315
|
+
databaseConfig = this.getConfiguration()
|
|
316
|
+
reuseKey = this.getConfigurationReuseKey(databaseConfig)
|
|
251
317
|
connection = this.takeIdleConnectionForReuseKey(reuseKey)
|
|
252
318
|
|
|
253
319
|
if (connection) return await this.activateConnection(connection, options)
|
|
254
320
|
|
|
255
|
-
if (this.canSpawnConnection()) {
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
321
|
+
if (this.canSpawnConnection(databaseConfig)) {
|
|
322
|
+
// The post-reap configuration is fresh for the current caller, and its reuse key is
|
|
323
|
+
// derived from this exact captured object so the connection cannot open one tenant while
|
|
324
|
+
// being stamped for another. The queued path retains the same captured pair.
|
|
325
|
+
connection = await this.spawnConnectionForCheckout(
|
|
326
|
+
databaseConfig,
|
|
327
|
+
reuseKey,
|
|
328
|
+
currentTestProfileContext(this.configuration)
|
|
329
|
+
)
|
|
262
330
|
|
|
263
331
|
return await this.activateConnection(connection, options)
|
|
264
332
|
}
|
|
@@ -291,7 +359,11 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
291
359
|
if (connection) return await this.activateConnection(connection, options)
|
|
292
360
|
|
|
293
361
|
if (this.canSpawnConnection(databaseConfig)) {
|
|
294
|
-
connection = await this.spawnConnectionForCheckout(
|
|
362
|
+
connection = await this.spawnConnectionForCheckout(
|
|
363
|
+
databaseConfig,
|
|
364
|
+
reuseKey,
|
|
365
|
+
currentTestProfileContext(this.configuration)
|
|
366
|
+
)
|
|
295
367
|
|
|
296
368
|
return await this.activateConnection(connection, options)
|
|
297
369
|
}
|
|
@@ -436,21 +508,20 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
436
508
|
* Runs spawn connection for checkout.
|
|
437
509
|
* @param {import("../../configuration-types.js").DatabaseConfigurationType} databaseConfig - Resolved database config for the checkout.
|
|
438
510
|
* @param {string} reuseKey - Database configuration reuse key for the checkout.
|
|
511
|
+
* @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} profileContext - Profile context captured when checkout began.
|
|
439
512
|
* @returns {Promise<import("../drivers/base.js").default>} - Spawned connection.
|
|
440
513
|
*/
|
|
441
|
-
async spawnConnectionForCheckout(databaseConfig, reuseKey) {
|
|
514
|
+
async spawnConnectionForCheckout(databaseConfig, reuseKey, profileContext) {
|
|
442
515
|
this.connectionsBeingSpawned++
|
|
443
516
|
|
|
444
517
|
try {
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
connectionWithPoolKey[POOL_CONFIGURATION_KEY] = reuseKey
|
|
449
|
-
connection.setSchemaCacheInvalidator(() => {
|
|
450
|
-
this.clearSchemaCache()
|
|
451
|
-
this.configuration.clearSchemaCachesForReuseKey(reuseKey)
|
|
518
|
+
const environmentHandler = this.configuration.getEnvironmentHandler()
|
|
519
|
+
const connection = await environmentHandler.runWithTestProfileContext(profileContext, async () => {
|
|
520
|
+
return await this.spawnConnectionWithConfiguration(databaseConfig)
|
|
452
521
|
})
|
|
453
522
|
|
|
523
|
+
this.stampConnectionForConfigurationReuseKey(connection, reuseKey)
|
|
524
|
+
|
|
454
525
|
return connection
|
|
455
526
|
} finally {
|
|
456
527
|
this.connectionsBeingSpawned--
|
|
@@ -478,7 +549,8 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
478
549
|
reuseKey,
|
|
479
550
|
timeoutAt: timeoutMillis === null ? null : enqueuedAt + timeoutMillis,
|
|
480
551
|
timeoutMillis,
|
|
481
|
-
timeoutTimer: undefined
|
|
552
|
+
timeoutTimer: undefined,
|
|
553
|
+
testProfileContext: currentTestProfileContext(this.configuration)
|
|
482
554
|
}
|
|
483
555
|
|
|
484
556
|
checkout.timeoutTimer = this.startPendingCheckoutTimeout(checkout)
|
|
@@ -578,11 +650,12 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
578
650
|
* @returns {void}
|
|
579
651
|
*/
|
|
580
652
|
recordCheckoutWait(checkout) {
|
|
581
|
-
const waitedForMs = Math.max(0,
|
|
653
|
+
const waitedForMs = Math.max(0, this.nowMs() - checkout.enqueuedAt)
|
|
582
654
|
|
|
583
655
|
this.telemetry.checkoutWaitCount++
|
|
584
656
|
this.telemetry.checkoutWaitTotalMs += waitedForMs
|
|
585
657
|
this.telemetry.checkoutWaitMaxMs = Math.max(this.telemetry.checkoutWaitMaxMs, waitedForMs)
|
|
658
|
+
this.recordTestProfilePoolMetric(checkout.testProfileContext, "checkoutWait", {durationMs: waitedForMs})
|
|
586
659
|
}
|
|
587
660
|
|
|
588
661
|
/**
|
|
@@ -611,6 +684,8 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
611
684
|
if (index === -1) return
|
|
612
685
|
|
|
613
686
|
this.removePendingCheckoutAt(index)
|
|
687
|
+
this.telemetry.checkoutTimeoutCount++
|
|
688
|
+
this.recordTestProfilePoolMetric(checkout.testProfileContext, "checkoutTimeout")
|
|
614
689
|
checkout.reject(this.pendingCheckoutTimeoutError(checkout))
|
|
615
690
|
}
|
|
616
691
|
|
|
@@ -744,16 +819,24 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
744
819
|
* @returns {Promise<void>} - Resolves when the checkout has been handled.
|
|
745
820
|
*/
|
|
746
821
|
async spawnAndResolvePendingCheckout(checkout) {
|
|
747
|
-
|
|
822
|
+
const environmentHandler = this.configuration.getEnvironmentHandler()
|
|
748
823
|
|
|
749
|
-
|
|
750
|
-
connection
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
824
|
+
return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
|
|
825
|
+
let connection
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
connection = await this.spawnConnectionForCheckout(
|
|
829
|
+
checkout.databaseConfig,
|
|
830
|
+
checkout.reuseKey,
|
|
831
|
+
checkout.testProfileContext
|
|
832
|
+
)
|
|
833
|
+
} catch (error) {
|
|
834
|
+
checkout.reject(error instanceof Error ? error : new Error("Failed to spawn database connection.", {cause: error}))
|
|
835
|
+
return
|
|
836
|
+
}
|
|
755
837
|
|
|
756
|
-
|
|
838
|
+
await this.resolvePendingCheckout(checkout, connection)
|
|
839
|
+
})
|
|
757
840
|
}
|
|
758
841
|
|
|
759
842
|
/**
|
|
@@ -763,11 +846,15 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
763
846
|
* @returns {Promise<void>} - Resolves when the checkout has been handled.
|
|
764
847
|
*/
|
|
765
848
|
async resolvePendingCheckout(checkout, connection) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
849
|
+
const environmentHandler = this.configuration.getEnvironmentHandler()
|
|
850
|
+
|
|
851
|
+
return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
|
|
852
|
+
try {
|
|
853
|
+
checkout.resolve(await this.activateConnection(connection, checkout.options))
|
|
854
|
+
} catch (error) {
|
|
855
|
+
checkout.reject(error instanceof Error ? error : new Error("Failed to activate database connection.", {cause: error}))
|
|
856
|
+
}
|
|
857
|
+
})
|
|
771
858
|
}
|
|
772
859
|
|
|
773
860
|
/**
|
|
@@ -1313,24 +1400,40 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
1313
1400
|
const idleTimeoutMillis = this.idleTimeoutMillis()
|
|
1314
1401
|
|
|
1315
1402
|
if (idleTimeoutMillis === null) return
|
|
1403
|
+
const startedAt = this.nowMs()
|
|
1404
|
+
const profileContext = currentTestProfileContext(this.configuration)
|
|
1405
|
+
let failed = true
|
|
1406
|
+
|
|
1407
|
+
try {
|
|
1408
|
+
const {expiredConnections, keptConnections} = this.classifyIdleConnectionsForReaping({idleTimeoutMillis, now: this.nowMs()})
|
|
1316
1409
|
|
|
1317
|
-
|
|
1410
|
+
this.connections = keptConnections
|
|
1411
|
+
await this.closeExpiredIdleConnections(expiredConnections, profileContext)
|
|
1412
|
+
await this.awaitInflightConnectionCloses()
|
|
1413
|
+
if (this.connections.length > 0) this.scheduleIdleConnectionReaper()
|
|
1414
|
+
failed = false
|
|
1415
|
+
} finally {
|
|
1416
|
+
const durationMs = Math.max(0, this.nowMs() - startedAt)
|
|
1318
1417
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1418
|
+
this.telemetry.idleReapCount++
|
|
1419
|
+
if (failed) this.telemetry.idleReapFailureCount++
|
|
1420
|
+
this.telemetry.idleReapTotalMs += durationMs
|
|
1421
|
+
this.telemetry.idleReapMaxMs = Math.max(this.telemetry.idleReapMaxMs, durationMs)
|
|
1422
|
+
this.recordTestProfilePoolMetric(profileContext, "idleReap", {durationMs, failed})
|
|
1423
|
+
}
|
|
1323
1424
|
}
|
|
1324
1425
|
|
|
1325
1426
|
/**
|
|
1326
1427
|
* Runs close expired idle connections.
|
|
1327
1428
|
* @param {import("../drivers/base.js").default[]} expiredConnections - Connections to close.
|
|
1429
|
+
* @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} [profileContext] - Reaper profile context.
|
|
1328
1430
|
* @returns {Promise<void>} - Resolves when closed.
|
|
1329
1431
|
*/
|
|
1330
|
-
async closeExpiredIdleConnections(expiredConnections) {
|
|
1432
|
+
async closeExpiredIdleConnections(expiredConnections, profileContext) {
|
|
1331
1433
|
for (const connection of expiredConnections) {
|
|
1332
1434
|
await this.closeConnection(connection)
|
|
1333
1435
|
this.telemetry.idleReapDisposalCount++
|
|
1436
|
+
this.recordTestProfilePoolMetric(profileContext, "idleReapDisposal")
|
|
1334
1437
|
}
|
|
1335
1438
|
}
|
|
1336
1439
|
|