Skip to content

Commit 2afc652

Browse files
committed
fix(projects): bound the trigger installation lock window
1 parent b5cc3c4 commit 2afc652

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

‎packages/db/migrations/0395_project_membership_enforcement.sql‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ $$;
323323
--> statement-breakpoint
324324
BEGIN;
325325
--> statement-breakpoint
326+
-- Bound the whole table-lock window, including time between client statements.
327+
SET LOCAL transaction_timeout = '5s';
328+
--> statement-breakpoint
326329
SET LOCAL lock_timeout = '1s';
327330
--> statement-breakpoint
328331
SET LOCAL statement_timeout = '5s';

‎packages/db/scripts/project-contract.integration.ts‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)