Skip to content

Commit 6d68acd

Browse files
Add concurrent role creation test and performance benchmarks
This test demonstrates the race condition that occurs when multiple processes attempt to create the same PostgreSQL role simultaneously. On the main branch, this test FAILS with unique_violation error when multiple concurrent CREATE ROLE statements hit the pg_authid_rolname_index. The test includes: - Concurrent role creation test (reproduces the bug) - Performance benchmarks at concurrency levels 2, 4, 8 - Concurrent GRANT operations test - High concurrency stress test with retries This PR intentionally fails on main to demonstrate the problem. Compare against PR #325 (with locks) and PR #326 (no locks) to see fixes. Co-Authored-By: Dan Lynch <[email protected]>
1 parent a871ed8 commit 6d68acd

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
process.env.LOG_SCOPE = 'pgsql-test';
2+
3+
import { Pool } from 'pg';
4+
import { getPgEnvOptions } from 'pg-env';
5+
import { DbAdmin } from '../src/admin';
6+
7+
/**
8+
* Concurrent Role Creation Test
9+
*
10+
* This test demonstrates the race condition that occurs when multiple processes
11+
* attempt to create the same PostgreSQL role simultaneously. On the main branch,
12+
* this test should FAIL with a unique_violation error.
13+
*
14+
* The test also includes performance benchmarks comparing different concurrency levels.
15+
*/
16+
17+
const config = getPgEnvOptions({});
18+
const admin = new DbAdmin(config, false);
19+
20+
describe('Concurrent Role Creation', () => {
21+
const testDbName = `concurrent_role_test_${Date.now()}`;
22+
let pool: Pool;
23+
24+
beforeAll(async () => {
25+
admin.create(testDbName);
26+
27+
const bootstrapSql = `
28+
CREATE ROLE IF NOT EXISTS anonymous;
29+
CREATE ROLE IF NOT EXISTS authenticated;
30+
CREATE ROLE IF NOT EXISTS administrator;
31+
`;
32+
await admin.streamSql(bootstrapSql, testDbName);
33+
34+
pool = new Pool({
35+
...config,
36+
database: testDbName,
37+
max: 20
38+
});
39+
});
40+
41+
afterAll(async () => {
42+
await pool?.end();
43+
admin.drop(testDbName);
44+
});
45+
46+
/**
47+
* This test reproduces the unique_violation error that occurs under concurrency.
48+
*
49+
* Expected behavior on main branch: FAIL with unique_violation
50+
* Expected behavior on fix branches: PASS
51+
*/
52+
it('should handle concurrent role creation without errors', async () => {
53+
const roleName = `test_user_${Date.now()}`;
54+
const password = 'test_password';
55+
56+
const createRoleSql = `
57+
DO $$
58+
BEGIN
59+
BEGIN
60+
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', '${roleName}', '${password}');
61+
EXCEPTION
62+
WHEN duplicate_object THEN
63+
NULL;
64+
END;
65+
END $$;
66+
`;
67+
68+
const concurrentCreations = Array.from({ length: 4 }, async () => {
69+
const client = await pool.connect();
70+
try {
71+
await client.query(createRoleSql);
72+
} finally {
73+
client.release();
74+
}
75+
});
76+
77+
await expect(Promise.all(concurrentCreations)).resolves.not.toThrow();
78+
});
79+
80+
/**
81+
* Performance benchmark: Measure latency at different concurrency levels
82+
*/
83+
it('should benchmark concurrent role creation performance', async () => {
84+
const concurrencyLevels = [2, 4, 8];
85+
const iterations = 5;
86+
87+
console.log('\n=== Performance Benchmark Results ===\n');
88+
89+
for (const concurrency of concurrencyLevels) {
90+
const latencies: number[] = [];
91+
92+
for (let i = 0; i < iterations; i++) {
93+
const roleName = `bench_user_${concurrency}_${i}_${Date.now()}`;
94+
const password = 'bench_password';
95+
96+
const createRoleSql = `
97+
DO $$
98+
BEGIN
99+
BEGIN
100+
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', '${roleName}', '${password}');
101+
EXCEPTION
102+
WHEN duplicate_object OR unique_violation THEN
103+
NULL;
104+
END;
105+
END $$;
106+
`;
107+
108+
const startTime = Date.now();
109+
110+
const concurrentCreations = Array.from({ length: concurrency }, async () => {
111+
const client = await pool.connect();
112+
try {
113+
await client.query(createRoleSql);
114+
} finally {
115+
client.release();
116+
}
117+
});
118+
119+
await Promise.all(concurrentCreations);
120+
121+
const endTime = Date.now();
122+
const latency = endTime - startTime;
123+
latencies.push(latency);
124+
125+
try {
126+
await pool.query(`DROP ROLE IF EXISTS ${roleName}`);
127+
} catch (err) {
128+
}
129+
}
130+
131+
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
132+
const sortedLatencies = [...latencies].sort((a, b) => a - b);
133+
const medianLatency = sortedLatencies[Math.floor(sortedLatencies.length / 2)];
134+
135+
console.log(`Concurrency Level: ${concurrency}`);
136+
console.log(` Average Latency: ${avgLatency.toFixed(2)}ms`);
137+
console.log(` Median Latency: ${medianLatency}ms`);
138+
console.log(` Min Latency: ${Math.min(...latencies)}ms`);
139+
console.log(` Max Latency: ${Math.max(...latencies)}ms`);
140+
console.log('');
141+
}
142+
143+
console.log('=== End Performance Benchmark ===\n');
144+
});
145+
146+
/**
147+
* Test concurrent GRANT operations
148+
*/
149+
it('should handle concurrent GRANT operations without errors', async () => {
150+
const userName = `grant_test_user_${Date.now()}`;
151+
const password = 'test_password';
152+
153+
await pool.query(`CREATE ROLE ${userName} LOGIN PASSWORD '${password}'`);
154+
155+
const grantSql = `
156+
DO $$
157+
BEGIN
158+
BEGIN
159+
EXECUTE format('GRANT %I TO %I', 'anonymous', '${userName}');
160+
EXCEPTION
161+
WHEN unique_violation THEN
162+
NULL;
163+
END;
164+
END $$;
165+
`;
166+
167+
const concurrentGrants = Array.from({ length: 4 }, async () => {
168+
const client = await pool.connect();
169+
try {
170+
await client.query(grantSql);
171+
} finally {
172+
client.release();
173+
}
174+
});
175+
176+
await expect(Promise.all(concurrentGrants)).resolves.not.toThrow();
177+
178+
await pool.query(`DROP ROLE IF EXISTS ${userName}`);
179+
});
180+
181+
/**
182+
* Stress test: Create many roles concurrently with retries
183+
*/
184+
it('should handle high concurrency role creation with retries', async () => {
185+
const baseRoleName = `stress_test_${Date.now()}`;
186+
const numRoles = 10;
187+
const concurrencyPerRole = 3;
188+
189+
const createRoleWithRetries = async (roleName: string, password: string, maxRetries = 3) => {
190+
for (let attempt = 0; attempt < maxRetries; attempt++) {
191+
try {
192+
const createRoleSql = `
193+
DO $$
194+
BEGIN
195+
BEGIN
196+
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', '${roleName}', '${password}');
197+
EXCEPTION
198+
WHEN duplicate_object OR unique_violation THEN
199+
NULL;
200+
END;
201+
END $$;
202+
`;
203+
204+
const client = await pool.connect();
205+
try {
206+
await client.query(createRoleSql);
207+
return;
208+
} finally {
209+
client.release();
210+
}
211+
} catch (err: any) {
212+
if (attempt === maxRetries - 1) {
213+
throw err;
214+
}
215+
await new Promise(resolve => setTimeout(resolve, 10));
216+
}
217+
}
218+
};
219+
220+
const allCreations = [];
221+
for (let i = 0; i < numRoles; i++) {
222+
const roleName = `${baseRoleName}_${i}`;
223+
const password = 'stress_password';
224+
225+
for (let j = 0; j < concurrencyPerRole; j++) {
226+
allCreations.push(createRoleWithRetries(roleName, password));
227+
}
228+
}
229+
230+
await expect(Promise.all(allCreations)).resolves.not.toThrow();
231+
232+
for (let i = 0; i < numRoles; i++) {
233+
const roleName = `${baseRoleName}_${i}`;
234+
try {
235+
await pool.query(`DROP ROLE IF EXISTS ${roleName}`);
236+
} catch (err) {
237+
}
238+
}
239+
});
240+
});

0 commit comments

Comments
 (0)