fix(db): keep the lock-free path for the transaction handle only (#408) #433

Merged
maximus merged 3 commits from issue-408-db-strict-lock into main 2026-10-14 00:28:08 +00:00
Showing only changes of commit 8efe99aa20 - Show all commits

View file

@ -130,11 +130,15 @@ describe("db serialization (database-locked fix)", () => {
await tx.execute("COMMIT"); await tx.execute("COMMIT");
await gate; await gate;
}); });
await vi.waitFor(() => expect(events).toContain("end:COMMIT")); try {
expect(events).not.toContain("start:NESTED"); await vi.waitFor(() => expect(events).toContain("end:COMMIT"));
release(); expect(events).not.toContain("start:NESTED");
await txn; } finally {
await nested; // Released even on failure, so a red assertion leaves no held lock.
release();
await txn;
await nested;
}
expect(events).toEqual(["start:BEGIN", "end:BEGIN", "start:COMMIT", "end:COMMIT", "start:NESTED", "end:NESTED"]); expect(events).toEqual(["start:BEGIN", "end:BEGIN", "start:COMMIT", "end:COMMIT", "start:NESTED", "end:NESTED"]);
}); });
@ -358,20 +362,23 @@ describe("blocking alert (#408)", () => {
nested = (await getDb()).execute("NESTED"); nested = (await getDb()).execute("NESTED");
await gate; await gate;
}); });
await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS - 1); try {
expect(logWarn).not.toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS - 1);
await vi.advanceTimersByTimeAsync(1); expect(logWarn).not.toHaveBeenCalled();
expect(logWarn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(1);
const message = String(vi.mocked(logWarn).mock.calls[0][0]); expect(logWarn).toHaveBeenCalledTimes(1);
expect(message).toContain("a query has waited 15 s"); const message = String(vi.mocked(logWarn).mock.calls[0][0]);
expect(message).toContain(`held by a transaction since ${startedAt}`); expect(message).toContain("a query has waited 15 s");
// Once per operation: still one warning much later. expect(message).toContain(`held by a transaction since ${startedAt}`);
await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS * 3); // Once per operation: still one warning much later.
expect(logWarn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS * 3);
release(); expect(logWarn).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(10); } finally {
await txn; release();
await nested; await vi.advanceTimersByTimeAsync(10);
await txn;
await nested;
}
}); });
it("says nothing for an operation that got the lock in time", async () => { it("says nothing for an operation that got the lock in time", async () => {
@ -393,12 +400,17 @@ describe("blocking alert (#408)", () => {
const switching = connectToProfile("next.db"); const switching = connectToProfile("next.db");
// Settled outcome caught at once: the rejection lands while timers advance. // Settled outcome caught at once: the rejection lands while timers advance.
const waiting = withTransaction(async () => "late").catch((e: unknown) => e); const waiting = withTransaction(async () => "late").catch((e: unknown) => e);
await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS); try {
expect(logWarn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(BLOCKED_ALERT_MS);
expect(String(vi.mocked(logWarn).mock.calls[0][0])).toContain("a transaction has waited 15 s, the lock is held by a profile opening"); expect(logWarn).toHaveBeenCalledTimes(1);
finish(); expect(String(vi.mocked(logWarn).mock.calls[0][0])).toContain(
await vi.advanceTimersByTimeAsync(10); "a transaction has waited 15 s, the lock is held by a profile opening"
await switching; );
} finally {
finish();
await vi.advanceTimersByTimeAsync(10);
await switching;
}
expect(await waiting).toBeInstanceOf(DbConnectionClosedError); expect(await waiting).toBeInstanceOf(DbConnectionClosedError);
}); });
}); });