Skip to content

Keep portal open when H is last with B or E in the sequence - #1332

Merged
levkk merged 3 commits into
pgdogdev:mainfrom
nsavvide:bug/transaction-mode-client-flush-portal-termination
Aug 11, 2026
Merged

Keep portal open when H is last with B or E in the sequence#1332
levkk merged 3 commits into
pgdogdev:mainfrom
nsavvide:bug/transaction-mode-client-flush-portal-termination

Conversation

@nsavvide

Copy link
Copy Markdown
Contributor

Problem

In pooler_mode = "transaction", a client that ends an extended-query batch with Flush before Execute loses its portal:

  ERROR:  portal "C_1" does not exist
  SQLSTATE: 34000

This breaks the node-postgres cursor/streaming stack deterministically; pg-cursor, pg-query-stream, knex .stream(), and anything layered on them (TypeORM/Sequelize streaming). They open with Parse/Bind/Describe/Flush and no Execute, which is legal: it reads the RowDescription before deciding to pull rows.

Closes #1330

Root cause

Flush and Sync were treated as the same boundary. They aren't:

  • Flush = push pending output; the batch continues.
  • Sync = the batch is over; close the implicit transaction, drop portals, send ReadyForQuery.

Fix

Reuse the existing pin mechanism (advisory locks, manual pin) with a third reason: the client is mid extended-protocol unit.

Behavior changes

  • A client holding an open portal now holds its backend until Sync. Same exposure as a client that sends BEGIN and goes idle; client_idle_in_transaction_timeout governs it (default infinite, unchanged).
  • Cursor traffic now appears in sv_locked. Accurate, but new in that metric.

Tests

Unit; opens_portal(): P B D H -> true, P D H -> false, E H -> true, B E S -> false, S -> false.

Integration (frontend::client::test), red before this change with the reported 34000:

  • test_flush_after_bind_keeps_portal — the pg-cursor opening sequence
  • test_flush_portal_paging_stays_pinnedExecute/Flush paging; pin survives several batches
  • test_flush_portal_released_on_terminate — client walks away mid-batch, backend returns to the pool
  • test_parse_describe_flush_bind_execute_close_sync — now asserts server_assignment_count == 2, locking in that the Parse-only path still releases

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@levkk

levkk commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Nice. Could you add some node tests as well to make sure this works end-to-end?

@nsavvide

nsavvide commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Nice. Could you add some node tests as well to make sure this works end-to-end?

integration/js/pg_tests/test/cursor.js, added to the existing mocha suite. Uses pg-cursor 2.22; the same version
reported, which is what pg-query-stream and knex .stream() are built on.

Against a pre-fix binary, 3 of the 4 fail with the reported error, including
the portal naming from the issue:

  1. reads the first page of a cursor:
    error: portal "C_1" does not exist
  2. pages through a cursor without losing or repeating rows:
    error: portal "C_2" does not exist
  3. runs two cursors in sequence on the same client:
    error: portal "C_3" does not exist

c3a5a73

Comment thread pgdog/src/frontend/client/query_engine/lock.rs Outdated
@levkk

levkk commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Almost good to go, just would be good to remove the locked change since it's not actually fit for this particular purpose.

@nsavvide
nsavvide requested a review from levkk August 11, 2026 06:27
@nsavvide

Copy link
Copy Markdown
Contributor Author

@levkk I have taken the alternative route, making the solution simpler really.

@levkk

levkk commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Aaah! Beautiful.

@levkk
levkk merged commit 21a38c3 into pgdogdev:main Aug 11, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transaction mode loses portals when a client Flushes before Execute (SQLSTATE 34000) — breaks node-postgres cursors/streaming

2 participants