porsager / porsager/postgres

Cursor in transaction deadlock

Open
#1,054 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
8.7k
Forks
374
Avg merge
11d 16h
Merged PRs (30d)
1

Description

Hey,

Thanks for making this library, it's very nice!

I believe I've found a deadlock when a cursor is used in a transaction and queries are issued on the same transaction as the cursor is being iterated.

Reproduction

Postgres Versions: 15 and 17
postgres version: 3.4.5 (latest)

const postgres = require('postgres');

const pg = postgres('postgres://...');
(async () => {
    try {
        await pg.begin(async (sql) => {
            const cursor = sql`select * from generate_series(0, 1000)`.cursor(1);
            for await (const _ of cursor) {
                // If `sql` is replaced with `pg` here it all works.
                await sql`select 1`;
                console.log('Loop iterated');
            }
        });
    } catch (e) {
        console.error('Caught error:', e.message);
    }
})()
    .then(() => {
        console.log('Done');
        process.exit(0);
    })
    .catch((e) => {
        console.error('Caught error:', e.message);
    });

This will stall forever and never log Loop iterated or Done. The resulting Postgres connection state will be wait_event: ClientRead.

$ SELECT wait_event, wait_event_type, query, backend_type, state FROM pg_stat_activity;
ClientRead	Client	select * from generate_series(0, 1)	client backend	active

Cause

As I understand this the cause is that the cursor query is conceptually still running and thus the second query ends up waiting for the cursor query to complete. Even if the query was allowed to run I don't think the Postgres backend would accept the query at this time. The unnamed portal is suspended and bound to the cursor query, a new query would clobber the unnamed portal and break the cursor in the next iteration.

I don't fully understand the code, but adding this line console.dir({ full, queries, isFull: c.queue === full, fullQ: full._xs[0] }); after src/index.js:318 on b231b688 prints isFull: true. Presumably this means the query will be queued until the cursor is exhausted.

Further proof of this is the following

const postgres = require('postgres');

const pg = postgres('postgres://...');

(async () => {
    try {
        await pg.begin(async (sql) => {
            const cursor = sql`select * from generate_series(0, 1)`.cursor(1);
            for await (const _ of cursor) {
                // NB: no await
                sql`select 1`
                    .then((v) => console.log('Select result:', v))
                    .catch((e) => console.error('Select error:', e.message));
                console.log('Loop iterated');
            }
            console.log('After loop');
        });
    } catch (e) {
        console.error('Caught error:', e.message);
    }
})()
    .then(() => {
        console.log('Done');
        process.exit(0);
    })
    .catch((e) => {
        console.error('Caught error:', e.message);
    });

Which prints

Loop iterated
Loop iterated
After loop
Select result: Result(1) [ { '?column?': 1 } ]
Select result: Result(1) [ { '?column?': 1 } ]
Done

Solution ideation

Feel free to ignore this section, but here are my thoughts on solves.

Throw on query while iterating over a cursor

It would be great if an exception is thrown if you attempt to issue a query like this while in the process of iterating over a cursor. This seems better than the current silent stall.

Migrate to true postgres Cursors

Instead of using the unnamed portal and suspending it, it would be nice if actual Postgres cursors were used under the hood i.e. DECLARE CURSOR et.al. I played around a little with creating cursors like this and wrapping it in an AsyncIterator, but it's currently unergonomic for consumers because all the usual awesomeness of the library isn't available (have to rely on sql.unsafe, no argument interpolation etc).

Thanks for reading!

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the reproduction against PostgreSQL 15 or 17, then inspect the queue handling around src/index.js:318, including the cursor and transaction state. Determine the intended behavior when a query is issued during cursor iteration, and add coverage showing that the operation no longer silently stalls and that the chosen behavior is consistent.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nodejs, postgresql
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.