pg-interleave 0.0.0-stage → 0.1.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.
@@ -0,0 +1,30 @@
1
+ import type { Client } from "pg";
2
+ export interface InterceptOptions {
3
+ client?: Client;
4
+ clientPid?: number;
5
+ onQueryMatch: string | RegExp;
6
+ action?: "PAUSE_BEFORE_QUERY" | "PAUSE_AFTER_RESPONSE" | "pause_before_query" | "pause_after_response";
7
+ times?: number;
8
+ }
9
+ export declare class BarrierHandle {
10
+ private readonly controlUrl;
11
+ readonly barrierId?: string | undefined;
12
+ constructor(controlUrl: string, barrierId?: string | undefined);
13
+ wait(timeoutMs?: number): Promise<void>;
14
+ resume(): Promise<void>;
15
+ }
16
+ export declare class PgInterleaveClient {
17
+ private readonly controlUrl;
18
+ constructor(controlUrl?: string);
19
+ reset(): Promise<void>;
20
+ hasTimeouts(): Promise<boolean>;
21
+ getPid(client: Client): Promise<number>;
22
+ intercept(options: InterceptOptions): Promise<BarrierHandle>;
23
+ injectTornCommit(client: Client, times?: number): Promise<void>;
24
+ injectDeadlock(options: {
25
+ client?: Client;
26
+ clientPid?: number;
27
+ onQueryMatch: string | RegExp;
28
+ times?: number;
29
+ }): Promise<void>;
30
+ }
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
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
+ async reset() {
46
+ const res = await fetch(`${this.controlUrl}/reset`, { method: "POST" });
47
+ if (!res.ok) {
48
+ throw new Error(`Failed to reset proxy: HTTP ${res.status}`);
49
+ }
50
+ }
51
+ async hasTimeouts() {
52
+ const res = await fetch(`${this.controlUrl}/events`);
53
+ if (!res.ok) {
54
+ throw new Error(`Failed to fetch events: HTTP ${res.status}`);
55
+ }
56
+ const data = (await res.json());
57
+ return data.had_timeouts;
58
+ }
59
+ async getPid(client) {
60
+ const res = await client.query("SELECT pg_backend_pid() AS pid;");
61
+ return res.rows[0].pid;
62
+ }
63
+ async intercept(options) {
64
+ let pid = options.clientPid;
65
+ if (!pid && options.client) {
66
+ pid = await this.getPid(options.client);
67
+ }
68
+ const pattern = typeof options.onQueryMatch === "string"
69
+ ? options.onQueryMatch
70
+ : options.onQueryMatch.source;
71
+ const action = options.action
72
+ ? options.action.toLowerCase()
73
+ : "pause_before_query";
74
+ const res = await fetch(`${this.controlUrl}/intercept`, {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify({
78
+ target_pid: pid ?? null,
79
+ query_pattern: pattern,
80
+ action,
81
+ times: options.times ?? 1,
82
+ }),
83
+ });
84
+ if (!res.ok) {
85
+ const errBody = await res.text();
86
+ throw new Error(`Failed to register interceptor: HTTP ${res.status} - ${errBody}`);
87
+ }
88
+ return new BarrierHandle(this.controlUrl);
89
+ }
90
+ async injectTornCommit(client, times = 1) {
91
+ const pid = await this.getPid(client);
92
+ const res = await fetch(`${this.controlUrl}/intercept`, {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({
96
+ target_pid: pid,
97
+ query_pattern: "COMMIT",
98
+ action: "drop_connection_on_commit",
99
+ times,
100
+ }),
101
+ });
102
+ if (!res.ok) {
103
+ throw new Error(`Failed to inject torn commit: HTTP ${res.status}`);
104
+ }
105
+ }
106
+ async injectDeadlock(options) {
107
+ let pid = options.clientPid;
108
+ if (!pid && options.client) {
109
+ pid = await this.getPid(options.client);
110
+ }
111
+ const pattern = typeof options.onQueryMatch === "string"
112
+ ? options.onQueryMatch
113
+ : options.onQueryMatch.source;
114
+ const res = await fetch(`${this.controlUrl}/intercept`, {
115
+ method: "POST",
116
+ headers: { "Content-Type": "application/json" },
117
+ body: JSON.stringify({
118
+ target_pid: pid ?? null,
119
+ query_pattern: pattern,
120
+ action: "synthetic_deadlock",
121
+ times: options.times ?? 1,
122
+ }),
123
+ });
124
+ if (!res.ok) {
125
+ throw new Error(`Failed to inject deadlock rule: HTTP ${res.status}`);
126
+ }
127
+ }
128
+ }
129
+ exports.PgInterleaveClient = PgInterleaveClient;
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "pg-interleave",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.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
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.