pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
Pgbench: remove synchronous prepare
15+ messages / 5 participants
[nested] [flat]

* Pgbench: remove synchronous prepare
@ 2026-01-27 03:34  Dmitrii Bondar <d.bondar@postgrespro.ru>
  0 siblings, 1 reply; 15+ messages in thread

From: Dmitrii Bondar @ 2026-01-27 03:34 UTC (permalink / raw)
  To: pgsql-hackers@lists.postgresql.org

Hi, Hackers!

I was testing a connection pooler with pgbench and pgbench froze. I 
checked the traffic and noticed that pgbench just blocks the execution 
while it is waiting the response to the prepare command.

To reproduce the problem, it is enough to run pgbouncer with the session 
pooling mode and use more clients than the pool size. With the pool size 
of 20:

pgbench -h localhost -p 6432 --client=21 --jobs=1 -S -T 1000 -P 1 
postgres --protocol=prepared

Pgbench with the extended protocol flag does not have this issue because 
pgbench sends the whole parse/bind/execute/sync packet sequence at once 
and waits for the result asynchronously. I suggest implementing this 
behavior for the prepared protocol too.

I attached the pgbouncer configuration to reproduce the issue and the 
proposed fix. I prefer to add a new function to libpqfe instead of 
changing the existing behavior or adding a new state to pgbench. 
Although it is largely duplicated code, it looks to be as non-invasive 
as possible. Implementation and naming need to be discussed.


Tests for pgbench passed. I made small changes to the expected output.


Regards,
Dmitrii Bondar.
[databases]
* = host=127.0.0.1 port=5432

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
logfile = ./pgbouncer.log
pidfile = ./pgbouncer.pid
admin_users = postgres
auth_type = trust
auth_file = ./userlist.txt
pool_mode=session

"postgres" ""
"regress_user" ""
"test" ""
"postgres" ""


Attachments:

  [text/plain] pgbouncer.ini (235B, ../../2da56b59-acb6-4ee3-af6b-6e800115dc7f@postgrespro.ru/2-pgbouncer.ini)
  download | inline:
[databases]
* = host=127.0.0.1 port=5432

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
logfile = ./pgbouncer.log
pidfile = ./pgbouncer.pid
admin_users = postgres
auth_type = trust
auth_file = ./userlist.txt
pool_mode=session

  [text/plain] userlist.txt (56B, ../../2da56b59-acb6-4ee3-af6b-6e800115dc7f@postgrespro.ru/3-userlist.txt)
  download | inline:
"postgres" ""
"regress_user" ""
"test" ""
"postgres" ""

  [text/x-patch] 0001-Remove-synchronous-prepare-from-pgbench.patch (5.0K, ../../2da56b59-acb6-4ee3-af6b-6e800115dc7f@postgrespro.ru/4-0001-Remove-synchronous-prepare-from-pgbench.patch)
  download | inline diff:
From 1eaed7dc94359315c80f8a38e4b2b158cecf1b06 Mon Sep 17 00:00:00 2001
From: Dmitrii Bondar <d.bondar@postgrespro.ru>
Date: Tue, 27 Jan 2026 08:29:46 +0700
Subject: [PATCH] Remove synchronous prepare from pgbench

Pgbench waits for the result of a prepare packet synchronously
when the prepared protocol flag is provided. For this reason
it is impossible to use it with some poolers (such as PgBouncer)
in the session pooling mode.
It is replaced by the full sequence of parse/bind/execute/sync
packets. This behaviour is similar to pgbench with the extended
protocol flag.
---
 src/bin/pgbench/pgbench.c                    | 22 ++++++++--
 src/bin/pgbench/t/001_pgbench_with_server.pl |  1 -
 src/interfaces/libpq/exports.txt             |  1 +
 src/interfaces/libpq/fe-exec.c               | 46 ++++++++++++++++++++
 src/interfaces/libpq/libpq-fe.h              |  9 ++++
 5 files changed, 74 insertions(+), 5 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 58735871c17..8e11bc39de2 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -3179,12 +3179,26 @@ sendCommand(CState *st, Command *command)
 	{
 		const char *params[MAX_ARGS];
 
-		prepareCommand(st, st->command);
 		getQueryParams(&st->variables, command, params);
 
-		pg_log_debug("client %d sending %s", st->id, command->prepname);
-		r = PQsendQueryPrepared(st->con, command->prepname, command->argc - 1,
-								params, NULL, NULL, 0);
+		if (!st->prepared)
+			allocCStatePrepared(st);
+
+		if (!st->prepared[st->use_file][st->command])
+		{
+			r = PQsendPBES(st->con, command->prepname,
+						   command->argv[0], command->argc - 1, NULL,
+						   params, NULL, NULL, 0);
+			if (!r)
+				pg_log_error("%s", PQerrorMessage(st->con));
+			st->prepared[st->use_file][st->command] = true;
+		}
+		else
+		{
+			pg_log_debug("client %d sending %s", st->id, command->prepname);
+			r = PQsendQueryPrepared(st->con, command->prepname, command->argc - 1,
+									params, NULL, NULL, 0);
+		}
 	}
 	else						/* unknown sql mode */
 		r = 0;
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index b7685ea5d20..c047b5d0460 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -1231,7 +1231,6 @@ my @errors = (
 		2,
 		[
 			qr{ERROR:  syntax error},
-			qr{prepared statement .* does not exist}
 		],
 		q{-- SQL syntax error
     SELECT 1 + ;
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index dbbae642d76..ebebbacd5cd 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -210,3 +210,4 @@ PQgetAuthDataHook         207
 PQdefaultAuthDataHook     208
 PQfullProtocolVersion     209
 appendPQExpBufferVA       210
+PQsendPBES                210
\ No newline at end of file
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 203d388bdbf..bef103ac3f3 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -1682,6 +1682,52 @@ PQsendQueryPrepared(PGconn *conn,
 						   resultFormat);
 }
 
+/* PQsendPBES
+ *		Like PQsendQueryPrepared, but prepare a query
+ */
+int
+PQsendPBES(PGconn *conn,
+		   const char *stmtName,
+		   const char *query,
+		   int nParams,
+		   const Oid *paramTypes,
+		   const char *const *paramValues,
+		   const int *paramLengths,
+		   const int *paramFormats,
+		   int resultFormat)
+{
+	if (!PQsendQueryStart(conn, true))
+		return 0;
+
+	/* check the arguments */
+	if (!stmtName)
+	{
+		libpq_append_conn_error(conn, "statement name is a null pointer");
+		return 0;
+	}
+	if (!query)
+	{
+		libpq_append_conn_error(conn, "command string is a null pointer");
+		return 0;
+	}
+	if (nParams < 0 || nParams > PQ_QUERY_PARAM_MAX_LIMIT)
+	{
+		libpq_append_conn_error(conn, "number of parameters must be between 0 and %d",
+								PQ_QUERY_PARAM_MAX_LIMIT);
+		return 0;
+	}
+
+	return PQsendQueryGuts(conn,
+						   query,
+						   stmtName,
+						   nParams,
+						   paramTypes,
+						   paramValues,
+						   paramLengths,
+						   paramFormats,
+						   resultFormat);
+}
+
 /*
  * PQsendQueryStart
  *	Common startup code for PQsendQuery and sibling routines
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 905f2f33ab8..772583dcea9 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -525,6 +525,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendPBES(PGconn *conn,
+					   const char *stmtName,
+					   const char *query,
+					   int nParams,
+					   const Oid *paramTypes,
+					   const char *const *paramValues,
+					   const int *paramLengths,
+					   const int *paramFormats,
+					   int resultFormat);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern int	PQsetChunkedRowsMode(PGconn *conn, int chunkSize);
 extern PGresult *PQgetResult(PGconn *conn);
-- 
2.52.0



^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-03-02 11:35  Dmitrii Bondar <d.bondar@postgrespro.ru>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  0 siblings, 1 reply; 15+ messages in thread

From: Dmitrii Bondar @ 2026-03-02 11:35 UTC (permalink / raw)
  To: pgsql-hackers@lists.postgresql.org

Add docs, fix a function number.

On 1/27/26 10:34 AM, Dmitrii Bondar wrote:
> Hi, Hackers!
>
> I was testing a connection pooler with pgbench and pgbench froze. I 
> checked the traffic and noticed that pgbench just blocks the execution 
> while it is waiting the response to the prepare command.
>
> To reproduce the problem, it is enough to run pgbouncer with the 
> session pooling mode and use more clients than the pool size. With the 
> pool size of 20:
>
> pgbench -h localhost -p 6432 --client=21 --jobs=1 -S -T 1000 -P 1 
> postgres --protocol=prepared
>
> Pgbench with the extended protocol flag does not have this 
> issue because pgbench sends the whole parse/bind/execute/sync packet 
> sequence at once and waits for the result asynchronously. I suggest 
> implementing this behavior for the prepared protocol too.
>
> I attached the pgbouncer configuration to reproduce the issue and the 
> proposed fix. I prefer to add a new function to libpqfe instead of 
> changing the existing behavior or adding a new state to pgbench. 
> Although it is largely duplicated code, it looks to be as non-invasive 
> as possible. Implementation and naming need to be discussed.
>
>
> Tests for pgbench passed. I made small changes to the expected output.
>
>
> Regards,
> Dmitrii Bondar.

Attachments:

  [text/x-patch] 0002-Remove-synchronous-prepare-from-pgbench.patch (7.4K, ../../d8b4e3b2-a21f-4989-9663-3689f0d4232b@postgrespro.ru/2-0002-Remove-synchronous-prepare-from-pgbench.patch)
  download | inline diff:
From 4e4379a852f1f8f24cead5b3a5e7b90ae9cdac56 Mon Sep 17 00:00:00 2001
From: Dmitrii Bondar <d.bondar@postgrespro.ru>
Date: Mon, 2 Mar 2026 18:08:30 +0700
Subject: [PATCH 2/2] Remove synchronous prepare from pgbench

Pgbench waits for the result of a prepare packet synchronously
when the prepared protocol flag is provided. For this reason
it is impossible to use it with some poolers (such as PgBouncer)
in the session pooling mode.
It is replaced by the full sequence of parse/bind/execute/sync
packets. This behaviour is similar to pgbench with the extended
protocol flag.
---
 doc/src/sgml/libpq.sgml                      | 30 +++++++++++++
 src/bin/pgbench/pgbench.c                    | 22 ++++++++--
 src/bin/pgbench/t/001_pgbench_with_server.pl |  1 -
 src/interfaces/libpq/exports.txt             |  1 +
 src/interfaces/libpq/fe-exec.c               | 46 ++++++++++++++++++++
 src/interfaces/libpq/libpq-fe.h              |  9 ++++
 6 files changed, 104 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index e08d46782cc..1cd9c3558a4 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5329,6 +5329,7 @@ unsigned char *PQunescapeBytea(const unsigned char *from, size_t *to_length);
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendPBES"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>,
    <xref linkend="libpq-PQsendDescribePortal"/>,
    <xref linkend="libpq-PQsendClosePrepared"/>, and
@@ -5450,6 +5451,34 @@ int PQsendQueryPrepared(PGconn *conn,
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendPBES">
+     <term><function>PQsendPBES</function><indexterm><primary>PQsendPBES</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to prepare and execute a statement with given
+       parameters, without waiting for the result(s).
+<synopsis>
+int PQsendPBES(PGconn *conn,
+               const char *stmtName,
+               const char *query,
+               int nParams,
+               const Oid *paramTypes,
+               const char *const *paramValues,
+               const int *paramLengths,
+               const int *paramFormats,
+               int resultFormat);
+</synopsis>
+
+       This is similar to <xref linkend="libpq-PQsendQueryPrepared"/>, but the
+       statement is prepared as part of the same request using the supplied
+       query string. Internally, it sends a Parse, Bind, Execute, and Sync
+       message sequence. The function's parameters are handled identically to
+       <xref linkend="libpq-PQexecParams"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -5540,6 +5569,7 @@ int PQsendClosePortal(PGconn *conn, const char *portalName);
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendPBES"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>,
        <xref linkend="libpq-PQsendClosePrepared"/>,
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 1dae918cc09..749b9347a6e 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -3179,12 +3179,26 @@ sendCommand(CState *st, Command *command)
 	{
 		const char *params[MAX_ARGS];
 
-		prepareCommand(st, st->command);
 		getQueryParams(&st->variables, command, params);
 
-		pg_log_debug("client %d sending %s", st->id, command->prepname);
-		r = PQsendQueryPrepared(st->con, command->prepname, command->argc - 1,
-								params, NULL, NULL, 0);
+		if (!st->prepared)
+			allocCStatePrepared(st);
+
+		if (!st->prepared[st->use_file][st->command])
+		{
+			r = PQsendPBES(st->con, command->prepname,
+						   command->argv[0], command->argc - 1, NULL,
+						   params, NULL, NULL, 0);
+			if (!r)
+				pg_log_error("%s", PQerrorMessage(st->con));
+			st->prepared[st->use_file][st->command] = true;
+		}
+		else
+		{
+			pg_log_debug("client %d sending %s", st->id, command->prepname);
+			r = PQsendQueryPrepared(st->con, command->prepname, command->argc - 1,
+									params, NULL, NULL, 0);
+		}
 	}
 	else						/* unknown sql mode */
 		r = 0;
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index b7685ea5d20..c047b5d0460 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -1231,7 +1231,6 @@ my @errors = (
 		2,
 		[
 			qr{ERROR:  syntax error},
-			qr{prepared statement .* does not exist}
 		],
 		q{-- SQL syntax error
     SELECT 1 + ;
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index dbbae642d76..7f6664319d7 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -210,3 +210,4 @@ PQgetAuthDataHook         207
 PQdefaultAuthDataHook     208
 PQfullProtocolVersion     209
 appendPQExpBufferVA       210
+PQsendPBES                211
\ No newline at end of file
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 203d388bdbf..19e4381e6bb 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -1682,6 +1682,52 @@ PQsendQueryPrepared(PGconn *conn,
 						   resultFormat);
 }
 
+/* PQsendPBES
+ *		Like PQsendQueryPrepared, but it also prepares a query
+ */
+int
+PQsendPBES(PGconn *conn,
+		   const char *stmtName,
+		   const char *query,
+		   int nParams,
+		   const Oid *paramTypes,
+		   const char *const *paramValues,
+		   const int *paramLengths,
+		   const int *paramFormats,
+		   int resultFormat)
+{
+	if (!PQsendQueryStart(conn, true))
+		return 0;
+
+	/* check the arguments */
+	if (!stmtName)
+	{
+		libpq_append_conn_error(conn, "statement name is a null pointer");
+		return 0;
+	}
+	if (!query)
+	{
+		libpq_append_conn_error(conn, "command string is a null pointer");
+		return 0;
+	}
+	if (nParams < 0 || nParams > PQ_QUERY_PARAM_MAX_LIMIT)
+	{
+		libpq_append_conn_error(conn, "number of parameters must be between 0 and %d",
+								PQ_QUERY_PARAM_MAX_LIMIT);
+		return 0;
+	}
+
+	return PQsendQueryGuts(conn,
+						   query,
+						   stmtName,
+						   nParams,
+						   paramTypes,
+						   paramValues,
+						   paramLengths,
+						   paramFormats,
+						   resultFormat);
+}
+
 /*
  * PQsendQueryStart
  *	Common startup code for PQsendQuery and sibling routines
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 905f2f33ab8..772583dcea9 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -525,6 +525,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendPBES(PGconn *conn,
+					   const char *stmtName,
+					   const char *query,
+					   int nParams,
+					   const Oid *paramTypes,
+					   const char *const *paramValues,
+					   const int *paramLengths,
+					   const int *paramFormats,
+					   int resultFormat);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern int	PQsetChunkedRowsMode(PGconn *conn, int chunkSize);
 extern PGresult *PQgetResult(PGconn *conn);
-- 
2.52.0



^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-03-16 07:45  Dmitrii Bondar <d.bondar@postgrespro.ru>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  0 siblings, 2 replies; 15+ messages in thread

From: Dmitrii Bondar @ 2026-03-16 07:45 UTC (permalink / raw)
  To: pgsql-hackers@lists.postgresql.org

Rebase.

Regards,
Dmitrii Bondar.

Attachments:

  [text/x-patch] v3-0001-Remove-synchronous-prepare-from-pgbench.patch (7.4K, ../../cdb264ce-acb5-4a20-bd69-5eb924bef253@postgrespro.ru/2-v3-0001-Remove-synchronous-prepare-from-pgbench.patch)
  download | inline diff:
From 5d49526ee4e802b67c5b21fd473d5e02c4554048 Mon Sep 17 00:00:00 2001
From: Dmitrii Bondar <d.bondar@postgrespro.ru>
Date: Mon, 2 Mar 2026 18:08:30 +0700
Subject: [PATCH v3] Remove synchronous prepare from pgbench

Pgbench waits for the result of a prepare packet synchronously
when the prepared protocol flag is provided. For this reason
it is impossible to use it with some poolers (such as PgBouncer)
in the session pooling mode.
It is replaced by the full sequence of parse/bind/execute/sync
packets. This behaviour is similar to pgbench with the extended
protocol flag.
---
 doc/src/sgml/libpq.sgml                      | 30 +++++++++++++
 src/bin/pgbench/pgbench.c                    | 22 ++++++++--
 src/bin/pgbench/t/001_pgbench_with_server.pl |  1 -
 src/interfaces/libpq/exports.txt             |  1 +
 src/interfaces/libpq/fe-exec.c               | 46 ++++++++++++++++++++
 src/interfaces/libpq/libpq-fe.h              |  9 ++++
 6 files changed, 104 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 6db823808fc..ff890e739ac 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5329,6 +5329,7 @@ unsigned char *PQunescapeBytea(const unsigned char *from, size_t *to_length);
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendPBES"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>,
    <xref linkend="libpq-PQsendDescribePortal"/>,
    <xref linkend="libpq-PQsendClosePrepared"/>, and
@@ -5450,6 +5451,34 @@ int PQsendQueryPrepared(PGconn *conn,
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendPBES">
+     <term><function>PQsendPBES</function><indexterm><primary>PQsendPBES</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to prepare and execute a statement with given
+       parameters, without waiting for the result(s).
+<synopsis>
+int PQsendPBES(PGconn *conn,
+               const char *stmtName,
+               const char *query,
+               int nParams,
+               const Oid *paramTypes,
+               const char *const *paramValues,
+               const int *paramLengths,
+               const int *paramFormats,
+               int resultFormat);
+</synopsis>
+
+       This is similar to <xref linkend="libpq-PQsendQueryPrepared"/>, but the
+       statement is prepared as part of the same request using the supplied
+       query string. Internally, it sends a Parse, Bind, Execute, and Sync
+       message sequence. The function's parameters are handled identically to
+       <xref linkend="libpq-PQexecParams"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -5540,6 +5569,7 @@ int PQsendClosePortal(PGconn *conn, const char *portalName);
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendPBES"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>,
        <xref linkend="libpq-PQsendClosePrepared"/>,
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 1dae918cc09..749b9347a6e 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -3179,12 +3179,26 @@ sendCommand(CState *st, Command *command)
 	{
 		const char *params[MAX_ARGS];
 
-		prepareCommand(st, st->command);
 		getQueryParams(&st->variables, command, params);
 
-		pg_log_debug("client %d sending %s", st->id, command->prepname);
-		r = PQsendQueryPrepared(st->con, command->prepname, command->argc - 1,
-								params, NULL, NULL, 0);
+		if (!st->prepared)
+			allocCStatePrepared(st);
+
+		if (!st->prepared[st->use_file][st->command])
+		{
+			r = PQsendPBES(st->con, command->prepname,
+						   command->argv[0], command->argc - 1, NULL,
+						   params, NULL, NULL, 0);
+			if (!r)
+				pg_log_error("%s", PQerrorMessage(st->con));
+			st->prepared[st->use_file][st->command] = true;
+		}
+		else
+		{
+			pg_log_debug("client %d sending %s", st->id, command->prepname);
+			r = PQsendQueryPrepared(st->con, command->prepname, command->argc - 1,
+									params, NULL, NULL, 0);
+		}
 	}
 	else						/* unknown sql mode */
 		r = 0;
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index b7685ea5d20..c047b5d0460 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -1231,7 +1231,6 @@ my @errors = (
 		2,
 		[
 			qr{ERROR:  syntax error},
-			qr{prepared statement .* does not exist}
 		],
 		q{-- SQL syntax error
     SELECT 1 + ;
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..cca19c0193d 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQdefaultAuthDataHook     208
 PQfullProtocolVersion     209
 appendPQExpBufferVA       210
 PQgetThreadLock           211
+PQsendPBES                212
\ No newline at end of file
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 203d388bdbf..19e4381e6bb 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -1682,6 +1682,52 @@ PQsendQueryPrepared(PGconn *conn,
 						   resultFormat);
 }
 
+/* PQsendPBES
+ *		Like PQsendQueryPrepared, but it also prepares a query
+ */
+int
+PQsendPBES(PGconn *conn,
+		   const char *stmtName,
+		   const char *query,
+		   int nParams,
+		   const Oid *paramTypes,
+		   const char *const *paramValues,
+		   const int *paramLengths,
+		   const int *paramFormats,
+		   int resultFormat)
+{
+	if (!PQsendQueryStart(conn, true))
+		return 0;
+
+	/* check the arguments */
+	if (!stmtName)
+	{
+		libpq_append_conn_error(conn, "statement name is a null pointer");
+		return 0;
+	}
+	if (!query)
+	{
+		libpq_append_conn_error(conn, "command string is a null pointer");
+		return 0;
+	}
+	if (nParams < 0 || nParams > PQ_QUERY_PARAM_MAX_LIMIT)
+	{
+		libpq_append_conn_error(conn, "number of parameters must be between 0 and %d",
+								PQ_QUERY_PARAM_MAX_LIMIT);
+		return 0;
+	}
+
+	return PQsendQueryGuts(conn,
+						   query,
+						   stmtName,
+						   nParams,
+						   paramTypes,
+						   paramValues,
+						   paramLengths,
+						   paramFormats,
+						   resultFormat);
+}
+
 /*
  * PQsendQueryStart
  *	Common startup code for PQsendQuery and sibling routines
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f06e7a972c3..e12a6e79ca1 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -535,6 +535,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendPBES(PGconn *conn,
+					   const char *stmtName,
+					   const char *query,
+					   int nParams,
+					   const Oid *paramTypes,
+					   const char *const *paramValues,
+					   const int *paramLengths,
+					   const int *paramFormats,
+					   int resultFormat);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern int	PQsetChunkedRowsMode(PGconn *conn, int chunkSize);
 extern PGresult *PQgetResult(PGconn *conn);
-- 
2.52.0



^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-04-06 06:54  lakshmi <lakshmigcdac@gmail.com>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  1 sibling, 1 reply; 15+ messages in thread

From: lakshmi @ 2026-04-06 06:54 UTC (permalink / raw)
  To: Dmitrii Bondar <d.bondar@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org

On Mon, Mar 23, 2026 at 11:45 AM Dmitrii Bondar <d.bondar@postgrespro.ru>
wrote:

> Rebase.
>
> Hi Dmitrii,
>
> I tested the latest patch with PgBouncer in session pooling mode (pool
> size 20, 21 clients).
>
> Before applying the patch, pgbench got stuck under this setup and
> eventually hit a query_wait_timeout error.
>
> After applying the patch, pgbench runs smoothly even when clients are
> queued. I can see continuous progress output and normal throughput (~60k
> TPS), with no errors or stalls.
>
> The change works well in my testing.
>
> Thanks for the patch!
>
> Regards,
> Lakshmi G
>
>

^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-04-16 07:15  Dmitrii Bondar <d.bondar@postgrespro.ru>
  parent: lakshmi <lakshmigcdac@gmail.com>
  0 siblings, 1 reply; 15+ messages in thread

From: Dmitrii Bondar @ 2026-04-16 07:15 UTC (permalink / raw)
  To: lakshmi <lakshmigcdac@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org

Hi!

Thank you for reviewing my patch! Should I consider your review complete 
and move the patch to ‘ready for committer’?

On 4/6/26 1:54 PM, lakshmi wrote:
>
>
> On Mon, Mar 23, 2026 at 11:45 AM Dmitrii Bondar 
> <d.bondar@postgrespro.ru> wrote:
>
>     Rebase.
>
>     Hi Dmitrii,
>
>     I tested the latest patch with PgBouncer in session pooling mode
>     (pool size 20, 21 clients).
>
>     Before applying the patch, pgbench got stuck under this setup and
>     eventually hit a query_wait_timeout error.
>
>     After applying the patch, pgbench runs smoothly even when clients
>     are queued. I can see continuous progress output and normal
>     throughput (~60k TPS), with no errors or stalls.
>
>     The change works well in my testing.
>
>     Thanks for the patch!
>
>     Regards,
>     Lakshmi G
>
>

^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-04-16 12:10  lakshmi <lakshmigcdac@gmail.com>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  0 siblings, 0 replies; 15+ messages in thread

From: lakshmi @ 2026-04-16 12:10 UTC (permalink / raw)
  To: Dmitrii Bondar <d.bondar@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org

Hi Dmitrii,

Yes, my review is complete. The patch works well in my testing and resolves
the blocking issue without any regressions.

You can move it to 'Ready for Committer.'

Regards,
Lakshmi G



On Thu, Apr 16, 2026 at 12:45 PM Dmitrii Bondar <d.bondar@postgrespro.ru>
wrote:

> Hi!
>
> Thank you for reviewing my patch! Should I consider your review complete
> and move the patch to ‘ready for committer’?
> On 4/6/26 1:54 PM, lakshmi wrote:
>
>
> On Mon, Mar 23, 2026 at 11:45 AM Dmitrii Bondar <d.bondar@postgrespro.ru>
> wrote:
>
>> Rebase.
>>
>> Hi Dmitrii,
>>
>> I tested the latest patch with PgBouncer in session pooling mode (pool
>> size 20, 21 clients).
>>
>> Before applying the patch, pgbench got stuck under this setup and
>> eventually hit a query_wait_timeout error.
>>
>> After applying the patch, pgbench runs smoothly even when clients are
>> queued. I can see continuous progress output and normal throughput (~60k
>> TPS), with no errors or stalls.
>>
>> The change works well in my testing.
>>
>> Thanks for the patch!
>>
>> Regards,
>> Lakshmi G
>>
>>

^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-04-26 17:01  Robert Haas <robertmhaas@gmail.com>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  1 sibling, 1 reply; 15+ messages in thread

From: Robert Haas @ 2026-04-26 17:01 UTC (permalink / raw)
  To: Dmitrii Bondar <d.bondar@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org

On Mon, Mar 16, 2026 at 3:46 AM Dmitrii Bondar <d.bondar@postgrespro.ru> wrote:
> Rebase.

Hi,

I think that this patch is changing more behavior than is explained in
the commit message. The existing code calls PQsendQueryPrepared, which
only tries to execute an already-prepared query. The replacement code
tries to prepare the query. It is not clear to me what's going on
here. I would have expected that we would only ever reach that point
in the code with the query already prepared; otherwise, the existing
code would presumably fail. But if that is the case then how is the
new code managing to do anything different than the old code?

Another way to see that the patch must be changing more behavior than
advertised is the change to 001_pgbench_with_server.pl. That change
comes with no comment changes and no explanation of any kind.

If this patch were just about doing something asynchronously instead
of synchronously, I think that would be fine, but I don't think that's
all that is happening here. The original post explains the problem
behavior (pgbench freezing under certain circumstances) but I don't
understand what causes that behavior. I think I would understand
better if the original complaint were about something other than
session pooling mode: then, I might expect that we might unexpectedly
discover that our session does not have something prepared which we
expected to find prepared, and maybe this revised logic in
sendCommand() would somehow fix that. But in session pooling mode,
shouldn't everything be the same as if connection pooling is not in
use at all? What's actually different?

-- 
Robert Haas
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-04-28 09:29  Dmitrii Bondar <d.bondar@postgrespro.ru>
  parent: Robert Haas <robertmhaas@gmail.com>
  0 siblings, 1 reply; 15+ messages in thread

From: Dmitrii Bondar @ 2026-04-28 09:29 UTC (permalink / raw)
  To: Robert Haas <robertmhaas@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org


On 4/27/26 12:01 AM, Robert Haas wrote:
> On Mon, Mar 16, 2026 at 3:46 AM Dmitrii Bondar<d.bondar@postgrespro.ru> wrote:
>> Rebase.
> Hi,
>
> I think that this patch is changing more behavior than is explained in
> the commit message. The existing code calls PQsendQueryPrepared, which
> only tries to execute an already-prepared query. The replacement code
> tries to prepare the query. It is not clear to me what's going on
> here. I would have expected that we would only ever reach that point
> in the code with the query already prepared; otherwise, the existing
> code would presumably fail. But if that is the case then how is the
> new code managing to do anything different than the old code?
>
> Another way to see that the patch must be changing more behavior than
> advertised is the change to 001_pgbench_with_server.pl. That change
> comes with no comment changes and no explanation of any kind.
>
> If this patch were just about doing something asynchronously instead
> of synchronously, I think that would be fine, but I don't think that's
> all that is happening here. The original post explains the problem
> behavior (pgbench freezing under certain circumstances) but I don't
> understand what causes that behavior. I think I would understand
> better if the original complaint were about something other than
> session pooling mode: then, I might expect that we might unexpectedly
> discover that our session does not have something prepared which we
> expected to find prepared, and maybe this revised logic in
> sendCommand() would somehow fix that. But in session pooling mode,
> shouldn't everything be the same as if connection pooling is not in
> use at all? What's actually different?
>
Hi,

The patch does not change the existing behavior when a query has already 
been prepared. Both the old and new code paths use 
|PQsendQueryPrepared|, which sends a bind-execute-sync packet sequence 
without waiting for a response.

The main difference appears when the query has not yet been prepared. In 
the old code, |PQprepare| is called, which sends a parse message and 
then waits for the result via |PQexecFinish|. Since |PQexecFinish| 
blocks until a response arrives, it can block the entire thread if the 
server has not responded yet.

I replaced the call to |PQprepare| with a call to the new |PQsendPBES| 
function. Like |PQsendQueryPrepared|, it works asynchronously, but it 
sends a parse-bind-execute-sync sequence instead. This change avoids 
thread blocking because it eliminates the need to call |PQexecFinish|.

I chose to send a parse-bind-execute-sync sequence to match the behavior 
of extended query mode, in which pgbench sends the same sequence, but 
with an unnamed statement.

The expected output for |001_pgbench_with_server.pl| was changed for the 
following reason. In the old pgbench code, |prepareCommand| is called 
and receives |ERROR: syntax error|. Since |prepareCommand| does not 
return a status, pgbench continues execution and then attempts to run 
the command with |PQsendQueryPrepared|. This leads to the error 
|prepared statement .* does not exist|, which is caused by the bind packet.

In the new code, |PQsendPBES| sends a parse-bind-execute-sync packet 
sequence. If the parse step fails with |ERROR: syntax error|, all 
subsequent messages are ignored until the sync packet is processed. That 
is why the additional |prepared statement .* does not exist| error from 
the bind packet no longer appears.

Session mode is indeed the most transparent way to use a pooler. 
However, pgbench can become stuck when the number of clients exceeds the 
pool size. If the pooler cannot reserve a backend for a client, it 
places the client in a waiting queue. In that case, pgbench may wait 
indefinitely because it is blocked in |PQprepare|, and the pgbench 
thread cannot process responses for other clients.

Regards,
Dmitrii Bondar

^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-04-29 21:46  Robert Haas <robertmhaas@gmail.com>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  0 siblings, 1 reply; 15+ messages in thread

From: Robert Haas @ 2026-04-29 21:46 UTC (permalink / raw)
  To: Dmitrii Bondar <d.bondar@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org

On Tue, Apr 28, 2026 at 5:29 AM Dmitrii Bondar <d.bondar@postgrespro.ru> wrote:
> Session mode is indeed the most transparent way to use a pooler. However, pgbench can become stuck when the number of clients exceeds the pool size. If the pooler cannot reserve a backend for a client, it places the client in a waiting queue. In that case, pgbench may wait indefinitely because it is blocked in PQprepare, and the pgbench thread cannot process responses for other clients.

Ah, I see! This is a key point I wasn't understanding previously.

Why isn't the solution to use the existing PQsendPrepare function
instead of adding a new libpq entrypoint?

Even if we stick with the design you propose here, I don't think we
can add a function with a name like PQsendPBES, and I think we need to
find a way to more clearly explain what it does. It's kind of
unfortunate that there's such a large gap between the names of the
functions and the protocol messages that they send, but if all the
other functions are named without reference to the underlying protocol
messages, and this one is an exception, then it seems like it's going
to be hard to understand.

-- 
Robert Haas
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-05-05 07:48  Dmitrii Bondar <d.bondar@postgrespro.ru>
  parent: Robert Haas <robertmhaas@gmail.com>
  0 siblings, 1 reply; 15+ messages in thread

From: Dmitrii Bondar @ 2026-05-05 07:48 UTC (permalink / raw)
  To: Robert Haas <robertmhaas@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org


On 4/30/26 4:46 AM, Robert Haas wrote:
> Why isn't the solution to use the existing PQsendPrepare function
> instead of adding a new libpq entrypoint?

pgbench is not designed to process a response to a Parse message alone, 
because of meta-commands. For example, the \gset command requires a 
tuple to be stored, but a response to a Parse message does not provide 
one. This leads to the error: pgbench: error: client 0 script 0 command 
0 query 0: expected one row, got 0. Sending all additional messages with 
PQsendQueryPrepared may look like the exact solution, but it is not. 
PQsendQueryStart does not allow more than one command to be sent unless 
pipeline mode is enabled. This could be fixed in two ways: either by 
allowing libpq to send more than one command when pipeline is disabled, 
or by adding a new state-machine state to pgbench. Both options seem 
more invasive than the current solution. Adding a new libpq function 
just for pgbench (at least for now) does not seem ideal either, but it 
may be simpler and safer.

> I don't think we
> can add a function with a name like PQsendPBES, and I think we need to

I have other suggestions:
"PQsendQueryPrepare" but it is too close to the existing name 
"PQsendQueryPrepared".
"PQsendPrepareQuery" similar to "PQsendPrepare" but it also executes the 
query.
"PQsendPrepareExecute" is not especially well aligned with the existing 
naming scheme, but it may describe the intent quite well.

^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-05-27 12:39  Robert Haas <robertmhaas@gmail.com>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  0 siblings, 1 reply; 15+ messages in thread

From: Robert Haas @ 2026-05-27 12:39 UTC (permalink / raw)
  To: Dmitrii Bondar <d.bondar@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org

On Tue, May 5, 2026 at 3:48 AM Dmitrii Bondar <d.bondar@postgrespro.ru> wrote:
> pgbench is not designed to process a response to a Parse message alone, because of meta-commands. For example, the \gset command requires a tuple to be stored, but a response to a Parse message does not provide one. This leads to the error: pgbench: error: client 0 script 0 command 0 query 0: expected one row, got 0. Sending all additional messages with PQsendQueryPrepared may look like the exact solution, but it is not. PQsendQueryStart does not allow more than one command to be sent unless pipeline mode is enabled. This could be fixed in two ways: either by allowing libpq to send more than one command when pipeline is disabled, or by adding a new state-machine state to pgbench. Both options seem more invasive than the current solution. Adding a new libpq function just for pgbench (at least for now) does not seem ideal either, but it may be simpler and safer.

[ Catching up after pgconf.dev ]

I think there's something I'm still not understanding here. What you
said was that PQprepare can block, and I suggested using
PQsendPrepare. But your respond seems to be about PQsendQueryPrepared,
which is something different. I feel like if pgbench is using a
blocking call (like PQprepare) there should be a solution possible by
using the non-blocking variant of the same function (which in this
case is PQsendPrepare). If using that causes pgbench to die with some
weird error, that seems like a sign that other parts of pgbench also
need a bit of adjustment, rather than a sign that we need a new libpq
entrypoint.

-- 
Robert Haas
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-06-24 03:26  Dmitrii Bondar <d.bondar@postgrespro.ru>
  parent: Robert Haas <robertmhaas@gmail.com>
  0 siblings, 1 reply; 15+ messages in thread

From: Dmitrii Bondar @ 2026-06-24 03:26 UTC (permalink / raw)
  To: Robert Haas <robertmhaas@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org


On 5/27/26 7:39 PM, Robert Haas wrote:
> On Tue, May 5, 2026 at 3:48 AM Dmitrii Bondar <d.bondar@postgrespro.ru> wrote:
> [ Catching up after pgconf.dev ]
>
> ... there should be a solution possible by
> using the non-blocking variant of the same function (which in this
> case is PQsendPrepare). If using that causes pgbench to die with some
> weird error, that seems like a sign that other parts of pgbench also
> need a bit of adjustment, rather than a sign that we need a new libpq
> entrypoint.
>
You are right that there is another solution, but it requires changes in other parts of pgbench. As I mentioned in my previous message, using PQsendPrepare is absolutely possible, but it would require changes to the pgbench state machine. In particular, a new state would be needed to wait asynchronously for the response to a Prepare message. The only reason I have not done that is that it would require rewriting code in several places, whereas adding a new libpq entry would mostly extend the code rather than modify the existing logic.






^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-07-15 11:03  Бондарь Дмитрий <d.bondar@postgrespro.ru>
  parent: Dmitrii Bondar <d.bondar@postgrespro.ru>
  0 siblings, 1 reply; 15+ messages in thread

From: Бондарь Дмитрий @ 2026-07-15 11:03 UTC (permalink / raw)
  To: Robert Haas <robertmhaas@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org


I see that we are kind of stuck at this point. I agree that it is possible to change pgbench without changing libpq, even though I am not sure that this is better. I would like to submit another approach, which introduces a new state into the pgbench state machine to wait for the response to a Parse message.
 

Attachments:

  [text/x-patch] v4-0001-add-wait-prepare-state.patch (9.0K, ../../502eb70c-658f-ae0e-780b-e9ebe7104db3@postgrespro.ru/3-v4-0001-add-wait-prepare-state.patch)
  download | inline diff:
From 093e801ab8ba953736ff2b9f6c3da59321105f84 Mon Sep 17 00:00:00 2001
From: Dmitrii Bondar <d.bondar@postgrespro.ru>
Date: Wed, 15 Jul 2026 14:29:32 +0700
Subject: [PATCH v4] Remove synchronous prepare from pgbench

Pgbench waits for the result of a prepare packet synchronously
when the prepared protocol flag is provided. For this reason
it is impossible to use it with some poolers (such as PgBouncer)
in the session pooling mode.

Replace the blocking PQprepare() call in the single-command path
with an async PQsendPrepare(), and add a new CSTATE_WAIT_PREPARE_RESULT
state to the connection state machine that drains the prepare result
without stalling the thread's event loop. After the prepare succeeds,
the prepared flag is set and the actual query is sent via
PQsendQueryPrepared as before. The pipeline path
(prepareCommandsInPipeline) is left synchronous because prepares must
complete before PQenterPipelineMode to avoid acquiring a snapshot
that would break BEGIN ISOLATION LEVEL SERIALIZABLE.

Also fix a latent bug: prepareCommand used to set the prepared flag
to true even when the prepare failed, so subsequent attempts ran
PQsendQueryPrepared against a non-existent statement. The flag is
now set only on success, and prepare failures are routed through the
normal error path (CSTATE_ERROR for retryable errors, CSTATE_ABORTED
otherwise). Update the sql-syntax-error TAP test accordingly: it no
longer expects the downstream "prepared statement does not exist"
error.
---
 src/bin/pgbench/pgbench.c                    | 138 +++++++++++++++++--
 src/bin/pgbench/t/001_pgbench_with_server.pl |   1 -
 2 files changed, 123 insertions(+), 16 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 90d22e12fdb..2ddf97e41fa 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -537,6 +537,11 @@ typedef enum
 	 * CSTATE_WAIT_RESULT waits until we get a result set back from the server
 	 * for the current command.
 	 *
+	 * CSTATE_WAIT_PREPARE_RESULT waits for the result of an async PQsendPrepare
+	 * issued from CSTATE_START_COMMAND in QUERY_PREPARED mode.  Once the prepare
+	 * succeeds, the prepared flag is set and we loop back to CSTATE_START_COMMAND
+	 * to send the actual query.  On failure we go to CSTATE_ERROR.
+	 *
 	 * CSTATE_SLEEP waits until the end of \sleep.
 	 *
 	 * CSTATE_END_COMMAND records the end-of-command timestamp, increments the
@@ -549,6 +554,7 @@ typedef enum
 	 */
 	CSTATE_START_COMMAND,
 	CSTATE_WAIT_RESULT,
+	CSTATE_WAIT_PREPARE_RESULT,
 	CSTATE_SLEEP,
 	CSTATE_END_COMMAND,
 	CSTATE_SKIP_COMMAND,
@@ -3108,6 +3114,30 @@ prepareCommand(CState *st, int command_num)
 	}
 }
 
+/*
+ * Asynchronously start preparing the SQL command from st->use_file at
+ * command_num.  Caller must ensure st->prepared is allocated and that the
+ * command is not yet prepared.  Returns true if the Prepare was sent; the
+ * caller must then wait for the result in CSTATE_WAIT_PREPARE_RESULT and set
+ * the prepared flag only after the result is read successfully.
+ *
+ * Unlike prepareCommand (synchronous, used for the pipeline path), this does
+ * not set st->prepared[...].
+ */
+static bool
+sendPrepareCommand(CState *st, int command_num)
+{
+	Command    *command = sql_script[st->use_file].commands[command_num];
+
+	Assert(command->type == SQL_COMMAND);
+	Assert(st->prepared != NULL);
+	Assert(!st->prepared[st->use_file][command_num]);
+
+	pg_log_debug("client %d preparing %s", st->id, command->prepname);
+	return PQsendPrepare(st->con, command->prepname,
+						 command->argv[0], command->argc - 1, NULL) != 0;
+}
+
 /*
  * Prepare all the commands in the script that come after the \startpipeline
  * that's at position st->command, and the first \endpipeline we find.
@@ -3147,8 +3177,16 @@ prepareCommandsInPipeline(CState *st)
 	st->prepared[st->use_file][st->command] = true;
 }
 
+/* Result of sendCommand(). */
+typedef enum
+{
+	SEND_FAILED,				/* could not send */
+	SEND_OK,					/* sent; wait for query result */
+	SEND_PREPARE_PENDING,		/* async Prepare issued; wait for prepare result */
+} SendCommandResult;
+
 /* Send a SQL command, using the chosen querymode */
-static bool
+static SendCommandResult
 sendCommand(CState *st, Command *command)
 {
 	int			r;
@@ -3179,7 +3217,20 @@ sendCommand(CState *st, Command *command)
 	{
 		const char *params[MAX_ARGS];
 
-		prepareCommand(st, st->command);
+		if (!st->prepared)
+			allocCStatePrepared(st);
+
+		if (!st->prepared[st->use_file][st->command])
+		{
+			if (!sendPrepareCommand(st, st->command))
+			{
+				pg_log_debug("client %d could not send %s",
+							 st->id, command->argv[0]);
+				return SEND_FAILED;
+			}
+			return SEND_PREPARE_PENDING;
+		}
+
 		getQueryParams(&st->variables, command, params);
 
 		pg_log_debug("client %d sending %s", st->id, command->prepname);
@@ -3192,10 +3243,10 @@ sendCommand(CState *st, Command *command)
 	if (r == 0)
 	{
 		pg_log_debug("client %d could not send %s", st->id, command->argv[0]);
-		return false;
+		return SEND_FAILED;
 	}
 	else
-		return true;
+		return SEND_OK;
 }
 
 /*
@@ -3894,18 +3945,23 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 						}
 					}
 
-					if (!sendCommand(st, command))
+					switch (sendCommand(st, command))
 					{
-						commandFailed(st, "SQL", "SQL command send failed");
-						st->state = CSTATE_ABORTED;
-					}
-					else
-					{
-						/* Wait for results, unless in pipeline mode */
-						if (PQpipelineStatus(st->con) == PQ_PIPELINE_OFF)
-							st->state = CSTATE_WAIT_RESULT;
-						else
-							st->state = CSTATE_END_COMMAND;
+						case SEND_FAILED:
+							commandFailed(st, "SQL", "SQL command send failed");
+							st->state = CSTATE_ABORTED;
+							break;
+						case SEND_PREPARE_PENDING:
+							/* Wait for the async Prepare result */
+							st->state = CSTATE_WAIT_PREPARE_RESULT;
+							break;
+						case SEND_OK:
+							/* Wait for results, unless in pipeline mode */
+							if (PQpipelineStatus(st->con) == PQ_PIPELINE_OFF)
+								st->state = CSTATE_WAIT_RESULT;
+							else
+								st->state = CSTATE_END_COMMAND;
+							break;
 					}
 				}
 				else if (command->type == META_COMMAND)
@@ -3927,6 +3983,7 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 				 * something bad happened.
 				 */
 				Assert(st->state == CSTATE_WAIT_RESULT ||
+					   st->state == CSTATE_WAIT_PREPARE_RESULT ||
 					   st->state == CSTATE_END_COMMAND ||
 					   st->state == CSTATE_SLEEP ||
 					   st->state == CSTATE_ABORTED);
@@ -4070,6 +4127,55 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 					st->state = CSTATE_ABORTED;
 				break;
 
+				/*
+				 * Wait for the async PQsendPrepare result.
+				 */
+			case CSTATE_WAIT_PREPARE_RESULT:
+				{
+					PGresult   *res;
+
+					pg_log_debug("client %d receiving prepare result", st->id);
+
+					if (PQisBusy(st->con) && !PQconsumeInput(st->con))
+					{
+						commandFailed(st, "SQL", "perhaps the backend died while preparing");
+						st->state = CSTATE_ABORTED;
+						break;
+					}
+					if (PQisBusy(st->con))
+						return;		/* don't have the whole result yet */
+
+					res = PQgetResult(st->con);
+					if (PQresultStatus(res) == PGRES_COMMAND_OK)
+					{
+						PQclear(res);
+						/* null terminator expected after a single result */
+						res = PQgetResult(st->con);
+						Assert(res == NULL);
+
+						st->prepared[st->use_file][st->command] = true;
+						/* Re-enter START_COMMAND to send the actual query */
+						st->state = CSTATE_START_COMMAND;
+					}
+					else
+					{
+						const char *sqlstate = PQresultErrorField(res,
+																 PG_DIAG_SQLSTATE);
+						st->estatus = getSQLErrorStatus(st, sqlstate);
+						pg_log_error("%s", PQerrorMessage(st->con));
+						PQclear(res);
+						/* drain anything else pending on this command */
+						while ((res = PQgetResult(st->con)) != NULL)
+							PQclear(res);
+						if (canRetryError(st->estatus) ||
+							canContinueOnError(st->estatus))
+							st->state = CSTATE_ERROR;
+						else
+							st->state = CSTATE_ABORTED;
+					}
+					break;
+				}
+
 				/*
 				 * Wait until sleep is done. This state is entered after a
 				 * \sleep metacommand. The behavior is similar to
@@ -7631,6 +7737,7 @@ threadRun(void *arg)
 					min_usec = this_usec;
 			}
 			else if (st->state == CSTATE_WAIT_RESULT ||
+					 st->state == CSTATE_WAIT_PREPARE_RESULT ||
 					 st->state == CSTATE_WAIT_ROLLBACK_RESULT)
 			{
 				/*
@@ -7721,6 +7828,7 @@ threadRun(void *arg)
 			CState	   *st = &state[i];
 
 			if (st->state == CSTATE_WAIT_RESULT ||
+				st->state == CSTATE_WAIT_PREPARE_RESULT ||
 				st->state == CSTATE_WAIT_ROLLBACK_RESULT)
 			{
 				/* don't call advanceConnectionState unless data is available */
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index bb12ade59f5..5306c8c1abc 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -1231,7 +1231,6 @@ my @errors = (
 		2,
 		[
 			qr{ERROR:  syntax error},
-			qr{prepared statement .* does not exist}
 		],
 		q{-- SQL syntax error
     SELECT 1 + ;
-- 
2.54.0



^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-09-10 13:09  Andrey Borodin <x4mmm@yandex-team.ru>
  parent: Бондарь Дмитрий <d.bondar@postgrespro.ru>
  0 siblings, 1 reply; 15+ messages in thread

From: Andrey Borodin @ 2026-09-10 13:09 UTC (permalink / raw)
  To: Бондарь Дмитрий <d.bondar@postgrespro.ru>; +Cc: Robert Haas <robertmhaas@gmail.com>; pgsql-hackers mailing list <pgsql-hackers@lists.postgresql.org>



> On 15 Jul 2026, at 16:03, Бондарь Дмитрий <d.bondar@postgrespro.ru> wrote:
> 
>   <v4-0001-add-wait-prepare-state.patch>

Hi Dmitrii,

I think this is worth fixing in pgbench itself.  The benefit is not
limited to poolers: a prepare waiting for a table lock can also stall
unrelated clients on the same worker thread.  That introduces a
dependency between clients into the generated workload, rather than
measuring how the server handles their independent requests.

The additional state seems a reasonable maintenance cost for fixing
that.  It fits pgbench's existing model of waiting for each connection
through the event loop, and keeps the change local instead of extending
libpq's public API.  I think this is the right direction, with a couple
of issues in the state transitions below.

Returning to CSTATE_START_COMMAND after preparing resets stmt_begin.
This drops the prepare time from --report-per-command.  In a local test,
blocking Prepare on a table lock gave 4436 ms transaction latency
but only 0.401 ms statement latency.  Could we send the query without
resetting the command timer?

The second PQgetResult() in CSTATE_WAIT_PREPARE_RESULT can still block:
the first returns ParseComplete, but the next must wait for
ReadyForQuery if it has not arrived yet.  The error path also drains
results synchronously through getSQLErrorStatus().  Similar reads
already exist elsewhere in pgbench, but this leaves a blocking window
in the new prepare path.  Could we wait for completion through the
event loop?  A connection failure between the two results can also
make the second call return an error rather than the asserted NULL.

I also found a way to test the main fix without PgBouncer.  With
-c 2 -j 1, use client_id to make client 0 select from a table locked
by another session in ACCESS EXCLUSIVE mode, and client 1 insert into
a different table.  In local runs, client 1 made progress while
client 0 was blocked in Prepare with v4, but not without the patch.
This would test the intended behavior more directly than the changed
syntax-error expectation.

I am fine with leaving prepareCommandsInPipeline() for a separate patch.
Could the commit message describe this as a scope choice?  Preparing
before PQenterPipelineMode() preserves the required ordering, but does
not require blocking the worker thread.  The prepared-flag fix is also
limited to the non-pipeline path: prepareCommand() still sets the flag
after an error.

Thank you!


Best regards, Andrey Borodin.





^ permalink  raw  reply  [nested|flat] 15+ messages in thread

* Re: Pgbench: remove synchronous prepare
@ 2026-09-17 11:06  Dmitrii Bondar <d.bondar@postgrespro.ru>
  parent: Andrey Borodin <x4mmm@yandex-team.ru>
  0 siblings, 0 replies; 15+ messages in thread

From: Dmitrii Bondar @ 2026-09-17 11:06 UTC (permalink / raw)
  To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: Robert Haas <robertmhaas@gmail.com>; pgsql-hackers mailing list <pgsql-hackers@lists.postgresql.org>

Hi, Andrey!

> Returning to CSTATE_START_COMMAND after preparing resets stmt_begin.
> This drops the prepare time from --report-per-command.  In a local test,
> blocking Prepare on a table lock gave 4436 ms transaction latency
> but only 0.401 ms statement latency.  Could we send the query without
> resetting the command timer?
Removed a transfer to CSTATE_START_COMMAND and just used sendCommand 
from the new state.
> The second PQgetResult() in CSTATE_WAIT_PREPARE_RESULT can still block:
> the first returns ParseComplete, but the next must wait for
> ReadyForQuery if it has not arrived yet.  The error path also drains
> results synchronously through getSQLErrorStatus().  Similar reads
> already exist elsewhere in pgbench, but this leaves a blocking window
> in the new prepare path.  Could we wait for completion through the
> event loop?  A connection failure between the two results can also
> make the second call return an error rather than the asserted NULL.
Spit the state into two stages to avoid blocking. Added an event loop to 
the error path.
> I also found a way to test the main fix without PgBouncer.  With
> -c 2 -j 1, use client_id to make client 0 select from a table locked
> by another session in ACCESS EXCLUSIVE mode, and client 1 insert into
> a different table.  In local runs, client 1 made progress while
> client 0 was blocked in Prepare with v4, but not without the patch.
> This would test the intended behavior more directly than the changed
> syntax-error expectation.
Implemented.
> I am fine with leaving prepareCommandsInPipeline() for a separate patch.
> Could the commit message describe this as a scope choice?  Preparing
> before PQenterPipelineMode() preserves the required ordering, but does
> not require blocking the worker thread.  The prepared-flag fix is also
> limited to the non-pipeline path: prepareCommand() still sets the flag
> after an error.

Fixed the flag and added a note to the commit message.

Looks much better now, thank you for reviewing the patch!

-- 
Regards,
Dmitrii Bondar.

Attachments:

  [text/x-patch] v5-0001-Remove-synchronous-prepare-from-pgbench.patch (14.7K, ../../9ac169f5-5c83-4980-bd28-27e4fe682453@postgrespro.ru/2-v5-0001-Remove-synchronous-prepare-from-pgbench.patch)
  download | inline diff:
From 310fe856457222828de276116666608da4dd38b1 Mon Sep 17 00:00:00 2001
From: Dmitrii Bondar <d.bondar@postgrespro.ru>
Date: Thu, 17 Sep 2026 16:32:30 +0700
Subject: [PATCH v5] Remove synchronous prepare from pgbench

With the prepared protocol (-M prepared), pgbench prepares each SQL
command with the blocking PQprepare() before executing it.  Nothing
else can be processed in the meantime, which makes the prepared
protocol unusable behind poolers that serialize a session's commands
(such as PgBouncer in session pooling mode): the whole client stalls
while the Prepare waits.

Replace the blocking PQprepare() in sendCommand() with an asynchronous
PQsendPrepare(), driven by a new CSTATE_WAIT_PREPARE_RESULT state.

The prepare failure path reads the sqlstate from the error result with
the new getSQLErrorStatusFromResult() helper, which, unlike
getSQLErrorStatus(), does not read or discard anything from the
connection.

The pipeline path is left synchronous on purpose (scope choice).
---
 src/bin/pgbench/pgbench.c                    | 261 +++++++++++++++++--
 src/bin/pgbench/t/001_pgbench_with_server.pl |  55 +++-
 2 files changed, 293 insertions(+), 23 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 181f0d892ef..ed969bd33bf 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -537,6 +537,13 @@ typedef enum
 	 * CSTATE_WAIT_RESULT waits until we get a result set back from the server
 	 * for the current command.
 	 *
+	 * CSTATE_WAIT_PREPARE_RESULT waits for the result of an async
+	 * PQsendPrepare issued from CSTATE_START_COMMAND in QUERY_PREPARED mode.
+	 * It has two phases, tracked by st->prepare_result_consumed: first the
+	 * result of the Prepare itself is read, then the trailing NULL result
+	 * that follows it. Both are consumed through the event loop, as waiting
+	 * for them can block.
+	 *
 	 * CSTATE_SLEEP waits until the end of \sleep.
 	 *
 	 * CSTATE_END_COMMAND records the end-of-command timestamp, increments the
@@ -549,6 +556,7 @@ typedef enum
 	 */
 	CSTATE_START_COMMAND,
 	CSTATE_WAIT_RESULT,
+	CSTATE_WAIT_PREPARE_RESULT,
 	CSTATE_SLEEP,
 	CSTATE_END_COMMAND,
 	CSTATE_SKIP_COMMAND,
@@ -631,6 +639,13 @@ typedef struct
 	/* whether client prepared each command of each script */
 	bool	  **prepared;
 
+	/*
+	 * Sub-state for CSTATE_WAIT_PREPARE_RESULT: true once the result of the
+	 * async Prepare itself has been consumed; the trailing NULL result still
+	 * remains to be read at that point.
+	 */
+	bool		prepare_result_consumed;
+
 	/*
 	 * For processing failures and repeating transactions with serialization
 	 * or deadlock errors:
@@ -3103,11 +3118,36 @@ prepareCommand(CState *st, int command_num)
 						command->argv[0], command->argc - 1, NULL);
 		if (PQresultStatus(res) != PGRES_COMMAND_OK)
 			pg_log_error("%s", PQerrorMessage(st->con));
+		else
+			st->prepared[st->use_file][command_num] = true;
 		PQclear(res);
-		st->prepared[st->use_file][command_num] = true;
 	}
 }
 
+/*
+ * Asynchronously start preparing the SQL command from st->use_file at
+ * command_num.  The caller must then wait for the result in
+ * CSTATE_WAIT_PREPARE_RESULT and set the prepared flag only after the
+ * result is read successfully.
+ *
+ * This must not be used in pipeline mode: pipeline commands are always
+ * pre-prepared synchronously by prepareCommandsInPipeline()
+ */
+static bool
+sendPrepareCommand(CState *st, int command_num)
+{
+	Command    *command = sql_script[st->use_file].commands[command_num];
+
+	Assert(command->type == SQL_COMMAND);
+	Assert(st->prepared != NULL);
+	Assert(!st->prepared[st->use_file][command_num]);
+	Assert(PQpipelineStatus(st->con) == PQ_PIPELINE_OFF);
+
+	pg_log_debug("client %d preparing %s", st->id, command->prepname);
+	return PQsendPrepare(st->con, command->prepname,
+						 command->argv[0], command->argc - 1, NULL) != 0;
+}
+
 /*
  * Prepare all the commands in the script that come after the \startpipeline
  * that's at position st->command, and the first \endpipeline we find.
@@ -3147,8 +3187,17 @@ prepareCommandsInPipeline(CState *st)
 	st->prepared[st->use_file][st->command] = true;
 }
 
+/* Result of sendCommand(). */
+typedef enum
+{
+	SEND_FAILED,
+	SEND_OK,					/* sent; wait for query result */
+	SEND_PREPARE_PENDING,		/* async Prepare issued; wait for prepare
+								 * result */
+}			SendCommandResult;
+
 /* Send a SQL command, using the chosen querymode */
-static bool
+static SendCommandResult
 sendCommand(CState *st, Command *command)
 {
 	int			r;
@@ -3179,7 +3228,28 @@ sendCommand(CState *st, Command *command)
 	{
 		const char *params[MAX_ARGS];
 
-		prepareCommand(st, st->command);
+		if (!st->prepared)
+			allocCStatePrepared(st);
+
+		if (!st->prepared[st->use_file][st->command])
+		{
+			/*
+			 * If a pipeline prepare failed, its flag is not set, so this
+			 * state is reachable; bail out rather than issuing an
+			 * asynchronous Prepare in the middle of a pipeline.
+			 */
+			if (PQpipelineStatus(st->con) != PQ_PIPELINE_OFF)
+				return SEND_FAILED;
+
+			if (!sendPrepareCommand(st, st->command))
+			{
+				pg_log_debug("client %d could not send %s",
+							 st->id, command->argv[0]);
+				return SEND_FAILED;
+			}
+			return SEND_PREPARE_PENDING;
+		}
+
 		getQueryParams(&st->variables, command, params);
 
 		pg_log_debug("client %d sending %s", st->id, command->prepname);
@@ -3192,10 +3262,10 @@ sendCommand(CState *st, Command *command)
 	if (r == 0)
 	{
 		pg_log_debug("client %d could not send %s", st->id, command->argv[0]);
-		return false;
+		return SEND_FAILED;
 	}
 	else
-		return true;
+		return SEND_OK;
 }
 
 /*
@@ -3227,15 +3297,14 @@ discardAvailableResults(CState *st)
 }
 
 /*
- * Determine the error status based on the connection status and error code.
+ * Determine the error status from an error result's sqlstate.
+ *
+ * Unlike getSQLErrorStatus(), this does not read or discard anything from the
+ * connection, so it is safe to use in states that must not block
  */
 static EStatus
-getSQLErrorStatus(CState *st, const char *sqlState)
+getSQLErrorStatusFromResult(const char *sqlState)
 {
-	discardAvailableResults(st);
-	if (PQstatus(st->con) == CONNECTION_BAD)
-		return ESTATUS_CONN_ERROR;
-
 	if (sqlState != NULL)
 	{
 		if (strcmp(sqlState, ERRCODE_T_R_SERIALIZATION_FAILURE) == 0)
@@ -3247,6 +3316,19 @@ getSQLErrorStatus(CState *st, const char *sqlState)
 	return ESTATUS_OTHER_SQL_ERROR;
 }
 
+/*
+ * Determine the error status based on the connection status and error code.
+ */
+static EStatus
+getSQLErrorStatus(CState *st, const char *sqlState)
+{
+	discardAvailableResults(st);
+	if (PQstatus(st->con) == CONNECTION_BAD)
+		return ESTATUS_CONN_ERROR;
+
+	return getSQLErrorStatusFromResult(sqlState);
+}
+
 /*
  * Returns true if this type of error can be retried.
  */
@@ -3907,18 +3989,22 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 						}
 					}
 
-					if (!sendCommand(st, command))
-					{
-						commandFailed(st, "SQL", "SQL command send failed");
-						st->state = CSTATE_ABORTED;
-					}
-					else
+					switch (sendCommand(st, command))
 					{
-						/* Wait for results, unless in pipeline mode */
-						if (PQpipelineStatus(st->con) == PQ_PIPELINE_OFF)
-							st->state = CSTATE_WAIT_RESULT;
-						else
-							st->state = CSTATE_END_COMMAND;
+						case SEND_FAILED:
+							commandFailed(st, "SQL", "SQL command send failed");
+							st->state = CSTATE_ABORTED;
+							break;
+						case SEND_PREPARE_PENDING:
+							st->prepare_result_consumed = false;
+							st->state = CSTATE_WAIT_PREPARE_RESULT;
+							break;
+						case SEND_OK:
+							if (PQpipelineStatus(st->con) == PQ_PIPELINE_OFF)
+								st->state = CSTATE_WAIT_RESULT;
+							else
+								st->state = CSTATE_END_COMMAND;
+							break;
 					}
 				}
 				else if (command->type == META_COMMAND)
@@ -3940,6 +4026,7 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 				 * something bad happened.
 				 */
 				Assert(st->state == CSTATE_WAIT_RESULT ||
+					   st->state == CSTATE_WAIT_PREPARE_RESULT ||
 					   st->state == CSTATE_END_COMMAND ||
 					   st->state == CSTATE_SLEEP ||
 					   st->state == CSTATE_ABORTED);
@@ -4083,6 +4170,134 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 					st->state = CSTATE_ABORTED;
 				break;
 
+				/*
+				 * Wait for the async PQsendPrepare result.  Two results are
+				 * expected: the result of the Prepare itself, then a NULL
+				 * result.  Both have to be consumed through the event loop,
+				 * as waiting for either can block.
+				 */
+			case CSTATE_WAIT_PREPARE_RESULT:
+				{
+					PGresult   *res;
+
+					pg_log_debug("client %d receiving prepare result", st->id);
+
+					if (PQisBusy(st->con) && !PQconsumeInput(st->con))
+					{
+						commandFailed(st, "SQL", "perhaps the backend died while preparing");
+						st->state = CSTATE_ABORTED;
+						break;
+					}
+					if (PQisBusy(st->con))
+						return; /* don't have the whole result yet */
+
+					if (!st->prepare_result_consumed)
+					{
+						/* Phase 1: consume the result of the Prepare. */
+						st->prepare_result_consumed = true;
+
+						res = PQgetResult(st->con);
+						if (PQresultStatus(res) == PGRES_COMMAND_OK)
+						{
+							PQclear(res);
+							/* phase 2 will consume the trailing NULL */
+							break;
+						}
+
+						if (PQstatus(st->con) == CONNECTION_BAD)
+						{
+							/* connection failure: nothing more to consume */
+							st->estatus = ESTATUS_CONN_ERROR;
+							st->state = CSTATE_ABORTED;
+						}
+						else
+						{
+							/*
+							 * Determine the error status from the sqlstate
+							 * without touching the connection: the remaining
+							 * results are consumed in phase 2 below
+							 */
+							st->estatus =
+								getSQLErrorStatusFromResult(PQresultErrorField(res,
+																			   PG_DIAG_SQLSTATE));
+							if (canRetryError(st->estatus) ||
+								canContinueOnError(st->estatus))
+							{
+								if (verbose_errors)
+									commandError(st, PQresultErrorMessage(res));
+							}
+							else
+							{
+								/* anything else is unexpected */
+								pg_log_error("client %d script %d aborted in command %d: %s",
+											 st->id, st->use_file, st->command,
+											 PQresultErrorMessage(res));
+							}
+						}
+						PQclear(res);
+						break;
+					}
+
+					/* Phase 2: consume the trailing NULL result. */
+					res = PQgetResult(st->con);
+					if (PQstatus(st->con) == CONNECTION_BAD)
+					{
+						pg_log_error("client %d aborted while receiving the prepare result; perhaps the backend died while preparing",
+									 st->id);
+						PQclear(res);
+						st->estatus = ESTATUS_CONN_ERROR;
+						st->state = CSTATE_ABORTED;
+						break;
+					}
+					if (res != NULL)
+					{
+						/*
+						 * The NULL result should always follow the prepare
+						 * result, but a connection failure between the two
+						 * reads can surface here as a fatal error result
+						 * instead.
+						 */
+						pg_log_error("client %d aborted while receiving the prepare result; %s",
+									 st->id, PQerrorMessage(st->con));
+						PQclear(res);
+						st->estatus = ESTATUS_CONN_ERROR;
+						st->state = CSTATE_ABORTED;
+						break;
+					}
+
+					if (st->estatus == ESTATUS_NO_ERROR)
+					{
+						Command    *cmd = sql_script[st->use_file].commands[st->command];
+
+						st->prepared[st->use_file][st->command] = true;
+
+						switch (sendCommand(st, cmd))
+						{
+							case SEND_FAILED:
+								commandFailed(st, "SQL", "SQL command send failed");
+								st->state = CSTATE_ABORTED;
+								break;
+							case SEND_PREPARE_PENDING:
+								Assert(false);
+								st->state = CSTATE_ABORTED;
+								break;
+							case SEND_OK:
+								Assert(PQpipelineStatus(st->con) == PQ_PIPELINE_OFF);
+								st->state = CSTATE_WAIT_RESULT;
+								break;
+						}
+					}
+					else
+					{
+						if (canRetryError(st->estatus) ||
+							canContinueOnError(st->estatus))
+							st->state = CSTATE_ERROR;
+						else
+							st->state = CSTATE_ABORTED;
+					}
+					break;
+				}
+
 				/*
 				 * Wait until sleep is done. This state is entered after a
 				 * \sleep metacommand. The behavior is similar to
@@ -7644,6 +7859,7 @@ threadRun(void *arg)
 					min_usec = this_usec;
 			}
 			else if (st->state == CSTATE_WAIT_RESULT ||
+					 st->state == CSTATE_WAIT_PREPARE_RESULT ||
 					 st->state == CSTATE_WAIT_ROLLBACK_RESULT)
 			{
 				/*
@@ -7734,6 +7950,7 @@ threadRun(void *arg)
 			CState	   *st = &state[i];
 
 			if (st->state == CSTATE_WAIT_RESULT ||
+				st->state == CSTATE_WAIT_PREPARE_RESULT ||
 				st->state == CSTATE_WAIT_ROLLBACK_RESULT)
 			{
 				/* don't call advanceConnectionState unless data is available */
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index b669ad9d92f..a9cfeb8157e 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -1245,7 +1245,6 @@ my @errors = (
 		2,
 		[
 			qr{ERROR:  syntax error},
-			qr{prepared statement .* does not exist}
 		],
 		q{-- SQL syntax error
     SELECT 1 + ;
@@ -1866,6 +1865,60 @@ $node->pgbench(
 # Clean up
 $node->safe_psql('postgres', 'DROP TABLE unique_table;');
 
+# Test that a client blocked in an asynchronous Prepare does not block
+# other clients: with -c 2 -j 1, both clients are driven by the same event loop,
+# so a blocking wait for the prepare result would prevent client 1 from
+# proceeding, and this test would hang.
+$node->safe_psql('postgres',
+	'CREATE TABLE blocked_prepare_locked (i int); '
+  . 'CREATE TABLE blocked_prepare_progress (i int);');
+
+$node->pgbench(
+	'-n -c 2 -j 1 -t 1 -M prepared', 0,
+	[ qr{processed: 2/2} ], [qr{^$}],
+	'client blocked in Prepare does not block other clients',
+	{
+		'001_pgbench_async_prepare' => q{
+\if :client_id = 0
+-- wait until client 1 holds the exclusive lock, then block in Prepare
+DO $$
+DECLARE
+  lockers integer;
+BEGIN
+  LOOP
+	SELECT count(*) INTO lockers FROM pg_locks
+	WHERE relation = 'blocked_prepare_locked'::regclass
+	  AND mode = 'AccessExclusiveLock' AND granted
+	  AND pid <> pg_backend_pid();
+	EXIT WHEN lockers = 1;
+  END LOOP;
+END$$;
+SELECT * FROM blocked_prepare_locked;
+\else
+BEGIN;
+LOCK TABLE blocked_prepare_locked IN ACCESS EXCLUSIVE MODE;
+-- wait until client 0's Parse is queued behind the lock
+DO $$
+DECLARE
+  waiters integer;
+BEGIN
+  LOOP
+	SELECT count(*) INTO waiters FROM pg_locks
+	WHERE relation = 'blocked_prepare_locked'::regclass
+	  AND NOT granted;
+	EXIT WHEN waiters = 1;
+  END LOOP;
+END$$;
+INSERT INTO blocked_prepare_progress VALUES (:client_id);
+COMMIT;
+\endif
+}
+	});
+
+# Clean up
+$node->safe_psql('postgres',
+	'DROP TABLE blocked_prepare_locked, blocked_prepare_progress;');
+
 # done
 $node->safe_psql('postgres', 'DROP TABLESPACE regress_pgbench_tap_1_ts');
 $node->stop;
-- 
2.54.0



^ permalink  raw  reply  [nested|flat] 15+ messages in thread


end of thread, other threads:[~2026-09-17 11:06 UTC | newest]

Thread overview: 15+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-01-27 03:34 Pgbench: remove synchronous prepare Dmitrii Bondar <d.bondar@postgrespro.ru>
2026-03-02 11:35 ` Dmitrii Bondar <d.bondar@postgrespro.ru>
2026-03-16 07:45   ` Dmitrii Bondar <d.bondar@postgrespro.ru>
2026-04-06 06:54     ` lakshmi <lakshmigcdac@gmail.com>
2026-04-16 07:15       ` Dmitrii Bondar <d.bondar@postgrespro.ru>
2026-04-16 12:10         ` lakshmi <lakshmigcdac@gmail.com>
2026-04-26 17:01     ` Robert Haas <robertmhaas@gmail.com>
2026-04-28 09:29       ` Dmitrii Bondar <d.bondar@postgrespro.ru>
2026-04-29 21:46         ` Robert Haas <robertmhaas@gmail.com>
2026-05-05 07:48           ` Dmitrii Bondar <d.bondar@postgrespro.ru>
2026-05-27 12:39             ` Robert Haas <robertmhaas@gmail.com>
2026-06-24 03:26               ` Dmitrii Bondar <d.bondar@postgrespro.ru>
2026-07-15 11:03                 ` Бондарь Дмитрий <d.bondar@postgrespro.ru>
2026-09-10 13:09                   ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-09-17 11:06                     ` Dmitrii Bondar <d.bondar@postgrespro.ru>

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox