pg-interleave 0.0.0-stage → 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/dist/index.d.ts +35 -0
- package/dist/index.js +136 -0
- package/package.json +47 -3
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Client } from "pg";
|
|
2
|
+
export interface InterceptOptions {
|
|
3
|
+
client?: Client;
|
|
4
|
+
clientPid?: number;
|
|
5
|
+
sessionTag?: string;
|
|
6
|
+
onQueryMatch: string | RegExp;
|
|
7
|
+
action?: "PAUSE_BEFORE_QUERY" | "PAUSE_AFTER_RESPONSE" | "pause_before_query" | "pause_after_response";
|
|
8
|
+
times?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class BarrierHandle {
|
|
11
|
+
private readonly controlUrl;
|
|
12
|
+
readonly barrierId?: string | undefined;
|
|
13
|
+
constructor(controlUrl: string, barrierId?: string | undefined);
|
|
14
|
+
wait(timeoutMs?: number): Promise<void>;
|
|
15
|
+
resume(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare class PgInterleaveClient {
|
|
18
|
+
private readonly controlUrl;
|
|
19
|
+
constructor(controlUrl?: string);
|
|
20
|
+
/**
|
|
21
|
+
* Appends session routing comment to an SQL query for connection pool compatibility
|
|
22
|
+
*/
|
|
23
|
+
static tag(sql: string, sessionTag: string): string;
|
|
24
|
+
reset(): Promise<void>;
|
|
25
|
+
hasTimeouts(): Promise<boolean>;
|
|
26
|
+
getPid(client: Client): Promise<number>;
|
|
27
|
+
intercept(options: InterceptOptions): Promise<BarrierHandle>;
|
|
28
|
+
injectTornCommit(client: Client, times?: number): Promise<void>;
|
|
29
|
+
injectDeadlock(options: {
|
|
30
|
+
client?: Client;
|
|
31
|
+
clientPid?: number;
|
|
32
|
+
onQueryMatch: string | RegExp;
|
|
33
|
+
times?: number;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PgInterleaveClient = exports.BarrierHandle = void 0;
|
|
4
|
+
class BarrierHandle {
|
|
5
|
+
controlUrl;
|
|
6
|
+
barrierId;
|
|
7
|
+
constructor(controlUrl, barrierId) {
|
|
8
|
+
this.controlUrl = controlUrl;
|
|
9
|
+
this.barrierId = barrierId;
|
|
10
|
+
}
|
|
11
|
+
async wait(timeoutMs = 5000) {
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
while (Date.now() - start < timeoutMs) {
|
|
14
|
+
const res = await fetch(`${this.controlUrl}/barriers`);
|
|
15
|
+
if (res.ok) {
|
|
16
|
+
const barriers = (await res.json());
|
|
17
|
+
if (this.barrierId) {
|
|
18
|
+
if (barriers.includes(this.barrierId))
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
else if (barriers.length > 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Timeout waiting for barrier to be reached (${timeoutMs}ms)`);
|
|
28
|
+
}
|
|
29
|
+
async resume() {
|
|
30
|
+
const url = this.barrierId
|
|
31
|
+
? `${this.controlUrl}/resume/${this.barrierId}`
|
|
32
|
+
: `${this.controlUrl}/resume-next`;
|
|
33
|
+
const res = await fetch(url, { method: "POST" });
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`Failed to resume barrier: HTTP ${res.status}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.BarrierHandle = BarrierHandle;
|
|
40
|
+
class PgInterleaveClient {
|
|
41
|
+
controlUrl;
|
|
42
|
+
constructor(controlUrl = "http://127.0.0.1:9090") {
|
|
43
|
+
this.controlUrl = controlUrl;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Appends session routing comment to an SQL query for connection pool compatibility
|
|
47
|
+
*/
|
|
48
|
+
static tag(sql, sessionTag) {
|
|
49
|
+
return `/* interleave: session=${sessionTag} */ ${sql}`;
|
|
50
|
+
}
|
|
51
|
+
async reset() {
|
|
52
|
+
const res = await fetch(`${this.controlUrl}/reset`, { method: "POST" });
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new Error(`Failed to reset proxy: HTTP ${res.status}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async hasTimeouts() {
|
|
58
|
+
const res = await fetch(`${this.controlUrl}/events`);
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new Error(`Failed to fetch events: HTTP ${res.status}`);
|
|
61
|
+
}
|
|
62
|
+
const data = (await res.json());
|
|
63
|
+
return data.had_timeouts;
|
|
64
|
+
}
|
|
65
|
+
async getPid(client) {
|
|
66
|
+
const res = await client.query("SELECT pg_backend_pid() AS pid;");
|
|
67
|
+
return res.rows[0].pid;
|
|
68
|
+
}
|
|
69
|
+
async intercept(options) {
|
|
70
|
+
let pid = options.clientPid;
|
|
71
|
+
if (!pid && options.client) {
|
|
72
|
+
pid = await this.getPid(options.client);
|
|
73
|
+
}
|
|
74
|
+
const pattern = typeof options.onQueryMatch === "string"
|
|
75
|
+
? options.onQueryMatch
|
|
76
|
+
: options.onQueryMatch.source;
|
|
77
|
+
const action = options.action
|
|
78
|
+
? options.action.toLowerCase()
|
|
79
|
+
: "pause_before_query";
|
|
80
|
+
const res = await fetch(`${this.controlUrl}/intercept`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "Content-Type": "application/json" },
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
target_pid: pid ?? null,
|
|
85
|
+
session_tag: options.sessionTag ?? null,
|
|
86
|
+
query_pattern: pattern,
|
|
87
|
+
action,
|
|
88
|
+
times: options.times ?? 1,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const errBody = await res.text();
|
|
93
|
+
throw new Error(`Failed to register interceptor: HTTP ${res.status} - ${errBody}`);
|
|
94
|
+
}
|
|
95
|
+
return new BarrierHandle(this.controlUrl);
|
|
96
|
+
}
|
|
97
|
+
async injectTornCommit(client, times = 1) {
|
|
98
|
+
const pid = await this.getPid(client);
|
|
99
|
+
const res = await fetch(`${this.controlUrl}/intercept`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
target_pid: pid,
|
|
104
|
+
query_pattern: "COMMIT",
|
|
105
|
+
action: "drop_connection_on_commit",
|
|
106
|
+
times,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
throw new Error(`Failed to inject torn commit: HTTP ${res.status}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async injectDeadlock(options) {
|
|
114
|
+
let pid = options.clientPid;
|
|
115
|
+
if (!pid && options.client) {
|
|
116
|
+
pid = await this.getPid(options.client);
|
|
117
|
+
}
|
|
118
|
+
const pattern = typeof options.onQueryMatch === "string"
|
|
119
|
+
? options.onQueryMatch
|
|
120
|
+
: options.onQueryMatch.source;
|
|
121
|
+
const res = await fetch(`${this.controlUrl}/intercept`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
target_pid: pid ?? null,
|
|
126
|
+
query_pattern: pattern,
|
|
127
|
+
action: "synthetic_deadlock",
|
|
128
|
+
times: options.times ?? 1,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
throw new Error(`Failed to inject deadlock rule: HTTP ${res.status}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.PgInterleaveClient = PgInterleaveClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pg-interleave",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Deterministic PostgreSQL wire-protocol proxy client for concurrency and chaos testing",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20.0.0"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.build.json",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run --fileParallelism=false"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"postgres",
|
|
21
|
+
"postgresql",
|
|
22
|
+
"concurrency",
|
|
23
|
+
"testing",
|
|
24
|
+
"chaos-engineering",
|
|
25
|
+
"race-condition",
|
|
26
|
+
"deadlock",
|
|
27
|
+
"proxy",
|
|
28
|
+
"wire-protocol"
|
|
29
|
+
],
|
|
30
|
+
"author": "alt-f6",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/alt-f6/pg-interleave.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/alt-f6/pg-interleave/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/alt-f6/pg-interleave#readme",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.11.0",
|
|
42
|
+
"@types/pg": "^8.11.0",
|
|
43
|
+
"pg": "^8.11.3",
|
|
44
|
+
"typescript": "^5.3.3",
|
|
45
|
+
"vitest": "^1.2.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"pg": "^8.0.0"
|
|
49
|
+
}
|
|
6
50
|
}
|
package/README.md
DELETED