ddlforge 0.1.1 → 0.2.0
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 +519 -198
- package/dist/src/cli.d.ts +25 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +253 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/engine/analyzer.d.ts.map +1 -1
- package/dist/src/engine/analyzer.js.map +1 -1
- package/dist/src/lexer/sqlTokenizer.d.ts.map +1 -1
- package/dist/src/lexer/sqlTokenizer.js.map +1 -1
- package/dist/src/reporters/json.js.map +1 -1
- package/dist/src/reporters/terminal.js.map +1 -1
- package/dist/src/runner/backoff.d.ts +53 -0
- package/dist/src/runner/backoff.d.ts.map +1 -0
- package/dist/src/runner/backoff.js +65 -0
- package/dist/src/runner/backoff.js.map +1 -0
- package/dist/src/runner/executor.d.ts +83 -0
- package/dist/src/runner/executor.d.ts.map +1 -0
- package/dist/src/runner/executor.js +340 -0
- package/dist/src/runner/executor.js.map +1 -0
- package/dist/src/runner/locksMonitor.d.ts +85 -0
- package/dist/src/runner/locksMonitor.d.ts.map +1 -0
- package/dist/src/runner/locksMonitor.js +183 -0
- package/dist/src/runner/locksMonitor.js.map +1 -0
- package/package.json +7 -3
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - Exponential backoff with full jitter for lock-timeout retry loops.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm: sleep = Math.random() * Math.min(maxDelay, baseDelay * 2 ** attempt)
|
|
5
|
+
* This prevents lock-convoy / stampede effects by randomising each retry interval.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Computes a full-jitter exponential backoff interval for the given attempt index.
|
|
9
|
+
*
|
|
10
|
+
* @param attempt 0-based attempt index (first retry = 0)
|
|
11
|
+
* @param options Backoff configuration
|
|
12
|
+
* @returns Randomised sleep duration in milliseconds
|
|
13
|
+
*/
|
|
14
|
+
export function computeBackoff(attempt, options = {}) {
|
|
15
|
+
const baseDelayMs = options.baseDelayMs ?? 250;
|
|
16
|
+
const maxDelayMs = options.maxDelayMs ?? 10_000;
|
|
17
|
+
const maxRetries = options.maxRetries ?? 5;
|
|
18
|
+
const ceiling = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt));
|
|
19
|
+
const sleepMs = Math.random() * ceiling;
|
|
20
|
+
const exceeded = attempt >= maxRetries;
|
|
21
|
+
return { sleepMs, attempt, exceeded };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns a promise that resolves after `ms` milliseconds.
|
|
25
|
+
* Optionally accepts an AbortSignal: if the signal is aborted before the
|
|
26
|
+
* timer fires, the promise rejects with the signal's reason.
|
|
27
|
+
*
|
|
28
|
+
* @param ms Sleep duration in milliseconds
|
|
29
|
+
* @param signal Optional AbortSignal for cancellation
|
|
30
|
+
*/
|
|
31
|
+
export function sleep(ms, signal) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
if (signal?.aborted) {
|
|
34
|
+
reject(signal.reason ?? new Error('AbortError'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
signal?.removeEventListener('abort', onAbort);
|
|
39
|
+
resolve();
|
|
40
|
+
}, ms);
|
|
41
|
+
function onAbort() {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
reject(this.reason ?? new Error('AbortError'));
|
|
44
|
+
}
|
|
45
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Iterates over retry attempts, yielding the computed backoff for each attempt.
|
|
50
|
+
* Callers should `sleep(backoff.sleepMs)` after receiving each non-first result.
|
|
51
|
+
*
|
|
52
|
+
* Example:
|
|
53
|
+
* ```ts
|
|
54
|
+
* for (const backoff of retryIterator(3, opts)) {
|
|
55
|
+
* if (backoff.attempt > 0) await sleep(backoff.sleepMs, signal);
|
|
56
|
+
* // ... attempt work ...
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function* retryIterator(maxRetries, options = {}) {
|
|
61
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
62
|
+
yield computeBackoff(attempt, { ...options, maxRetries });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../../../src/runner/backoff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,UAA0B,EAAE;IAC1E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,GAAG,CAAC;IAC/C,MAAM,UAAU,GAAI,OAAO,CAAC,UAAU,IAAK,MAAM,CAAC;IAClD,MAAM,UAAU,GAAI,OAAO,CAAC,UAAU,IAAK,CAAC,CAAC;IAE7C,MAAM,OAAO,GAAI,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAI,IAAI,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC;IACzC,MAAM,QAAQ,GAAG,OAAO,IAAI,UAAU,CAAC;IAEvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB;IACpD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QAEP,SAAS,OAAO;YACd,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAE,IAAoB,CAAC,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,SAAS,CAAC,CAAC,aAAa,CAC5B,UAAkB,EAClB,UAA8C,EAAE;IAEhD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,MAAM,cAAc,CAAC,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - PostgreSQL DDL execution supervisor.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities
|
|
5
|
+
* ────────────────
|
|
6
|
+
* 1. Dynamically imports `pg` at runtime (graceful error if not installed).
|
|
7
|
+
* 2. Parses SQL migration files into individual statements (reuses the existing
|
|
8
|
+
* zero-dependency lexer — no duplicate parser code).
|
|
9
|
+
* 3. For each statement:
|
|
10
|
+
* a. Opens a transaction on a fresh client.
|
|
11
|
+
* b. Injects `SET LOCAL lock_timeout` and `SET LOCAL statement_timeout`.
|
|
12
|
+
* c. Executes the statement.
|
|
13
|
+
* d. On error 55P03 (lock_not_available) or 57014 (query_canceled):
|
|
14
|
+
* – Rolls back, waits for a full-jitter backoff interval, retries.
|
|
15
|
+
* e. On avalanche detected by the lock monitor: rolls back and aborts.
|
|
16
|
+
* f. On success: commits and proceeds to the next statement.
|
|
17
|
+
* 4. Reports live progress via an optional EventEmitter-compatible callback.
|
|
18
|
+
*
|
|
19
|
+
* Zero-dependency invariant
|
|
20
|
+
* ─────────────────────────
|
|
21
|
+
* `pg` is imported with `await import('pg')` inside an async function so it
|
|
22
|
+
* never appears in the static import graph. Static linting (`ddlforge check`)
|
|
23
|
+
* works without `pg` installed.
|
|
24
|
+
*/
|
|
25
|
+
export interface ExecutorOptions {
|
|
26
|
+
/** Full PostgreSQL connection string or URL */
|
|
27
|
+
databaseUrl: string;
|
|
28
|
+
/** lock_timeout value (e.g. "2500ms" or "2500"). Default: "3000ms" */
|
|
29
|
+
lockTimeout?: string;
|
|
30
|
+
/** statement_timeout value (e.g. "30000ms"). Default: "30000ms" */
|
|
31
|
+
statementTimeout?: string;
|
|
32
|
+
/** Maximum number of retry attempts per statement on lock errors. Default: 5 */
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
/** If true, parse and validate statements but never execute them. Default: false */
|
|
35
|
+
dryRun?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Number of blocked backends that triggers an avalanche abort.
|
|
38
|
+
* Default: 1 (abort as soon as even one query queues behind ours).
|
|
39
|
+
*/
|
|
40
|
+
lockQueueThreshold?: number;
|
|
41
|
+
/** Milliseconds between lock-monitor polls. Default: 500 */
|
|
42
|
+
monitorPollMs?: number;
|
|
43
|
+
/** Progress callback invoked after each statement attempt */
|
|
44
|
+
onProgress?: (event: ExecutionEvent) => void;
|
|
45
|
+
/** AbortSignal for external cancellation */
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
export type ExecutionEventKind = 'statement-start' | 'statement-success' | 'statement-retry' | 'statement-failed' | 'avalanche-abort' | 'dry-run-statement' | 'migration-complete' | 'migration-failed';
|
|
49
|
+
export interface ExecutionEvent {
|
|
50
|
+
kind: ExecutionEventKind;
|
|
51
|
+
statementIndex: number;
|
|
52
|
+
totalStatements: number;
|
|
53
|
+
statementSql: string;
|
|
54
|
+
attempt: number;
|
|
55
|
+
elapsedMs: number;
|
|
56
|
+
error?: string;
|
|
57
|
+
lockTimeoutMs?: number;
|
|
58
|
+
retryBackoffMs?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface ExecutionResult {
|
|
61
|
+
success: boolean;
|
|
62
|
+
statementsExecuted: number;
|
|
63
|
+
statementsTotal: number;
|
|
64
|
+
durationMs: number;
|
|
65
|
+
error?: string;
|
|
66
|
+
avalanche?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Executes a SQL migration file against a PostgreSQL database with:
|
|
70
|
+
* - per-statement lock-timeout injection
|
|
71
|
+
* - exponential full-jitter retry on lock acquisition failures
|
|
72
|
+
* - background lock-queue monitoring with automatic cancellation
|
|
73
|
+
*
|
|
74
|
+
* @param sql Full text of the migration file
|
|
75
|
+
* @param options Execution configuration
|
|
76
|
+
*/
|
|
77
|
+
export declare function executeMigration(sql: string, options: ExecutorOptions): Promise<ExecutionResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Parses a SQL migration string and returns the list of individual statements
|
|
80
|
+
* that would be executed. Useful for tooling and dry-run previews.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseMigrationStatements(sql: string): string[];
|
|
83
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/runner/executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAUH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,4CAA4C;IAC5C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,MAAM,kBAAkB,GAC1B,iBAAiB,GACjB,mBAAmB,GACnB,iBAAiB,GACjB,kBAAkB,GAClB,iBAAiB,GACjB,mBAAmB,GACnB,oBAAoB,GACpB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAuGD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,eAAe,CAAC,CAmQ1B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9D"}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - PostgreSQL DDL execution supervisor.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities
|
|
5
|
+
* ────────────────
|
|
6
|
+
* 1. Dynamically imports `pg` at runtime (graceful error if not installed).
|
|
7
|
+
* 2. Parses SQL migration files into individual statements (reuses the existing
|
|
8
|
+
* zero-dependency lexer — no duplicate parser code).
|
|
9
|
+
* 3. For each statement:
|
|
10
|
+
* a. Opens a transaction on a fresh client.
|
|
11
|
+
* b. Injects `SET LOCAL lock_timeout` and `SET LOCAL statement_timeout`.
|
|
12
|
+
* c. Executes the statement.
|
|
13
|
+
* d. On error 55P03 (lock_not_available) or 57014 (query_canceled):
|
|
14
|
+
* – Rolls back, waits for a full-jitter backoff interval, retries.
|
|
15
|
+
* e. On avalanche detected by the lock monitor: rolls back and aborts.
|
|
16
|
+
* f. On success: commits and proceeds to the next statement.
|
|
17
|
+
* 4. Reports live progress via an optional EventEmitter-compatible callback.
|
|
18
|
+
*
|
|
19
|
+
* Zero-dependency invariant
|
|
20
|
+
* ─────────────────────────
|
|
21
|
+
* `pg` is imported with `await import('pg')` inside an async function so it
|
|
22
|
+
* never appears in the static import graph. Static linting (`ddlforge check`)
|
|
23
|
+
* works without `pg` installed.
|
|
24
|
+
*/
|
|
25
|
+
import { splitStatements } from '../lexer/sqlTokenizer.js';
|
|
26
|
+
import { sleep, computeBackoff } from './backoff.js';
|
|
27
|
+
import { startLockMonitor, isLockError } from './locksMonitor.js';
|
|
28
|
+
/**
|
|
29
|
+
* Dynamically imports pg and returns the Pool and Client constructors.
|
|
30
|
+
* If pg is not installed, prints clear installation instructions and throws.
|
|
31
|
+
*/
|
|
32
|
+
async function loadPg() {
|
|
33
|
+
try {
|
|
34
|
+
// Dynamic import: NOT in static import graph — ddlforge check still works
|
|
35
|
+
const mod = await import('pg');
|
|
36
|
+
return mod.default;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
const msg = [
|
|
40
|
+
'',
|
|
41
|
+
' ╔══════════════════════════════════════════════════════════╗',
|
|
42
|
+
' ║ ddlforge apply requires the "pg" package to be ║',
|
|
43
|
+
' ║ installed in your project. ║',
|
|
44
|
+
' ║ ║',
|
|
45
|
+
' ║ Run one of: ║',
|
|
46
|
+
' ║ npm install pg ║',
|
|
47
|
+
' ║ yarn add pg ║',
|
|
48
|
+
' ║ pnpm add pg ║',
|
|
49
|
+
' ╚══════════════════════════════════════════════════════════╝',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n');
|
|
52
|
+
process.stderr.write(msg + '\n');
|
|
53
|
+
throw new Error('pg package is not installed. Run: npm install pg');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
/* Helpers */
|
|
58
|
+
/* ------------------------------------------------------------------ */
|
|
59
|
+
/**
|
|
60
|
+
* Normalises a timeout value into a bare integer millisecond string
|
|
61
|
+
* suitable for `SET LOCAL lock_timeout = '<value>'`.
|
|
62
|
+
*
|
|
63
|
+
* Accepts:
|
|
64
|
+
* - "3000ms" → "3000"
|
|
65
|
+
* - "3s" → "3000" (NOT handled — pass pre-converted ms values)
|
|
66
|
+
* - "3000" → "3000"
|
|
67
|
+
* - 3000 → "3000"
|
|
68
|
+
*/
|
|
69
|
+
function normaliseTimeoutMs(value) {
|
|
70
|
+
const s = String(value).trim();
|
|
71
|
+
if (s.endsWith('ms')) {
|
|
72
|
+
return s.slice(0, -2);
|
|
73
|
+
}
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns the `code` from a Postgres-driver error object.
|
|
78
|
+
* pg errors expose `.code` as a top-level property.
|
|
79
|
+
*/
|
|
80
|
+
function pgErrorCode(err) {
|
|
81
|
+
if (err !== null && typeof err === 'object' && 'code' in err) {
|
|
82
|
+
return String(err['code'] ?? '');
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
/* ------------------------------------------------------------------ */
|
|
87
|
+
/* Statement text reconstruction */
|
|
88
|
+
/* ------------------------------------------------------------------ */
|
|
89
|
+
/**
|
|
90
|
+
* Extracts the original SQL text for a statement from the migration file.
|
|
91
|
+
* We reconstruct it from the token raw values so that whitespace is
|
|
92
|
+
* preserved reasonably well for display purposes.
|
|
93
|
+
*/
|
|
94
|
+
function statementText(raw) {
|
|
95
|
+
// The lexer joins tokens with single spaces; we just use the raw field.
|
|
96
|
+
return raw.trimEnd() + ';';
|
|
97
|
+
}
|
|
98
|
+
/* ------------------------------------------------------------------ */
|
|
99
|
+
/* Main executor */
|
|
100
|
+
/* ------------------------------------------------------------------ */
|
|
101
|
+
/**
|
|
102
|
+
* Executes a SQL migration file against a PostgreSQL database with:
|
|
103
|
+
* - per-statement lock-timeout injection
|
|
104
|
+
* - exponential full-jitter retry on lock acquisition failures
|
|
105
|
+
* - background lock-queue monitoring with automatic cancellation
|
|
106
|
+
*
|
|
107
|
+
* @param sql Full text of the migration file
|
|
108
|
+
* @param options Execution configuration
|
|
109
|
+
*/
|
|
110
|
+
export async function executeMigration(sql, options) {
|
|
111
|
+
const { databaseUrl, lockTimeout = '3000ms', statementTimeout = '30000ms', maxRetries = 5, dryRun = false, lockQueueThreshold = 1, monitorPollMs = 500, onProgress, signal, } = options;
|
|
112
|
+
const lockTimeoutMs = normaliseTimeoutMs(lockTimeout);
|
|
113
|
+
const statementTimeoutMs = normaliseTimeoutMs(statementTimeout);
|
|
114
|
+
const migrationStart = Date.now();
|
|
115
|
+
// ── Parse statements (zero-dependency lexer) ────────────────────────
|
|
116
|
+
const statements = splitStatements(sql);
|
|
117
|
+
const total = statements.length;
|
|
118
|
+
if (total === 0) {
|
|
119
|
+
return { success: true, statementsExecuted: 0, statementsTotal: 0, durationMs: 0 };
|
|
120
|
+
}
|
|
121
|
+
// ── Dry run mode ─────────────────────────────────────────────────────
|
|
122
|
+
if (dryRun) {
|
|
123
|
+
for (let i = 0; i < statements.length; i++) {
|
|
124
|
+
const stmt = statements[i];
|
|
125
|
+
onProgress?.({
|
|
126
|
+
kind: 'dry-run-statement',
|
|
127
|
+
statementIndex: i,
|
|
128
|
+
totalStatements: total,
|
|
129
|
+
statementSql: statementText(stmt.raw),
|
|
130
|
+
attempt: 0,
|
|
131
|
+
elapsedMs: Date.now() - migrationStart,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
statementsExecuted: 0,
|
|
137
|
+
statementsTotal: total,
|
|
138
|
+
durationMs: Date.now() - migrationStart,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// ── Load pg ──────────────────────────────────────────────────────────
|
|
142
|
+
const { Pool, Client } = await loadPg();
|
|
143
|
+
// One pool for the migration; monitor gets its own direct clients
|
|
144
|
+
const pool = new Pool({ connectionString: databaseUrl, max: 1 });
|
|
145
|
+
let statementsExecuted = 0;
|
|
146
|
+
let globalError;
|
|
147
|
+
let avalanche = false;
|
|
148
|
+
try {
|
|
149
|
+
for (let i = 0; i < statements.length; i++) {
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
globalError = 'Execution aborted by caller signal.';
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
const stmt = statements[i];
|
|
155
|
+
const stmtSql = statementText(stmt.raw);
|
|
156
|
+
const stmtStart = Date.now();
|
|
157
|
+
let succeeded = false;
|
|
158
|
+
let stmtError;
|
|
159
|
+
onProgress?.({
|
|
160
|
+
kind: 'statement-start',
|
|
161
|
+
statementIndex: i,
|
|
162
|
+
totalStatements: total,
|
|
163
|
+
statementSql: stmtSql,
|
|
164
|
+
attempt: 0,
|
|
165
|
+
elapsedMs: stmtStart - migrationStart,
|
|
166
|
+
lockTimeoutMs: Number(lockTimeoutMs),
|
|
167
|
+
});
|
|
168
|
+
// ── Retry loop per statement ──────────────────────────────────────
|
|
169
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
170
|
+
if (signal?.aborted)
|
|
171
|
+
break;
|
|
172
|
+
// Jitter-sleep before all retries (not before first attempt)
|
|
173
|
+
if (attempt > 0) {
|
|
174
|
+
const backoff = computeBackoff(attempt - 1, {
|
|
175
|
+
baseDelayMs: 250,
|
|
176
|
+
maxDelayMs: 10_000,
|
|
177
|
+
maxRetries,
|
|
178
|
+
});
|
|
179
|
+
onProgress?.({
|
|
180
|
+
kind: 'statement-retry',
|
|
181
|
+
statementIndex: i,
|
|
182
|
+
totalStatements: total,
|
|
183
|
+
statementSql: stmtSql,
|
|
184
|
+
attempt,
|
|
185
|
+
elapsedMs: Date.now() - migrationStart,
|
|
186
|
+
retryBackoffMs: backoff.sleepMs,
|
|
187
|
+
});
|
|
188
|
+
await sleep(backoff.sleepMs, signal);
|
|
189
|
+
if (signal?.aborted)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// Grab a client from the pool for this attempt
|
|
193
|
+
const client = await pool.connect();
|
|
194
|
+
// Snapshot our backend PID for the lock monitor
|
|
195
|
+
let migrationPid = -1;
|
|
196
|
+
try {
|
|
197
|
+
const pidResult = await client.query('SELECT pg_backend_pid() AS pid');
|
|
198
|
+
migrationPid = Number(pidResult.rows[0]?.['pid'] ?? -1);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Fallback: proceed without monitoring
|
|
202
|
+
}
|
|
203
|
+
// Start the lock monitor on its own independent connection
|
|
204
|
+
const monitorClientFactory = async () => {
|
|
205
|
+
const monClient = new Client({ connectionString: databaseUrl });
|
|
206
|
+
await monClient.connect?.();
|
|
207
|
+
return monClient;
|
|
208
|
+
};
|
|
209
|
+
const monitor = migrationPid > 0
|
|
210
|
+
? startLockMonitor({
|
|
211
|
+
migrationPid,
|
|
212
|
+
clientFactory: monitorClientFactory,
|
|
213
|
+
pollIntervalMs: monitorPollMs,
|
|
214
|
+
queueThreshold: lockQueueThreshold,
|
|
215
|
+
signal,
|
|
216
|
+
})
|
|
217
|
+
: null;
|
|
218
|
+
try {
|
|
219
|
+
// Open transaction
|
|
220
|
+
await client.query('BEGIN');
|
|
221
|
+
// Inject session-local timeouts (scoped to this transaction)
|
|
222
|
+
await client.query(`SET LOCAL lock_timeout = '${lockTimeoutMs}'`);
|
|
223
|
+
await client.query(`SET LOCAL statement_timeout = '${statementTimeoutMs}'`);
|
|
224
|
+
// Execute the DDL statement
|
|
225
|
+
await client.query(stmtSql);
|
|
226
|
+
// Commit
|
|
227
|
+
await client.query('COMMIT');
|
|
228
|
+
// Stop monitor — execution succeeded, no avalanche
|
|
229
|
+
monitor?.stop();
|
|
230
|
+
const monResult = await monitor?.result;
|
|
231
|
+
if (monResult?.avalanche) {
|
|
232
|
+
// Race condition: monitor fired just as we committed.
|
|
233
|
+
// The COMMIT succeeded so we treat this as a success.
|
|
234
|
+
// (pg_cancel_backend after COMMIT is a no-op.)
|
|
235
|
+
}
|
|
236
|
+
succeeded = true;
|
|
237
|
+
stmtError = undefined;
|
|
238
|
+
break; // exit retry loop
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
const code = pgErrorCode(err);
|
|
242
|
+
// Stop monitor before doing anything else
|
|
243
|
+
monitor?.stop();
|
|
244
|
+
const monResult = await monitor?.result;
|
|
245
|
+
// Rollback the failed transaction
|
|
246
|
+
try {
|
|
247
|
+
await client.query('ROLLBACK');
|
|
248
|
+
}
|
|
249
|
+
catch { /* ignore */ }
|
|
250
|
+
if (monResult?.avalanche) {
|
|
251
|
+
// Lock-queue avalanche: abort the entire migration
|
|
252
|
+
onProgress?.({
|
|
253
|
+
kind: 'avalanche-abort',
|
|
254
|
+
statementIndex: i,
|
|
255
|
+
totalStatements: total,
|
|
256
|
+
statementSql: stmtSql,
|
|
257
|
+
attempt,
|
|
258
|
+
elapsedMs: Date.now() - migrationStart,
|
|
259
|
+
error: `Lock-queue avalanche: ${monResult.blockedCount} backend(s) queued. Cancelled at ${monResult.cancelledAt}.`,
|
|
260
|
+
});
|
|
261
|
+
globalError = `Lock-queue avalanche detected. ${monResult.blockedCount} backend(s) were waiting behind migration PID ${migrationPid}. Migration aborted to protect connection pool.`;
|
|
262
|
+
avalanche = true;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (isLockError(code) && attempt < maxRetries) {
|
|
266
|
+
// Retryable lock error — continue retry loop
|
|
267
|
+
stmtError = `[${code}] ${err.message ?? String(err)}`;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// Non-retryable error or max retries exhausted
|
|
271
|
+
stmtError = `[${code || 'ERR'}] ${err.message ?? String(err)}`;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
client.release?.();
|
|
276
|
+
}
|
|
277
|
+
} // end retry loop
|
|
278
|
+
if (avalanche)
|
|
279
|
+
break;
|
|
280
|
+
if (signal?.aborted && !succeeded) {
|
|
281
|
+
globalError = 'Execution aborted by caller signal.';
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
if (succeeded) {
|
|
285
|
+
statementsExecuted++;
|
|
286
|
+
onProgress?.({
|
|
287
|
+
kind: 'statement-success',
|
|
288
|
+
statementIndex: i,
|
|
289
|
+
totalStatements: total,
|
|
290
|
+
statementSql: stmtSql,
|
|
291
|
+
attempt: 0,
|
|
292
|
+
elapsedMs: Date.now() - stmtStart,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
globalError = stmtError ?? 'Unknown execution error.';
|
|
297
|
+
onProgress?.({
|
|
298
|
+
kind: 'statement-failed',
|
|
299
|
+
statementIndex: i,
|
|
300
|
+
totalStatements: total,
|
|
301
|
+
statementSql: stmtSql,
|
|
302
|
+
attempt: maxRetries,
|
|
303
|
+
elapsedMs: Date.now() - stmtStart,
|
|
304
|
+
error: globalError,
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
await pool.end().catch(() => { });
|
|
312
|
+
}
|
|
313
|
+
const durationMs = Date.now() - migrationStart;
|
|
314
|
+
const success = !globalError;
|
|
315
|
+
onProgress?.({
|
|
316
|
+
kind: success ? 'migration-complete' : 'migration-failed',
|
|
317
|
+
statementIndex: statementsExecuted,
|
|
318
|
+
totalStatements: total,
|
|
319
|
+
statementSql: '',
|
|
320
|
+
attempt: 0,
|
|
321
|
+
elapsedMs: durationMs,
|
|
322
|
+
error: globalError,
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
success,
|
|
326
|
+
statementsExecuted,
|
|
327
|
+
statementsTotal: total,
|
|
328
|
+
durationMs,
|
|
329
|
+
error: globalError,
|
|
330
|
+
avalanche: avalanche || undefined,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Parses a SQL migration string and returns the list of individual statements
|
|
335
|
+
* that would be executed. Useful for tooling and dry-run previews.
|
|
336
|
+
*/
|
|
337
|
+
export function parseMigrationStatements(sql) {
|
|
338
|
+
return splitStatements(sql).map(s => statementText(s.raw));
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../src/runner/executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - Background lock-queue monitor for PostgreSQL execution supervisor.
|
|
3
|
+
*
|
|
4
|
+
* During DDL execution the monitor periodically queries pg_locks and
|
|
5
|
+
* pg_stat_activity to detect when other backends are queuing behind the
|
|
6
|
+
* migration's exclusive lock. If the queue depth exceeds a configurable
|
|
7
|
+
* threshold the monitor cancels the migration backend via pg_cancel_backend()
|
|
8
|
+
* and sets the `avalanche` flag so the executor can surface a clear error.
|
|
9
|
+
*
|
|
10
|
+
* Design notes
|
|
11
|
+
* ────────────
|
|
12
|
+
* • Uses a separate Client connection so it never shares the migration
|
|
13
|
+
* transaction (avoids deadlock on the monitor itself).
|
|
14
|
+
* • All monitor queries use SHORT local timeouts so a hung Postgres cannot
|
|
15
|
+
* also jam the monitor.
|
|
16
|
+
* • The monitor resolves its returned promise with a MonitorResult when it
|
|
17
|
+
* self-terminates; callers must await that to detect the avalanche flag.
|
|
18
|
+
*/
|
|
19
|
+
export interface MonitorOptions {
|
|
20
|
+
/** PID of the migration backend being guarded */
|
|
21
|
+
migrationPid: number;
|
|
22
|
+
/**
|
|
23
|
+
* Factory that returns a fresh client already connected to the database.
|
|
24
|
+
* The monitor will call end() on it when done.
|
|
25
|
+
*/
|
|
26
|
+
clientFactory: () => Promise<MonitorClient>;
|
|
27
|
+
/** Milliseconds between each poll (default: 500 ms) */
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Number of waiting backends that triggers a cancellation
|
|
31
|
+
* (default: 1 – i.e. cancel as soon as even one query is blocked).
|
|
32
|
+
*/
|
|
33
|
+
queueThreshold?: number;
|
|
34
|
+
/** AbortSignal: when aborted the monitor stops without cancelling */
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Minimal pg Client interface required by the monitor.
|
|
39
|
+
* Using a structural type allows tests to inject simple mocks.
|
|
40
|
+
*/
|
|
41
|
+
export interface MonitorClient {
|
|
42
|
+
query(sql: string, params?: unknown[]): Promise<{
|
|
43
|
+
rows: Record<string, unknown>[];
|
|
44
|
+
}>;
|
|
45
|
+
end(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface MonitorResult {
|
|
48
|
+
/** True when the monitor detected a queue avalanche and cancelled the backend */
|
|
49
|
+
avalanche: boolean;
|
|
50
|
+
/** Number of waiting backends detected at cancellation time */
|
|
51
|
+
blockedCount: number;
|
|
52
|
+
/** ISO timestamp at which cancellation was issued (empty if no cancellation) */
|
|
53
|
+
cancelledAt: string;
|
|
54
|
+
/** Reason the monitor stopped */
|
|
55
|
+
stopReason: 'aborted' | 'avalanche' | 'error' | 'external-stop';
|
|
56
|
+
}
|
|
57
|
+
export interface BlockedBackend {
|
|
58
|
+
waiterPid: number;
|
|
59
|
+
waiterQuery: string;
|
|
60
|
+
waiterState: string;
|
|
61
|
+
waitEventType: string;
|
|
62
|
+
waitEvent: string;
|
|
63
|
+
blockerMode: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Starts a non-blocking background lock-queue monitor.
|
|
67
|
+
*
|
|
68
|
+
* Returns a Promise<MonitorResult> that settles when the monitor self-stops
|
|
69
|
+
* (either via the AbortSignal, an avalanche cancellation, or an internal error).
|
|
70
|
+
*
|
|
71
|
+
* Also returns a `stop()` function that callers use to gracefully terminate
|
|
72
|
+
* the monitor after successful execution.
|
|
73
|
+
*/
|
|
74
|
+
export declare function startLockMonitor(options: MonitorOptions): {
|
|
75
|
+
result: Promise<MonitorResult>;
|
|
76
|
+
stop: () => void;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Determines whether a Postgres error code represents a lock-related failure.
|
|
80
|
+
*
|
|
81
|
+
* 55P03 — lock_not_available (SET LOCAL lock_timeout exceeded)
|
|
82
|
+
* 57014 — query_canceled (SET LOCAL statement_timeout exceeded, or pg_cancel_backend)
|
|
83
|
+
*/
|
|
84
|
+
export declare function isLockError(code: string): boolean;
|
|
85
|
+
//# sourceMappingURL=locksMonitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locksMonitor.d.ts","sourceRoot":"","sources":["../../../src/runner/locksMonitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IACrF,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,UAAU,EAAE,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,eAAe,CAAC;CACjE;AAqCD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG;IACzD,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CA0HA;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD"}
|