Skip to content

Commit f12a582

Browse files
fix: Ensure MessageContext and its children are actually cleared between queries (#783)
* chore(submodule): bump postgres-pglite to ed3ed3de5c * tests(pglite): add message context leak repro and changeset\n\nDepends on submodule PR: electric-sql/postgres-pglite#47 * chore(submodule): bump postgres-pglite to latest macro cleanup * chore(submodule): drop stray symlink and bump postgres-pglite to 788416ceb368 * chore(submodule): bump postgres-pglite to latest cleanup * chore(changeset): move changeset to repo root; remove package-local .changeset * test(message-context): remove trailing whitespace in test file * test(server): increase timeout for waitForPort helper function
1 parent cab4ce6 commit f12a582

File tree

4 files changed

+72
-2
lines changed

4 files changed

+72
-2
lines changed

.changeset/big-lobsters-bathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@electric-sql/pglite': minor
3+
---
4+
5+
Ensure MessageContext and its children are actually cleared between queries

packages/pglite-socket/tests/server.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
88
const serverScript = path.resolve(__dirname, '../src/scripts/server.ts')
99

1010
// Helper to wait for a port to be available
11-
async function waitForPort(port: number, timeout = 5000): Promise<boolean> {
11+
async function waitForPort(port: number, timeout = 15000): Promise<boolean> {
1212
const start = Date.now()
1313

1414
while (Date.now() - start < timeout) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it, expect, beforeEach } from 'vitest'
2+
import { testDTC } from './test-utils.js'
3+
import { PGlite } from '../dist/index.js'
4+
5+
// This test isolates the MessageContext leak reported in
6+
// https://github.com/electric-sql/pglite/issues/779
7+
// It inserts many rows with large JSON literals and then inspects
8+
// pg_backend_memory_contexts to ensure MessageContext has been reset
9+
// between queries and does not accumulate unbounded allocations.
10+
11+
const KB = 1024
12+
13+
function makeJsonBlob(size: number): string {
14+
// Keep the SQL literal simple (mostly "x" payload) to simulate
15+
// large messages while avoiding excessive parsing overhead.
16+
return JSON.stringify({ padding: 'x'.repeat(size) })
17+
}
18+
19+
testDTC(async (defaultDataTransferContainer) => {
20+
describe('MessageContext reset between queries', () => {
21+
let db: PGlite
22+
23+
beforeEach(async () => {
24+
db = new PGlite({ defaultDataTransferContainer })
25+
await db.exec(`
26+
CREATE TABLE IF NOT EXISTS leak_test (
27+
id SERIAL PRIMARY KEY,
28+
blob jsonb NOT NULL
29+
);
30+
`)
31+
})
32+
33+
it('does not accumulate allocations in MessageContext', async () => {
34+
// Choose sizes to expose the leak without taking too long.
35+
const blobSize = 100 * KB // ~100KB per row
36+
const rows = 300 // ~30MB of total SQL literal payload
37+
38+
const blob = makeJsonBlob(blobSize)
39+
40+
for (let i = 0; i < rows; i++) {
41+
await db.exec(`INSERT INTO leak_test (blob) VALUES ('${blob}')`)
42+
}
43+
44+
// After the loop, the next query should see a freshly reset
45+
// MessageContext (reset happens at the start of command read),
46+
// so used_bytes should remain small (well below the total data inserted).
47+
const mem = await db.query<{ used_bytes: number }>(`
48+
SELECT used_bytes
49+
FROM pg_backend_memory_contexts
50+
WHERE name = 'MessageContext'
51+
ORDER BY level
52+
LIMIT 1
53+
`)
54+
55+
expect(mem.rows).toHaveLength(1)
56+
const used = Number(mem.rows[0].used_bytes)
57+
58+
// On a correctly resetting build, MessageContext should typically be in the
59+
// low kilobytes to a few megabytes range. Set an upper bound that will fail
60+
// if allocations accumulated across the INSERTs (~30MB of literal payload).
61+
// Using 5MB as a generous ceiling for transient allocations of this SELECT.
62+
expect(used).toBeLessThan(5 * 1024 * 1024)
63+
})
64+
})
65+
})

0 commit comments

Comments
 (0)