@@ -135,6 +135,84 @@ describe('Project expand/backfill/contract against PostgreSQL', () => {
135135 }
136136 } )
137137
138+ it ( 'fails trigger installation promptly on a busy table and releases partially acquired locks' , async ( ) => {
139+ await database ( async ( sql ) => {
140+ const held = createDeferred < void > ( )
141+ const release = createDeferred < void > ( )
142+ const reader = sql . begin ( async ( tx ) => {
143+ await tx `SELECT * FROM workflow`
144+ held . resolve ( )
145+ await release . promise
146+ } )
147+ await held . promise
148+ try {
149+ const started = performance . now ( )
150+ await expect ( enforce ( sql ) ) . rejects . toSatisfy (
151+ ( error : unknown ) => getPostgresErrorCode ( error ) === '55P03'
152+ )
153+ expect ( performance . now ( ) - started ) . toBeLessThan ( 2000 )
154+ await sql . begin ( async ( tx ) => {
155+ await tx `SET LOCAL statement_timeout = '500ms'`
156+ await tx `INSERT INTO workspace (id, name, owner_id) VALUES ('live', 'Live', 'owner')`
157+ await tx `SELECT * FROM project`
158+ } )
159+ } finally {
160+ release . resolve ( )
161+ await reader
162+ }
163+ await enforce ( sql )
164+ expect ( await sql `SELECT * FROM project_workspace WHERE workspace_id = 'live'` ) . toHaveLength ( 1 )
165+ } )
166+ } )
167+
168+ it ( 'releases installation table locks when the migration client stalls between statements' , async ( ) => {
169+ await database ( async ( sql , url ) => {
170+ const result = await promisify ( execFile ) (
171+ 'bun' ,
172+ [
173+ '--no-env-file' ,
174+ '-e' ,
175+ `
176+ import { readFile } from 'node:fs/promises';
177+ import { sleep } from '@sim/utils/helpers';
178+ import postgres from 'postgres';
179+ const sql = postgres(process.env.TEST_DATABASE_URL, { max: 1, onnotice: () => undefined });
180+ const observer = postgres(process.env.TEST_DATABASE_URL, { max: 1 });
181+ const [{ pid }] = await sql.unsafe('SELECT pg_backend_pid() AS pid');
182+ const locks = async () => (await observer.unsafe(
183+ "SELECT count(*)::int AS count FROM pg_locks WHERE pid = $1 AND mode = 'AccessExclusiveLock' AND granted",
184+ [pid]
185+ ))[0].count;
186+ const migration = await readFile('migrations/0395_project_membership_enforcement.sql', 'utf8');
187+ for (const statement of migration.split('--> statement-breakpoint')) {
188+ await sql.unsafe(statement);
189+ if (statement.includes('LOCK TABLE workspace,')) {
190+ const before = await locks();
191+ await sleep(6500);
192+ process.stdout.write(JSON.stringify({ before, after: await locks() }));
193+ process.exit(0);
194+ }
195+ }
196+ process.exit(1);
197+ ` ,
198+ ] ,
199+ {
200+ cwd : new URL ( '..' , import . meta. url ) ,
201+ env : { ...process . env , TEST_DATABASE_URL : url } ,
202+ timeout : 12000 ,
203+ }
204+ )
205+ expect ( JSON . parse ( result . stdout ) ) . toEqual ( { before : 4 , after : 0 } )
206+ await sql . begin ( async ( tx ) => {
207+ await tx `SET LOCAL statement_timeout = '500ms'`
208+ await tx `INSERT INTO workspace (id, name, owner_id) VALUES ('live', 'Live', 'owner')`
209+ await tx `SELECT * FROM project`
210+ } )
211+ await enforce ( sql )
212+ expect ( await sql `SELECT * FROM project_workspace WHERE workspace_id = 'live'` ) . toHaveLength ( 1 )
213+ } )
214+ } , 15000 )
215+
138216 it ( 'backfills legacy environments before installing mandatory membership' , async ( ) => {
139217 await database ( async ( sql ) => {
140218 await sql `INSERT INTO workspace (id, name, owner_id) VALUES ('legacy', 'Legacy', 'owner')`
0 commit comments