Worker: a tab that closes while waiting for the query/transaction lock leaks it — every later query hangs
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- distributed-systems
Research direction
Read makeWorkerApi in src/worker/index.ts and trace _acquireQueryLock and _acquireTransactionLock alongside the tab-close cleanup. Reproduce the two-tab navigation scenario, then verify that a lock granted after the waiting tab closes is released and a later tab's query completes without a closed-channel error.
Written by the indexing model from the issue text.
Description
Summary
In the multi-tab worker (@electric-sql/pglite/worker), if a tab closes or navigates away while it is waiting for the query or transaction lock, the leader grants that lock after the tab's cleanup has already run. Nothing ever releases it, so every later query from every tab waits forever.
Reproduced with 0.5.5. The same code is in 0.5.8 (makeWorkerApi in src/worker/index.ts).
What happens
makeWorkerApi() releases a departed tab's locks when that tab's close lock frees up:
acquireLock(tabCloseLockId).then(() => {
if (transactionLockRelease) db.exec('ROLLBACK')
queryLockRelease?.()
transactionLockRelease?.()
})
That only covers locks the tab holds. Here's the sequence when it was still waiting:
- The tab calls
_acquireQueryLockover RPC. Another tab holds the lock, sodb._runExclusiveQuery(...)queues it. - The tab navigates away. Its close lock is released, and the handler above runs.
queryLockReleaseis stillnull, so nothing is released. - The queued exclusive query then runs and stores
queryLockRelease = release. It answers the RPC on the tab's channel, whichconnectTabhas already closed. The leader logsInvalidStateError: Failed to execute 'postMessage' on 'BroadcastChannel': Channel is closed. - Nobody ever calls
release. The query lock stays held, and every other tab's queries hang. A new tab can connect, but its first query never returns.
The same applies to _acquireTransactionLock.
Reproduction
This happens in a real app with two tabs of the same origin sharing one PGliteWorker. Tab A is the leader.
- Tab B opens and starts issuing queries.
- While those queries are in flight, tab B does a full navigation to another page that also uses PGlite.
- Tab B's new page never gets a result.
With a 20 s budget we hit this in 8/8 runs in Chromium, and intermittently in Firefox and WebKit.
Fix we're running
Remember that the tab closed, and release any lock granted afterwards straight away:
function makeWorkerApi(tabId: string, db: PGlite) {
let queryLockRelease: (() => void) | null = null
let transactionLockRelease: (() => void) | null = null
let tabClosed = false
acquireLock(`pglite-tab-close:${tabId}`).then(() => {
tabClosed = true
if (transactionLockRelease) db.exec('ROLLBACK')
queryLockRelease?.()
transactionLockRelease?.()
})
return {
// ...
async _acquireQueryLock() {
return new Promise<void>((resolve) => {
db._runExclusiveQuery(() => {
return new Promise<void>((release) => {
if (tabClosed) { release(); resolve(); return }
queryLockRelease = release
resolve()
})
})
})
},
// same guard in _acquireTransactionLock
}
}
With this patch the hang is gone: 3/3 passes in each of Chromium, Firefox and WebKit, and the new tab renders in about 1 s. With the patch disabled, the hang and the Channel is closed error come back.
#1112 is related, but it's the tab-side notify loop posting after close(). This one is the leader-side lock leak.
- Dominant language
- TypeScript
- Stars
- 16.1k
- Forks
- 456
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 15
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from electric-sql/pglite
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
electric-sql/pglite#1145 ·
Maintainers usually reply within 1 day
-
Feature request: Nitro 3 support (`unwasm` export condition)May be free again A pull request for this issue was closed without being merged. Open
Difficulty 1/5 Under an hour Newbie friendliness 85/100
electric-sql/pglite#1120 · 9 comments ·
Maintainers usually reply within 1 day
-
[BUG]: pglite-sync: after a `must-refetch` a synced table can stay empty; commit errors are swallowedPossibly taken @jeromerg claimed this 9 days ago. Open
Difficulty 4/5 3-5 days Newbie friendliness 45/100
electric-sql/pglite#1125 ·
Maintainers usually reply within 1 day
-
Worker: init() rejection is never propagated to clientsPossibly taken @tdrz claimed this 3 days ago. Open
Difficulty 4/5 3-5 days Newbie friendliness 72/100
electric-sql/pglite#1122 · 1 reaction · 1 assignee ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
electric-sql/pglite#1112 ·
Maintainers usually reply within 1 day
All issues in electric-sql/pglite
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
TomerAberbach/profiler-md#733 ·
Maintainers usually reply within 1 day
-
bug priority: medium
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
brentspine/brents-toasts#143 ·
-
🐛 bug 🗺️ DB Models & Ops
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
LibreChat-AI/LibreChat#17214 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Wavvon/Wavvon-clients#136 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
umami-software/umami#4637 ·
Maintainers usually reply within 3 days