Skip to content

Pgpool leaves an internal transaction open after autocommitted extended-protocol DML #172

Description

@apkipa

Pgpool-II version

4.8devel (1a873669), backend_clustering_mode = raw, connection_cache = on, one configured PostgreSQL node.

Description

In raw mode, an autocommitted extended-protocol UPDATE returns successfully through Pgpool, but the backend remains idle in transaction and retains the row lock. A second client trying to delete that row waits until its statement timeout. Direct PostgreSQL commits the update and the delete succeeds.

is_strict_query() classifies UPDATE as strict DML (pool_process_query.c), and the Parse() path starts an internal transaction and sets allow_close_transaction (pool_proto_modules.c):

if (is_strict_query(query_context->parse_tree))
{
	start_internal_transaction(frontend, backend, query_context->parse_tree);
	allow_close_transaction = 1;
}

start_internal_transaction() sends BEGIN to an idle backend and marks it as internal (pool_process_query.c). Pgpool's ReadyForQuery() handler processes the backend ReadyForQuery ('Z') message (pool_proto_modules.c), but closes the internal transaction only inside if (REPLICATION && allow_close_transaction) (pool_proto_modules.c). REPLICATION excludes CM_RAW (pool.h), so the internal transaction is left open.

Reproduce

  1. Start PostgreSQL 16 and Pgpool-II in raw mode with one configured PostgreSQL node and connection_cache = on.
  2. Install psycopg and run the following:
#!/usr/bin/env python3
import psycopg


TABLE = "scm_pgpool_txn_probe"


def reset_table() -> None:
    with psycopg.connect(DIRECT_DSN, autocommit=True) as conn:
        conn.execute(f"DROP TABLE IF EXISTS {TABLE}")
        conn.execute(f"CREATE TABLE {TABLE}(id integer primary key, value integer)")
        conn.execute(f"INSERT INTO {TABLE} VALUES (1, 10)")


def run(label: str, dsn: str) -> None:
    reset_table()
    a = psycopg.connect(dsn, autocommit=True)
    b = psycopg.connect(dsn, autocommit=True)
    try:
        with a.cursor() as cur:
            cur.execute(
                f"UPDATE {TABLE} SET value = value + 1 WHERE id = 1 RETURNING value",
                prepare=True,
            )
            print(label, "update", cur.fetchone()[0], flush=True)

        with b.cursor() as cur:
            cur.execute("SET statement_timeout = '5s'")
            try:
                cur.execute(f"DELETE FROM {TABLE} WHERE id = 1 RETURNING id")
                print(label, "delete", cur.fetchone()[0], flush=True)
            except Exception as exc:
                print(label, "delete", f"{type(exc).__name__}: {exc}", flush=True)
    finally:
        b.close()
        a.close()


run("direct", DIRECT_DSN)
run("pgpool", PGPOOL_DSN)

Expected behavior

Both endpoints should commit the autocommitted UPDATE; the second client should delete the row successfully.

Actual behavior

The direct endpoint succeeds. Through Pgpool, the UPDATE returns successfully, but the second client waits on the row lock and receives a statement-timeout error:

direct update 11
direct delete 1
pgpool update 11
pgpool delete QueryCanceled: canceling statement due to statement timeout
CONTEXT:  while deleting tuple (0,1) in relation "scm_pgpool_txn_probe"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions