pg-interleave 0.4.0 → 0.6.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/dist/demo.d.ts +1 -0
- package/dist/demo.js +44 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +28 -22
- package/package.json +1 -1
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const pg_1 = require("pg");
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
async function runDemo() {
|
|
7
|
+
const chaos = new index_1.PgInterleaveClient("http://127.0.0.1:9090");
|
|
8
|
+
const connStr = "postgres://postgres:postgres@127.0.0.1:5433/postgres";
|
|
9
|
+
console.log("\x1b[36m[pg-interleave]\x1b[0m Resetting proxy control plane...");
|
|
10
|
+
await chaos.reset();
|
|
11
|
+
const observer = new pg_1.Client({ connectionString: connStr });
|
|
12
|
+
const workerA = new pg_1.Client({ connectionString: connStr });
|
|
13
|
+
await observer.connect();
|
|
14
|
+
await workerA.connect();
|
|
15
|
+
console.log("\x1b[36m[setup]\x1b[0m Initializing accounts (id=1, balance=100)...");
|
|
16
|
+
await observer.query(`
|
|
17
|
+
DROP TABLE IF EXISTS accounts CASCADE;
|
|
18
|
+
CREATE TABLE accounts (id int PRIMARY KEY, balance int NOT NULL);
|
|
19
|
+
INSERT INTO accounts VALUES (1, 100);
|
|
20
|
+
`);
|
|
21
|
+
console.log("\x1b[33m[barrier]\x1b[0m Arming barrier on: \x1b[1mUPDATE accounts\x1b[0m");
|
|
22
|
+
const barrier = await chaos.intercept({
|
|
23
|
+
client: workerA,
|
|
24
|
+
onQueryMatch: /UPDATE accounts/,
|
|
25
|
+
});
|
|
26
|
+
console.log("\x1b[32m[workerA]\x1b[0m Dispatching: UPDATE accounts SET balance = balance - 50 WHERE id = 1");
|
|
27
|
+
const pendingUpdate = workerA.query("UPDATE accounts SET balance = balance - 50 WHERE id = 1");
|
|
28
|
+
console.log("\x1b[33m[barrier]\x1b[0m Waiting for query to freeze in proxy pipeline...");
|
|
29
|
+
await barrier.wait();
|
|
30
|
+
console.log("\x1b[31m[proxy]\x1b[0m \x1b[1mPAUSED!\x1b[0m PostgreSQL has NOT seen the query yet.");
|
|
31
|
+
await sleep(1500);
|
|
32
|
+
const beforeResume = await observer.query("SELECT balance FROM accounts WHERE id = 1");
|
|
33
|
+
console.log(`\x1b[35m[observer]\x1b[0m Reading balance during freeze: \x1b[1m$${beforeResume.rows[0].balance}\x1b[0m`);
|
|
34
|
+
await sleep(1500);
|
|
35
|
+
console.log("\x1b[33m[barrier]\x1b[0m Releasing barrier: barrier.resume()...");
|
|
36
|
+
await barrier.resume();
|
|
37
|
+
await pendingUpdate;
|
|
38
|
+
console.log("\x1b[32m[workerA]\x1b[0m UPDATE finished successfully.");
|
|
39
|
+
const afterResume = await observer.query("SELECT balance FROM accounts WHERE id = 1");
|
|
40
|
+
console.log(`\x1b[35m[observer]\x1b[0m Final balance: \x1b[1m$${afterResume.rows[0].balance}\x1b[0m`);
|
|
41
|
+
await observer.end();
|
|
42
|
+
await workerA.end();
|
|
43
|
+
}
|
|
44
|
+
runDemo().catch(console.error);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import type { Client } from "pg";
|
|
2
|
+
export type SqlOperation = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "select" | "insert" | "update" | "delete";
|
|
3
|
+
export interface AstSelector {
|
|
4
|
+
operation?: SqlOperation;
|
|
5
|
+
table?: string;
|
|
6
|
+
}
|
|
7
|
+
export type BarrierAction = "PAUSE_BEFORE_QUERY" | "PAUSE_AFTER_RESPONSE" | "pause_before_query" | "pause_after_response";
|
|
8
|
+
export type ParamValue = string | number | boolean | null;
|
|
2
9
|
export interface InterceptOptions {
|
|
3
10
|
client?: Client;
|
|
4
11
|
clientPid?: number;
|
|
5
12
|
sessionTag?: string;
|
|
6
|
-
onQueryMatch
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
onQueryMatch?: RegExp | string;
|
|
14
|
+
ast?: AstSelector;
|
|
15
|
+
withParams?: (ParamValue | "*")[];
|
|
16
|
+
action?: BarrierAction;
|
|
9
17
|
times?: number;
|
|
10
18
|
}
|
|
11
19
|
export declare class BarrierHandle {
|
|
@@ -18,9 +26,6 @@ export declare class BarrierHandle {
|
|
|
18
26
|
export declare class PgInterleaveClient {
|
|
19
27
|
private readonly controlUrl;
|
|
20
28
|
constructor(controlUrl?: string);
|
|
21
|
-
/**
|
|
22
|
-
* Appends session routing comment to an SQL query for connection pool compatibility
|
|
23
|
-
*/
|
|
24
29
|
static tag(sql: string, sessionTag: string): string;
|
|
25
30
|
reset(): Promise<void>;
|
|
26
31
|
resumeNext(): Promise<{
|
|
@@ -32,6 +37,7 @@ export declare class PgInterleaveClient {
|
|
|
32
37
|
getPid(client: Client): Promise<number>;
|
|
33
38
|
intercept(options: InterceptOptions): Promise<BarrierHandle>;
|
|
34
39
|
injectTornCommit(client: Client, times?: number): Promise<void>;
|
|
40
|
+
flushTrace(): Promise<void>;
|
|
35
41
|
injectDeadlock(options: {
|
|
36
42
|
client?: Client;
|
|
37
43
|
clientPid?: number;
|
package/dist/index.js
CHANGED
|
@@ -42,9 +42,6 @@ class PgInterleaveClient {
|
|
|
42
42
|
constructor(controlUrl = "http://127.0.0.1:9090") {
|
|
43
43
|
this.controlUrl = controlUrl;
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Appends session routing comment to an SQL query for connection pool compatibility
|
|
47
|
-
*/
|
|
48
45
|
static tag(sql, sessionTag) {
|
|
49
46
|
return `/* interleave: session=${sessionTag} */ ${sql}`;
|
|
50
47
|
}
|
|
@@ -83,34 +80,35 @@ class PgInterleaveClient {
|
|
|
83
80
|
return res.rows[0].pid;
|
|
84
81
|
}
|
|
85
82
|
async intercept(options) {
|
|
83
|
+
if (!options.onQueryMatch && !options.ast) {
|
|
84
|
+
throw new Error("Either 'onQueryMatch' or 'ast' selector must be provided");
|
|
85
|
+
}
|
|
86
86
|
let pid = options.clientPid;
|
|
87
87
|
if (!pid && options.client) {
|
|
88
88
|
pid = await this.getPid(options.client);
|
|
89
89
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
: options.
|
|
93
|
-
|
|
94
|
-
? options.action.toLowerCase()
|
|
95
|
-
: "pause_before_query";
|
|
96
|
-
const serializedParams = options.withParams !== undefined
|
|
97
|
-
? options.withParams.map((p) => (p === null || p === undefined ? null : String(p)))
|
|
98
|
-
: undefined;
|
|
99
|
-
const body = {
|
|
100
|
-
query_pattern: pattern,
|
|
101
|
-
action,
|
|
90
|
+
const payload = {
|
|
91
|
+
targetPid: pid,
|
|
92
|
+
sessionTag: options.sessionTag,
|
|
93
|
+
action: options.action ?? "pause_before_query",
|
|
102
94
|
times: options.times ?? 1,
|
|
103
95
|
};
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
if (options.onQueryMatch) {
|
|
97
|
+
payload.queryPattern =
|
|
98
|
+
typeof options.onQueryMatch === "string"
|
|
99
|
+
? options.onQueryMatch
|
|
100
|
+
: options.onQueryMatch.source;
|
|
101
|
+
}
|
|
102
|
+
if (options.ast) {
|
|
103
|
+
payload.ast = options.ast;
|
|
104
|
+
}
|
|
105
|
+
if (options.withParams) {
|
|
106
|
+
payload.withParams = options.withParams.map((p) => typeof p === "number" ? String(p) : p);
|
|
107
|
+
}
|
|
110
108
|
const res = await fetch(`${this.controlUrl}/intercept`, {
|
|
111
109
|
method: "POST",
|
|
112
110
|
headers: { "Content-Type": "application/json" },
|
|
113
|
-
body: JSON.stringify(
|
|
111
|
+
body: JSON.stringify(payload),
|
|
114
112
|
});
|
|
115
113
|
if (!res.ok) {
|
|
116
114
|
const errBody = await res.text();
|
|
@@ -134,6 +132,14 @@ class PgInterleaveClient {
|
|
|
134
132
|
throw new Error(`Failed to inject torn commit: HTTP ${res.status}`);
|
|
135
133
|
}
|
|
136
134
|
}
|
|
135
|
+
async flushTrace() {
|
|
136
|
+
const res = await fetch(`${this.controlUrl}/flush-trace`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
throw new Error(`Failed to flush trace: HTTP ${res.status}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
137
143
|
async injectDeadlock(options) {
|
|
138
144
|
let pid = options.clientPid;
|
|
139
145
|
if (!pid && options.client) {
|