Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/exec-protocol-raw-sync-spin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@electric-sql/pglite': patch
---

Fix `execProtocolRawSync` spinning forever at 100% CPU when the WASM backend terminates mid-message (e.g. `exit(1)` after hitting EOF during a `COPY ... FROM STDIN`). Non-longjmp exceptions from the protocol loop are now rethrown instead of silently swallowed, so the call rejects with the backend error and subsequent calls fail fast instead of hanging (#1058).
15 changes: 13 additions & 2 deletions packages/pglite/src/pglite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,12 @@ export class PGlite
// that we call whenever the exception longjmp is executed
// like this we also just need to setjmp only once, in a similar fashion to the original code.
mod._PostgresMainLongJmp()
} else {
// anything else (e.g. an ExitStatus from the backend terminating,
// or a WASM RuntimeError) means the backend is gone — swallowing
// the exception here would leave the loop spinning forever, as
// no more input is consumed. Rethrow so the caller sees the error.
throw e
}
// even if there is an exception caused by one of the batched queries,
// we need to continue processing the rest without throwing.
Expand All @@ -958,8 +964,13 @@ export class PGlite
}
}
} finally {
mod._PostgresSendReadyForQueryIfNecessary()
mod._pgl_pq_flush()
try {
mod._PostgresSendReadyForQueryIfNecessary()
mod._pgl_pq_flush()
} catch {
// if the backend terminated, the runtime may already be dead;
// don't let these calls mask the original error thrown above
}
}

this.#outputData = []
Expand Down
41 changes: 41 additions & 0 deletions packages/pglite/tests/backend-death.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { PGlite } from '../dist/index.js'

// Regression test for https://github.com/electric-sql/pglite/issues/1058
//
// When the WASM backend terminates while `execProtocolRawSync` is processing
// a message (e.g. `exit(1)` after hitting EOF during a `COPY ... FROM STDIN`),
// the protocol loop used to swallow the non-longjmp exception and spin forever,
// synchronously, at 100% CPU. The call never settled and the process had to be
// killed from outside.
describe('backend terminates mid-message', () => {
let db: PGlite

beforeAll(async () => {
db = await PGlite.create()
await db.exec('CREATE TABLE backend_death_t(a int)')
})

afterAll(async () => {
if (!db.closed) {
await db.close()
}
})

it('should reject instead of spinning when the backend exits', async () => {
// The backend hits EOF during COPY FROM STDIN and exits with status 1
await expect(db.exec('COPY backend_death_t FROM STDIN')).rejects.toThrow(
/terminated|exit/i,
)
})

it('should reject promptly for queries issued after the backend died', async () => {
await expect(
db.query('SELECT 1 FROM backend_death_t'),
).rejects.toThrow(/terminated|exit/i)
})

it('should still be able to close the database', async () => {
await expect(db.close()).resolves.toBeUndefined()
})
})