Hi hackers,
Attached is a v1 patch adding optional server-side automatic retry for
single-statement implicit transactions (autocommit mode) that fail due to
transient concurrency errors such as serialization failures (40001) and
deadlocks (40P01).
1. Problem & Motivation
=======================
Under REPEATABLE READ and SERIALIZABLE isolation levels (or high-contention
READ COMMITTED workloads encountering deadlocks or DDL lock conflicts),
applications must implement retry loops around transactions that fail with
SQLSTATE 40001 (serialization_failure) or 40P01 (deadlock_detected).
While multi-statement explicit transactions (BEGIN ... COMMIT) inherently
require client-side retry logic because intermediate application state may
depend on earlier queries in the transaction, standalone autocommit
statements contain their entire specification within a single wire-protocol
request. Retrying such statements directly inside the backend avoids a full
network round-trip, connection-pooler churn, and boilerplate retry wrappers
in simple clients, ORMs, and migration runners.
2. Design & Architecture
========================
The retry loop operates in `src/backend/tcop/postgres.c` around top-level
autocommit executions when `max_autocommit_retries > 0`:
a) Protocol Coverage:
- Simple Query Protocol (`exec_simple_query`):
Eligible when the query string parses into a single statement
(`list_length(parsetree_list) == 1`) and the backend is not initially in
an explicit transaction block (`!(xact_started && IsTransactionBlock())`).
On each retry attempt, the statement is re-parsed from the raw
`query_string` in a fresh `MessageContext` after aborting the failed
transaction.
- Extended Query Protocol (`exec_execute_message`):
Eligible when executing an uncompleted portal (`!portal->atStart` is
false, `max_rows <= 0` so no partial portal fetches occur), not inside an
explicit transaction block, with a single statement in `portal->stmts`.
Before execution begins, the bound `ParamListInfo` and portal context
(`stmt_text`, `prep_stmt_name`, `commandTag`, `cplan` or raw `stmts`) are
snapshot into a dedicated child memory context (`retry_cxt`) under
`TopMemoryContext`. If a retry is triggered after
`AbortCurrentTransaction()`
drops the failed unnamed portal, the portal is cleanly reconstructed and
re-defined from either the cached plan (`GetCachedPlan`) or a fresh copy
of the parse trees before re-running `PortalStart` and `PortalRun`.
b) Statement Classification & Eligible SQLSTATEs:
- Standard retryable statements (`AUTOCOMMIT_RETRY_DML`):
`SELECT`, `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `VALUES`, `TABLE`, and
`CALL` (provided the procedure does not issue internal transaction-ending
`COMMIT`/`ROLLBACK` commands, which disqualify retry as soon as the
original `TransactionId` advances/resets).
Retried on:
* `ERRCODE_T_R_SERIALIZATION_FAILURE` (`40001`)
* `ERRCODE_DEADLOCK_DETECTED` (`40P01`)
- Rollback-safe DDL (`AUTOCOMMIT_RETRY_DDL_SAFE`):
When `enable_ddl_autocommit_retry = on`, rollback-safe transactional DDL
statements (`CREATE`, `ALTER`, `DROP`, `COMMENT`, `SECURITY LABEL`,
`GRANT`/`REVOKE`, `IMPORT FOREIGN SCHEMA`, `REFRESH MATERIALIZED VIEW`
non-concurrently, `REINDEX` non-concurrently, `CLUSTER`, etc.) are also
eligible for retry. Non-transactional or non-rollback-safe commands
(`VACUUM`, `CREATE/DROP DATABASE`, `CREATE/DROP TABLESPACE`,
`ALTER SYSTEM`,
`REINDEX CONCURRENTLY`, `CREATE/DROP INDEX CONCURRENTLY`, `REFRESH
MATERIALIZED VIEW CONCURRENTLY`) and transaction control commands are
explicitly excluded (`AUTOCOMMIT_RETRY_NONE`).
In addition to `40001` and `40P01`, rollback-safe DDL is also retried on:
* `ERRCODE_LOCK_NOT_AVAILABLE` (`55P03`)
* `ERRCODE_QUERY_CANCELED` (`57014`) when caused by `lock_timeout`
(explicitly excluding user `SIGINT` cancels and `statement_timeout`).
c) Speculative Wire-Protocol Output Buffering (`pqcomm.c`):
A `SELECT` or `INSERT ... RETURNING` statement may stream `RowDescription`
and partial `DataRow` messages into `PqSendBuffer` before encountering a
serialization failure or deadlock part-way through execution.
To prevent corrupting the frontend wire stream on retry:
- `pq_begin_retry_Holdoff()` pins all outgoing protocol bytes in
`PqSendBuffer` (expanding the buffer dynamically up to an 8 MB ceiling,
`PQ_RETRY_BUFFER_MAX = 8 * 1024 * 1024`, before falling back to flushing
to the socket).
- If the statement fails with a retryable error and the buffer has not
flushed (`!pq_retry_Holdoff_flushed()`), `pq_discard_retry_Holdoff()`
rewinds `PqSendPointer` to discard the speculative `RowDescription` /
`DataRow` bytes, and `FlushErrorState()` clears the pending error before
sleeping and retrying.
- If the output exceeded 8 MB and already flushed bytes to the wire, retry
is suppressed and the error is reported normally.
d) Backoff, Jitter, and `statement_timeout`:
- Between retry attempts, the backend aborts the failed transaction so locks
and snapshots are released, then sleeps using `WaitLatch(MyLatch,
WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, sleep_ms,
WAIT_EVENT_AUTOCOMMIT_RETRY_BACKOFF)`.
- The backoff uses exponential growth with full uniform jitter via
`pg_prng_uint64_range(&pg_global_prng_state, 0, cap_ms)`:
`cap_ms = min(autocommit_retry_backoff_max_ms,
autocommit_retry_backoff_base_ms * 2^(attempt - 1))`.
- `statement_timeout` is re-armed per attempt (`enable_statement_timeout()`).
3. New GUC Parameters
=====================
- `max_autocommit_retries` (integer, default `0`, range `0..100`,
`PGC_USERSET`):
Maximum number of retry attempts for autocommit statements failing with
transient concurrency errors (`0` disables automatic retry).
- `enable_ddl_autocommit_retry` (bool, default `false`, `PGC_USERSET`):
Enables automatic retry for rollback-safe DDL statements on `40001`, `40P01`,
`55P03` (`lock_not_available`), and `lock_timeout` (`57014`).
- `autocommit_retry_backoff_base_ms` (integer, default `5`, range
`0..10000`, `PGC_USERSET`):
Base delay in milliseconds for exponential backoff with jitter
between retries.
- `autocommit_retry_backoff_max_ms` (integer, default `1000`, range
`1..60000`, `PGC_USERSET`):
Maximum delay in milliseconds for exponential backoff between retries.
- `log_autocommit_retries` (bool, default `false`, `PGC_USERSET`):
Emits a `LOG` message for each automatic retry attempt including the attempt
number, `max_autocommit_retries`, SQLSTATE, sleep duration, and
statement text.
4. Verification & Testing
=========================
- Isolation test suite (`src/test/isolation/specs/autocommit-retry.spec`):
10 permutations covering:
* Read-write skew (`40001`) under `default_transaction_isolation =
'repeatable read'`
with `max_autocommit_retries = 0` (fails) vs `max_autocommit_retries = 3`
(transparently retries and commits updated balance).
* CTE (`WITH ... UPDATE`) two-session mutual deadlock (`40P01`) where the
deadlock victim automatically retries and succeeds.
* Rollback-safe DDL (`ALTER TABLE ... ADD COLUMN`) failing with `lock_timeout`
(`57014`) when blocked by an open transaction, then retrying after the
blocker releases the lock (`enable_ddl_autocommit_retry = on` vs `off`).
* `SELECT ... FOR SHARE` / `FOR UPDATE` conflict retry.
* Explicit `BEGIN ... COMMIT` transaction block verification (never
auto-retried).
* Multi-statement Simple Query (`SELECT 1; UPDATE ...`) verification (never
auto-retried).
* `CALL` procedure with internal `COMMIT` verification (never auto-retried
once internal transaction control occurs).
- Regression tests (`src/test/regress/sql/psql.sql`):
* Extended Query protocol (`\bind`) retry on `40001`.
* Speculative `DataRow` output buffer discard (`pq_discard_retry_Holdoff()`)
when a multi-row `SELECT` fails mid-stream with `40001` on the first
attempt and succeeds cleanly on the retry attempt without duplicate rows.
Base commit: e75fcc9867fdfe295957ad5c9a98cbe2e1b4c5c3
GitHub branch:
https://github.com/postsql/postgres/tree/auto-retry-serialization-errors
CI run: https://github.com/postsql/postgres/actions/runs/39452018256
Regards,
Hannu Krosing
From 57e9874973b29f435a03262f815f24f11706cf2b Mon Sep 17 00:00:00 2001
From: Hannu Krosing <[email protected]>
Date: Mon, 12 Oct 2026 20:19:16 +0000
Subject: [PATCH v1 1/1] Add automatic retry for autocommit statements on
transient errors
Allow standalone single-statement queries outside an explicit transaction
block (in both Simple Query and Extended Query protocols) to automatically
retry on transient concurrency errors when max_autocommit_retries > 0:
- DML, SELECT, and CALL statements retry on serialization failure (40001)
and deadlock detected (40P01), provided CALL has not executed an internal
COMMIT or ROLLBACK prior to failing.
- Rollback-safe DDL and table-level utility commands optionally retry on
lock_timeout (55P03), statement_timeout (57014), 40001, and 40P01 when
enable_ddl_autocommit_retry = on.
- Buffer uncommitted protocol output (RowDescription, DataRow, NoticeResponse)
in PqSendBuffer (up to 8 MB) and discard it if an attempt fails and is
retried, disabling retry if the buffer already flushed to the socket.
- Sleep between attempts using interruptible exponential backoff with jitter
configured by autocommit_retry_backoff_base_ms and
autocommit_retry_backoff_max_ms, with optional server logging controlled
by log_autocommit_retries.
---
src/backend/libpq/pqcomm.c | 98 ++
src/backend/tcop/postgres.c | 1025 +++++++++++++----
src/backend/utils/misc/guc_parameters.dat | 39 +
src/backend/utils/misc/postgresql.conf.sample | 5 +
src/include/libpq/libpq.h | 5 +
src/include/tcop/tcopprot.h | 6 +
.../isolation/expected/autocommit-retry.out | 258 +++++
src/test/isolation/isolation_schedule | 1 +
.../isolation/specs/autocommit-retry.spec | 157 +++
src/test/regress/expected/psql.out | 52 +
src/test/regress/expected/sysviews.out | 3 +-
src/test/regress/sql/psql.sql | 30 +
12 files changed, 1434 insertions(+), 245 deletions(-)
create mode 100644 src/test/isolation/expected/autocommit-retry.out
create mode 100644 src/test/isolation/specs/autocommit-retry.spec
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 3704d121003..43bbd186431 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -71,6 +71,7 @@
#include "common/ip.h"
#include "libpq/libpq.h"
+#include "libpq/protocol.h"
#include "miscadmin.h"
#include "port/pg_bswap.h"
#include "postmaster/postmaster.h"
@@ -116,12 +117,18 @@ static List *sock_paths = NIL;
#define PQ_SEND_BUFFER_SIZE 8192
#define PQ_RECV_BUFFER_SIZE 8192
+#define PQ_MAX_RETRY_BUFFER_SIZE (8 * 1024 * 1024)
static char *PqSendBuffer;
static int PqSendBufferSize; /* Size send buffer */
static size_t PqSendPointer; /* Next index to store a byte in PqSendBuffer */
static size_t PqSendStart; /* Next index to send a byte in PqSendBuffer */
+/* State for buffering output during retryable autocommit statements */
+static bool PqRetryBuffering = false;
+static bool PqRetryBufferFlushed = false;
+static size_t PqSendRetryStart = 0;
+
static char PqRecvBuffer[PQ_RECV_BUFFER_SIZE];
static int PqRecvPointer; /* Next index to read a byte from PqRecvBuffer */
static int PqRecvLength; /* End of data available in PqRecvBuffer */
@@ -333,6 +340,9 @@ socket_comm_reset(void)
{
/* Do not throw away pending data, but do reset the busy flag */
PqCommBusy = false;
+ PqRetryBuffering = false;
+ PqRetryBufferFlushed = false;
+ PqSendRetryStart = 0;
}
/* --------------------------------
@@ -1278,6 +1288,34 @@ internal_putbytes(const void *b, size_t len)
while (len > 0)
{
+ /*
+ * If retry buffering is active and we haven't flushed yet, expand
+ * PqSendBuffer up to PQ_MAX_RETRY_BUFFER_SIZE instead of flushing
+ * uncommitted output to the client socket.
+ */
+ if (PqRetryBuffering && !PqRetryBufferFlushed &&
+ PqSendPointer + len > PqSendBufferSize)
+ {
+ size_t required = PqSendPointer + len;
+
+ if (required <= PQ_MAX_RETRY_BUFFER_SIZE)
+ {
+ size_t newsize = PqSendBufferSize;
+
+ while (newsize < required)
+ newsize *= 2;
+ if (newsize > PQ_MAX_RETRY_BUFFER_SIZE)
+ newsize = PQ_MAX_RETRY_BUFFER_SIZE;
+ PqSendBuffer = repalloc(PqSendBuffer, newsize);
+ PqSendBufferSize = (int) newsize;
+ }
+ else
+ {
+ /* Buffer limit exceeded; allow flush and mark un-retryable */
+ PqRetryBufferFlushed = true;
+ }
+ }
+
/* If buffer is full, then flush it out */
if (PqSendPointer >= PqSendBufferSize)
{
@@ -1329,6 +1367,14 @@ socket_flush(void)
/* No-op if reentrant call */
if (PqCommBusy)
return 0;
+
+ /*
+ * Defer explicit flushes (e.g., from send_message_to_frontend on NOTICE)
+ * while buffering uncommitted output for a retryable statement.
+ */
+ if (PqRetryBuffering && !PqRetryBufferFlushed)
+ return 0;
+
PqCommBusy = true;
socket_set_nonblocking(false);
res = internal_flush();
@@ -1364,6 +1410,9 @@ internal_flush_buffer(const char *buf, size_t *start, size_t *end)
const char *bufptr = buf + *start;
const char *bufend = buf + *end;
+ if (PqRetryBuffering && bufptr < bufend)
+ PqRetryBufferFlushed = true;
+
while (bufptr < bufend)
{
ssize_t r;
@@ -1442,6 +1491,10 @@ socket_flush_if_writable(void)
if (PqCommBusy)
return 0;
+ /* Defer flush while buffering uncommitted output for retry */
+ if (PqRetryBuffering && !PqRetryBufferFlushed)
+ return 0;
+
/* Temporarily put the socket into non-blocking mode */
socket_set_nonblocking(true);
@@ -1461,6 +1514,51 @@ socket_is_send_pending(void)
return (PqSendStart < PqSendPointer);
}
+/* --------------------------------
+ * Autocommit retry output buffering support
+ * --------------------------------
+ */
+void
+pq_begin_retry_buffer(void)
+{
+ PqRetryBuffering = true;
+ PqRetryBufferFlushed = false;
+ PqSendRetryStart = PqSendPointer;
+}
+
+void
+pq_end_retry_buffer(void)
+{
+ PqRetryBuffering = false;
+ PqRetryBufferFlushed = false;
+ PqSendRetryStart = 0;
+}
+
+bool
+pq_retry_buffer_flushed(void)
+{
+ return PqRetryBufferFlushed;
+}
+
+void
+pq_discard_retry_buffer(void)
+{
+ Assert(PqRetryBuffering);
+ Assert(!PqRetryBufferFlushed);
+ Assert(PqSendStart <= PqSendRetryStart);
+ Assert(PqSendRetryStart <= PqSendPointer);
+ PqSendPointer = PqSendRetryStart;
+}
+
+bool
+pq_is_next_msg_sync(void)
+{
+ if (PqRecvLength - PqRecvPointer >= 5 &&
+ PqRecvBuffer[PqRecvPointer] == PqMsg_Sync)
+ return true;
+ return false;
+}
+
/* --------------------------------
* Message-level I/O routines begin here.
* --------------------------------
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index b6bdfe213fe..aa9acdd6df2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -110,6 +110,13 @@ int client_connection_check_interval = 0;
/* flags for non-system relation kinds to restrict use */
int restrict_nonsystem_relation_kind;
+/* GUC variables for automatic retry of autocommit statements */
+int max_autocommit_retries = 0;
+bool enable_ddl_autocommit_retry = false;
+int autocommit_retry_backoff_base_ms = 10;
+int autocommit_retry_backoff_max_ms = 1000;
+bool log_autocommit_retries = false;
+
/*
* Include signal sender PID/UID in the server log when available
* (SA_SIGINFO). The caller must supply the already-captured pid and uid
@@ -132,6 +139,14 @@ typedef struct BindParamCbData
const char *paramval; /* textual input string, if available */
} BindParamCbData;
+/* statement classification for autocommit auto-retry */
+typedef enum AutocommitRetryClass
+{
+ AUTOCOMMIT_RETRY_NONE = 0,
+ AUTOCOMMIT_RETRY_DML, /* SELECT, INSERT, UPDATE, DELETE, MERGE, CALL */
+ AUTOCOMMIT_RETRY_DDL, /* Rollback-safe DDL / utility statements */
+} AutocommitRetryClass;
+
/* ----------------
* private variables
* ----------------
@@ -143,6 +158,12 @@ typedef struct BindParamCbData
*/
static bool xact_started = false;
+/*
+ * True if the most recent ERRCODE_QUERY_CANCELED was caused by
+ * statement_timeout rather than a user cancel request.
+ */
+static bool stmt_timeout_canceled = false;
+
/*
* Flag to indicate that we are doing the outer loop's read-from-client,
* as opposed to any random read from client that might happen within
@@ -193,6 +214,12 @@ static bool IsTransactionExitStmt(Node *parsetree);
static bool IsTransactionExitStmtList(List *pstmts);
static bool IsTransactionStmtList(List *pstmts);
static void drop_unnamed_stmt(void);
+static AutocommitRetryClass classify_autocommit_retry_stmt(Node *stmt);
+static bool is_autocommit_error_retryable(int sqlerrcode,
+ AutocommitRetryClass retry_class);
+static void autocommit_retry_sleep(int retry_attempt, int sqlerrcode,
+ const char *errmsg_str,
+ const char *query_string);
static void ProcessRecoveryConflictInterrupts(void);
static void ProcessRecoveryConflictInterrupt(RecoveryConflictReason reason);
static void report_recovery_conflict(RecoveryConflictReason reason);
@@ -1119,15 +1146,15 @@ exec_simple_query(const char *query_string)
foreach(parsetree_item, parsetree_list)
{
RawStmt *parsetree = lfirst_node(RawStmt, parsetree_item);
- bool snapshot_set = false;
CommandTag commandTag;
QueryCompletion qc;
MemoryContext per_parsetree_context = NULL;
- List *querytree_list,
- *plantree_list;
- Portal portal;
- DestReceiver *receiver;
- int16 format;
+ MemoryContext retry_cxt = NULL;
+ AutocommitRetryClass retry_class = AUTOCOMMIT_RETRY_NONE;
+ bool can_retry = false;
+ volatile int retry_attempt = 0;
+ volatile bool retry_done = false;
+ volatile LocalTransactionId initial_lxid = InvalidLocalTransactionId;
const char *cmdtagname;
size_t cmdtaglen;
@@ -1175,193 +1202,327 @@ exec_simple_query(const char *query_string)
if (use_implicit_block)
BeginImplicitTransactionBlock();
- /* If we got a cancel signal in parsing or prior command, quit */
- CHECK_FOR_INTERRUPTS();
-
/*
- * Set up a snapshot if parse analysis/planning will need one.
+ * Determine whether this statement is eligible for automatic retry
+ * in autocommit mode. Only single-statement query strings outside an
+ * explicit transaction block qualify.
*/
- if (analyze_requires_snapshot(parsetree))
+ if (!use_implicit_block && !IsTransactionBlock())
{
- PushActiveSnapshot(GetTransactionSnapshot());
- snapshot_set = true;
+ retry_class = classify_autocommit_retry_stmt((Node *) parsetree);
+ can_retry = (max_autocommit_retries > 0) &&
+ (retry_class == AUTOCOMMIT_RETRY_DML ||
+ (retry_class == AUTOCOMMIT_RETRY_DDL && enable_ddl_autocommit_retry));
}
- /*
- * OK to analyze, rewrite, and plan this query.
- *
- * Switch to appropriate context for constructing query and plan trees
- * (these can't be in the transaction context, as that will get reset
- * when the command is COMMIT/ROLLBACK). If we have multiple
- * parsetrees, we use a separate context for each one, so that we can
- * free that memory before moving on to the next one. But for the
- * last (or only) parsetree, just use MessageContext, which will be
- * reset shortly after completion anyway. In event of an error, the
- * per_parsetree_context will be deleted when MessageContext is reset.
- */
- if (lnext(parsetree_list, parsetree_item) != NULL)
+ if (can_retry)
+ retry_cxt = AllocSetContextCreate(MessageContext,
+ "autocommit retry context",
+ ALLOCSET_DEFAULT_SIZES);
+
+ for (;;)
{
- per_parsetree_context =
- AllocSetContextCreate(MessageContext,
- "per-parsetree message context",
- ALLOCSET_DEFAULT_SIZES);
- oldcontext = MemoryContextSwitchTo(per_parsetree_context);
- }
- else
- oldcontext = MemoryContextSwitchTo(MessageContext);
+ int retry_errcode = 0;
+ char *retry_errmsg = NULL;
- querytree_list = pg_analyze_and_rewrite_fixedparams(parsetree, query_string,
- NULL, 0, NULL);
+ stmt_timeout_canceled = false;
- plantree_list = pg_plan_queries(querytree_list, query_string,
- CURSOR_OPT_PARALLEL_OK, NULL);
+ if (retry_attempt > 0)
+ {
+ SetCurrentStatementStartTimestamp();
+ start_xact_command();
+ BeginCommand(commandTag, dest);
+ }
- /*
- * Done with the snapshot used for parsing/planning.
- *
- * While it looks promising to reuse the same snapshot for query
- * execution (at least for simple protocol), unfortunately it causes
- * execution to use a snapshot that has been acquired before locking
- * any of the tables mentioned in the query. This creates user-
- * visible anomalies, so refrain. Refer to
- * https://postgr.es/m/flat/[email protected] for details.
- */
- if (snapshot_set)
- PopActiveSnapshot();
+ initial_lxid = MyProc ? MyProc->vxid.lxid : InvalidLocalTransactionId;
- /* If we got a cancel signal in analysis or planning, quit */
- CHECK_FOR_INTERRUPTS();
+ if (can_retry && whereToSendOutput == DestRemote)
+ pq_begin_retry_buffer();
- /*
- * Create unnamed portal to run the query or queries in. If there
- * already is one, silently drop it.
- */
- portal = CreatePortal("", true, true);
- /* Don't display the portal in pg_cursors */
- portal->visible = false;
+ PG_TRY();
+ {
+ RawStmt *cur_parsetree = parsetree;
+ bool snapshot_set = false;
+ List *querytree_list,
+ *plantree_list;
+ Portal portal;
+ DestReceiver *receiver;
+ int16 format;
+
+ /* If we got a cancel signal in parsing or prior command, quit */
+ CHECK_FOR_INTERRUPTS();
- /*
- * We don't have to copy anything into the portal, because everything
- * we are passing here is in MessageContext or the
- * per_parsetree_context, and so will outlive the portal anyway.
- */
- PortalDefineQuery(portal,
- NULL,
- query_string,
- commandTag,
- plantree_list,
- NULL);
+ /*
+ * Set up a snapshot if parse analysis/planning will need one.
+ */
+ if (analyze_requires_snapshot(cur_parsetree))
+ {
+ PushActiveSnapshot(GetTransactionSnapshot());
+ snapshot_set = true;
+ }
- /*
- * Start the portal. No parameters here.
- */
- PortalStart(portal, NULL, 0, InvalidSnapshot);
+ /*
+ * OK to analyze, rewrite, and plan this query.
+ *
+ * Switch to appropriate context for constructing query and
+ * plan trees (these can't be in the transaction context, as
+ * that will get reset when the command is COMMIT/ROLLBACK).
+ * When autocommit retry is active, we allocate in retry_cxt
+ * and work on a deep copy of parsetree because parse analysis
+ * and utility execution can mutate raw parse nodes in-place.
+ */
+ if (retry_cxt != NULL)
+ {
+ oldcontext = MemoryContextSwitchTo(retry_cxt);
+ cur_parsetree = copyObject(parsetree);
+ }
+ else if (lnext(parsetree_list, parsetree_item) != NULL)
+ {
+ per_parsetree_context =
+ AllocSetContextCreate(MessageContext,
+ "per-parsetree message context",
+ ALLOCSET_DEFAULT_SIZES);
+ oldcontext = MemoryContextSwitchTo(per_parsetree_context);
+ }
+ else
+ oldcontext = MemoryContextSwitchTo(MessageContext);
- /*
- * Select the appropriate output format: text unless we are doing a
- * FETCH from a binary cursor. (Pretty grotty to have to do this here
- * --- but it avoids grottiness in other places. Ah, the joys of
- * backward compatibility...)
- */
- format = 0; /* TEXT is default */
- if (IsA(parsetree->stmt, FetchStmt))
- {
- FetchStmt *stmt = (FetchStmt *) parsetree->stmt;
+ querytree_list = pg_analyze_and_rewrite_fixedparams(cur_parsetree,
+ query_string,
+ NULL, 0, NULL);
- if (!stmt->ismove)
- {
- Portal fportal = GetPortalByName(stmt->portalname);
+ plantree_list = pg_plan_queries(querytree_list, query_string,
+ CURSOR_OPT_PARALLEL_OK, NULL);
- if (PortalIsValid(fportal) &&
- (fportal->cursorOptions & CURSOR_OPT_BINARY))
- format = 1; /* BINARY */
- }
- }
- PortalSetResultFormat(portal, 1, &format);
+ /*
+ * Done with the snapshot used for parsing/planning.
+ *
+ * While it looks promising to reuse the same snapshot for
+ * query execution (at least for simple protocol),
+ * unfortunately it causes execution to use a snapshot that
+ * has been acquired before locking any of the tables
+ * mentioned in the query. This creates user-visible
+ * anomalies, so refrain. Refer to
+ * https://postgr.es/m/flat/[email protected] for
+ * details.
+ */
+ if (snapshot_set)
+ PopActiveSnapshot();
- /*
- * Now we can create the destination receiver object.
- */
- receiver = CreateDestReceiver(dest);
- if (dest == DestRemote)
- SetRemoteDestReceiverParams(receiver, portal);
+ /* If we got a cancel signal in analysis or planning, quit */
+ CHECK_FOR_INTERRUPTS();
- /*
- * Switch back to transaction context for execution.
- */
- MemoryContextSwitchTo(oldcontext);
+ /*
+ * Create unnamed portal to run the query or queries in. If
+ * there already is one, silently drop it.
+ */
+ portal = CreatePortal("", true, true);
+ /* Don't display the portal in pg_cursors */
+ portal->visible = false;
- /*
- * Run the portal to completion, and then drop it (and the receiver).
- */
- (void) PortalRun(portal,
- FETCH_ALL,
- true, /* always top level */
- receiver,
- receiver,
- &qc);
+ /*
+ * We don't have to copy anything into the portal, because
+ * everything we are passing here is in MessageContext,
+ * per_parsetree_context, or retry_cxt, and so will outlive
+ * the portal anyway.
+ */
+ PortalDefineQuery(portal,
+ NULL,
+ query_string,
+ commandTag,
+ plantree_list,
+ NULL);
- receiver->rDestroy(receiver);
+ /*
+ * Start the portal. No parameters here.
+ */
+ PortalStart(portal, NULL, 0, InvalidSnapshot);
- PortalDrop(portal, false);
+ /*
+ * Select the appropriate output format: text unless we are
+ * doing a FETCH from a binary cursor. (Pretty grotty to have
+ * to do this here --- but it avoids grottiness in other
+ * places. Ah, the joys of backward compatibility...)
+ */
+ format = 0; /* TEXT is default */
+ if (IsA(cur_parsetree->stmt, FetchStmt))
+ {
+ FetchStmt *stmt = (FetchStmt *) cur_parsetree->stmt;
- if (lnext(parsetree_list, parsetree_item) == NULL)
- {
- /*
- * If this is the last parsetree of the query string, close down
- * transaction statement before reporting command-complete. This
- * is so that any end-of-transaction errors are reported before
- * the command-complete message is issued, to avoid confusing
- * clients who will expect either a command-complete message or an
- * error, not one and then the other. Also, if we're using an
- * implicit transaction block, we must close that out first.
- */
- if (use_implicit_block)
- EndImplicitTransactionBlock();
- finish_xact_command();
- }
- else if (IsA(parsetree->stmt, TransactionStmt))
- {
- /*
- * If this was a transaction control statement, commit it. We will
- * start a new xact command for the next command.
- */
- finish_xact_command();
- }
- else
- {
- /*
- * We had better not see XACT_FLAGS_NEEDIMMEDIATECOMMIT set if
- * we're not calling finish_xact_command(). (The implicit
- * transaction block should have prevented it from getting set.)
- */
- Assert(!(MyXactFlags & XACT_FLAGS_NEEDIMMEDIATECOMMIT));
+ if (!stmt->ismove)
+ {
+ Portal fportal = GetPortalByName(stmt->portalname);
- /*
- * We need a CommandCounterIncrement after every query, except
- * those that start or end a transaction block.
- */
- CommandCounterIncrement();
+ if (PortalIsValid(fportal) &&
+ (fportal->cursorOptions & CURSOR_OPT_BINARY))
+ format = 1; /* BINARY */
+ }
+ }
+ PortalSetResultFormat(portal, 1, &format);
- /*
- * Disable statement timeout between queries of a multi-query
- * string, so that the timeout applies separately to each query.
- * (Our next loop iteration will start a fresh timeout.)
- */
- disable_statement_timeout();
- }
+ /*
+ * Now we can create the destination receiver object.
+ */
+ receiver = CreateDestReceiver(dest);
+ if (dest == DestRemote)
+ SetRemoteDestReceiverParams(receiver, portal);
- /*
- * Tell client that we're done with this query. Note we emit exactly
- * one EndCommand report for each raw parsetree, thus one for each SQL
- * command the client sent, regardless of rewriting. (But a command
- * aborted by error will not send an EndCommand report at all.)
- */
- EndCommand(&qc, dest, false);
+ /*
+ * Switch back to transaction context for execution.
+ */
+ MemoryContextSwitchTo(oldcontext);
+
+ /*
+ * Run the portal to completion, and then drop it (and the
+ * receiver).
+ */
+ (void) PortalRun(portal,
+ FETCH_ALL,
+ true, /* always top level */
+ receiver,
+ receiver,
+ &qc);
+
+ receiver->rDestroy(receiver);
+
+ PortalDrop(portal, false);
+
+ if (lnext(parsetree_list, parsetree_item) == NULL)
+ {
+ /*
+ * If this is the last parsetree of the query string,
+ * close down transaction statement before reporting
+ * command-complete. This is so that any
+ * end-of-transaction errors are reported before the
+ * command-complete message is issued, to avoid confusing
+ * clients who will expect either a command-complete
+ * message or an error, not one and then the other. Also,
+ * if we're using an implicit transaction block, we must
+ * close that out first.
+ */
+ if (use_implicit_block)
+ EndImplicitTransactionBlock();
+ finish_xact_command();
+ }
+ else if (IsA(cur_parsetree->stmt, TransactionStmt))
+ {
+ /*
+ * If this was a transaction control statement, commit it.
+ * We will start a new xact command for the next command.
+ */
+ finish_xact_command();
+ }
+ else
+ {
+ /*
+ * We had better not see XACT_FLAGS_NEEDIMMEDIATECOMMIT
+ * set if we're not calling finish_xact_command(). (The
+ * implicit transaction block should have prevented it
+ * from getting set.)
+ */
+ Assert(!(MyXactFlags & XACT_FLAGS_NEEDIMMEDIATECOMMIT));
+
+ /*
+ * We need a CommandCounterIncrement after every query,
+ * except those that start or end a transaction block.
+ */
+ CommandCounterIncrement();
+
+ /*
+ * Disable statement timeout between queries of a
+ * multi-query string, so that the timeout applies
+ * separately to each query. (Our next loop iteration
+ * will start a fresh timeout.)
+ */
+ disable_statement_timeout();
+ }
- /* Now we may drop the per-parsetree context, if one was created. */
- if (per_parsetree_context)
- MemoryContextDelete(per_parsetree_context);
+ if (can_retry && whereToSendOutput == DestRemote)
+ pq_end_retry_buffer();
+
+ /*
+ * Tell client that we're done with this query. Note we emit
+ * exactly one EndCommand report for each raw parsetree, thus
+ * one for each SQL command the client sent, regardless of
+ * rewriting. (But a command aborted by error will not send an
+ * EndCommand report at all.)
+ */
+ EndCommand(&qc, dest, false);
+
+ /*
+ * Now we may drop the per-parsetree or retry context, if one
+ * was created.
+ */
+ if (per_parsetree_context)
+ MemoryContextDelete(per_parsetree_context);
+ if (retry_cxt)
+ MemoryContextDelete(retry_cxt);
+
+ retry_done = true;
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+ int errcode_val = geterrcode();
+
+ if (!can_retry ||
+ retry_attempt >= max_autocommit_retries ||
+ pq_retry_buffer_flushed() ||
+ IsTransactionBlock() ||
+ (MyProc != NULL && MyProc->vxid.lxid != initial_lxid) ||
+ !is_autocommit_error_retryable(errcode_val, retry_class))
+ {
+ pq_end_retry_buffer();
+ PG_RE_THROW();
+ }
+
+ HOLD_INTERRUPTS();
+ disable_all_timeouts(false);
+ QueryCancelPending = false;
+ stmt_timeout_canceled = false;
+
+ /*
+ * Discard any uncommitted RowDescription, DataRow, or Notice
+ * messages accumulated in PqSendBuffer during the failed
+ * attempt.
+ */
+ pq_discard_retry_buffer();
+ pq_end_retry_buffer();
+
+ MemoryContextSwitchTo(MessageContext);
+ edata = CopyErrorData();
+ retry_errcode = edata->sqlerrcode;
+ retry_errmsg = pstrdup(edata->message ? edata->message : "");
+ FreeErrorData(edata);
+ FlushErrorState();
+
+ AbortCurrentTransaction();
+ if (am_walsender)
+ WalSndErrorCleanup();
+ PortalErrorCleanup();
+ if (MyReplicationSlot != NULL)
+ ReplicationSlotRelease();
+ ReplicationSlotCleanup(false);
+ jit_reset_after_error();
+
+ xact_started = false;
+
+ MemoryContextSwitchTo(MessageContext);
+ MemoryContextReset(retry_cxt);
+
+ RESUME_INTERRUPTS();
+ }
+ PG_END_TRY();
+
+ if (retry_done)
+ break;
+
+ retry_attempt++;
+ autocommit_retry_sleep(retry_attempt, retry_errcode,
+ retry_errmsg, query_string);
+ if (retry_errmsg)
+ pfree(retry_errmsg);
+ }
} /* end loop over parsetrees */
/*
@@ -2151,13 +2312,21 @@ static void
exec_execute_message(const char *portal_name, long max_rows)
{
CommandDest dest;
- DestReceiver *receiver;
Portal portal;
- bool completed;
QueryCompletion qc;
const char *sourceText;
const char *prepStmtName;
+ const char *raw_stmt_name = NULL;
ParamListInfo portalParams;
+ CachedPlanSource *psrc = NULL;
+ ParamListInfo saved_params = NULL;
+ int16 *saved_rformats = NULL;
+ int saved_num_rformats = 0;
+ AutocommitRetryClass retry_class = AUTOCOMMIT_RETRY_NONE;
+ bool can_retry = false;
+ volatile int retry_attempt = 0;
+ volatile bool retry_done = false;
+ volatile LocalTransactionId initial_lxid = InvalidLocalTransactionId;
bool save_log_statement_stats = log_statement_stats;
bool is_xact_command;
bool execute_is_fetch;
@@ -2196,13 +2365,14 @@ exec_execute_message(const char *portal_name, long max_rows)
/*
* We must copy the sourceText and prepStmtName into MessageContext in
- * case the portal is destroyed during finish_xact_command. We do not
- * make a copy of the portalParams though, preferring to just not print
- * them in that case.
+ * case the portal is destroyed during finish_xact_command.
*/
sourceText = pstrdup(portal->sourceText);
if (portal->prepStmtName)
- prepStmtName = pstrdup(portal->prepStmtName);
+ {
+ raw_stmt_name = pstrdup(portal->prepStmtName);
+ prepStmtName = raw_stmt_name;
+ }
else
prepStmtName = "<unnamed>";
portalParams = portal->portalParams;
@@ -2245,14 +2415,6 @@ exec_execute_message(const char *portal_name, long max_rows)
BeginCommand(portal->commandTag, dest);
- /*
- * Create dest receiver in MessageContext (we don't want it in transaction
- * context, because that may get deleted if portal contains VACUUM).
- */
- receiver = CreateDestReceiver(dest);
- if (dest == DestRemoteExecute)
- SetRemoteDestReceiverParams(receiver, portal);
-
/*
* Ensure we are in a transaction command (this should normally be the
* case already due to prior BIND).
@@ -2300,91 +2462,297 @@ exec_execute_message(const char *portal_name, long max_rows)
errmsg("current transaction is aborted, "
"commands ignored until end of transaction block")));
- /* Check for cancel signal before we start execution */
- CHECK_FOR_INTERRUPTS();
+ if (max_rows <= 0)
+ max_rows = FETCH_ALL;
/*
- * Okay to run the portal. Set the error callback so that parameters are
- * logged. The parameters must have been saved during the bind phase.
+ * Determine whether this portal execution is eligible for automatic retry
+ * in autocommit mode.
*/
- params_data.portalName = portal->name;
- params_data.params = portalParams;
- params_errcxt.previous = error_context_stack;
- params_errcxt.callback = ParamsErrorCallback;
- params_errcxt.arg = ¶ms_data;
- error_context_stack = ¶ms_errcxt;
+ if (!execute_is_fetch &&
+ max_rows == FETCH_ALL &&
+ !IsTransactionBlock() &&
+ !(MyXactFlags & XACT_FLAGS_PIPELINING) &&
+ list_length(portal->stmts) == 1)
+ {
+ if (raw_stmt_name != NULL && raw_stmt_name[0] != '\0')
+ {
+ PreparedStatement *pstmt = FetchPreparedStatement(raw_stmt_name, false);
- if (max_rows <= 0)
- max_rows = FETCH_ALL;
+ if (pstmt != NULL)
+ psrc = pstmt->plansource;
+ }
+ else
+ psrc = unnamed_stmt_psrc;
- completed = PortalRun(portal,
- max_rows,
- true, /* always top level */
- receiver,
- receiver,
- &qc);
+ if (psrc != NULL)
+ {
+ retry_class = (psrc->raw_parse_tree != NULL) ?
+ classify_autocommit_retry_stmt((Node *) psrc->raw_parse_tree) :
+ classify_autocommit_retry_stmt((Node *) linitial(portal->stmts));
+ can_retry = (max_autocommit_retries > 0) &&
+ (retry_class == AUTOCOMMIT_RETRY_DML ||
+ (retry_class == AUTOCOMMIT_RETRY_DDL && enable_ddl_autocommit_retry));
+ }
+ }
- receiver->rDestroy(receiver);
+ if (can_retry)
+ {
+ MemoryContext oldcxt = MemoryContextSwitchTo(MessageContext);
- /* Done executing; remove the params error callback */
- error_context_stack = error_context_stack->previous;
+ if (portalParams != NULL)
+ {
+ saved_params = copyParamList(portalParams);
+ if (portalParams->paramValuesStr != NULL)
+ saved_params->paramValuesStr = pstrdup(portalParams->paramValuesStr);
+ }
+ if (portal->tupDesc != NULL && portal->formats != NULL)
+ {
+ saved_num_rformats = portal->tupDesc->natts;
+ saved_rformats = palloc_array(int16, saved_num_rformats);
+ memcpy(saved_rformats, portal->formats,
+ saved_num_rformats * sizeof(int16));
+ }
+ MemoryContextSwitchTo(oldcxt);
+ }
- if (completed)
+ for (;;)
{
- if (is_xact_command || (MyXactFlags & XACT_FLAGS_NEEDIMMEDIATECOMMIT))
+ int retry_errcode = 0;
+ char *retry_errmsg = NULL;
+
+ stmt_timeout_canceled = false;
+
+ if (can_retry && whereToSendOutput == DestRemote)
+ pq_begin_retry_buffer();
+
+ PG_TRY();
{
+ DestReceiver *receiver;
+ MemoryContext oldcxt;
+ bool completed;
+
+ if (retry_attempt > 0)
+ {
+ ParamListInfo retry_params = NULL;
+ CachedPlan *cplan;
+ bool snapshot_set = false;
+ char *portal_query_string;
+ char *portal_stmt_name;
+
+ SetCurrentStatementStartTimestamp();
+ start_xact_command();
+ BeginCommand(psrc->commandTag, dest);
+
+ if (portal_name[0] == '\0')
+ portal = CreatePortal(portal_name, true, true);
+ else
+ portal = CreatePortal(portal_name, false, false);
+
+ oldcxt = MemoryContextSwitchTo(portal->portalContext);
+ portal_query_string = pstrdup(psrc->query_string);
+ portal_stmt_name = (raw_stmt_name && raw_stmt_name[0] != '\0') ?
+ pstrdup(raw_stmt_name) : NULL;
+ if (saved_params != NULL)
+ {
+ retry_params = copyParamList(saved_params);
+ if (saved_params->paramValuesStr != NULL)
+ retry_params->paramValuesStr = pstrdup(saved_params->paramValuesStr);
+ }
+ MemoryContextSwitchTo(oldcxt);
+
+ if (retry_params != NULL ||
+ (psrc->raw_parse_tree &&
+ analyze_requires_snapshot(psrc->raw_parse_tree)))
+ {
+ PushActiveSnapshot(GetTransactionSnapshot());
+ snapshot_set = true;
+ }
+
+ oldcxt = MemoryContextSwitchTo(MessageContext);
+ cplan = GetCachedPlan(psrc, retry_params, NULL, NULL);
+ PortalDefineQuery(portal,
+ portal_stmt_name,
+ portal_query_string,
+ psrc->commandTag,
+ cplan->stmt_list,
+ cplan);
+ if (snapshot_set)
+ PopActiveSnapshot();
+ MemoryContextSwitchTo(oldcxt);
+
+ PortalStart(portal, retry_params, 0, InvalidSnapshot);
+ if (saved_num_rformats > 0)
+ PortalSetResultFormat(portal, saved_num_rformats, saved_rformats);
+
+ portalParams = retry_params;
+ }
+
+ initial_lxid = MyProc ? MyProc->vxid.lxid : InvalidLocalTransactionId;
+
/*
- * If this was a transaction control statement, commit it. We
- * will start a new xact command for the next command (if any).
- * Likewise if the statement required immediate commit. Without
- * this provision, we wouldn't force commit until Sync is
- * received, which creates a hazard if the client tries to
- * pipeline immediate-commit statements.
+ * Create dest receiver in MessageContext (we don't want it in
+ * transaction context, because that may get deleted if portal
+ * contains VACUUM).
*/
- finish_xact_command();
+ oldcxt = MemoryContextSwitchTo(MessageContext);
+ receiver = CreateDestReceiver(dest);
+ if (dest == DestRemoteExecute)
+ SetRemoteDestReceiverParams(receiver, portal);
+ MemoryContextSwitchTo(oldcxt);
+
+ /* Check for cancel signal before we start execution */
+ CHECK_FOR_INTERRUPTS();
/*
- * These commands typically don't have any parameters, and even if
- * one did we couldn't print them now because the storage went
- * away during finish_xact_command. So pretend there were none.
+ * Okay to run the portal. Set the error callback so that
+ * parameters are logged. The parameters must have been saved
+ * during the bind phase.
*/
- portalParams = NULL;
+ params_data.portalName = portal->name;
+ params_data.params = portalParams;
+ params_errcxt.previous = error_context_stack;
+ params_errcxt.callback = ParamsErrorCallback;
+ params_errcxt.arg = ¶ms_data;
+ error_context_stack = ¶ms_errcxt;
+
+ completed = PortalRun(portal,
+ max_rows,
+ true, /* always top level */
+ receiver,
+ receiver,
+ &qc);
+
+ receiver->rDestroy(receiver);
+
+ /* Done executing; remove the params error callback */
+ error_context_stack = error_context_stack->previous;
+
+ if (completed)
+ {
+ if (is_xact_command ||
+ (MyXactFlags & XACT_FLAGS_NEEDIMMEDIATECOMMIT) ||
+ (can_retry && pq_is_next_msg_sync()))
+ {
+ /*
+ * If this was a transaction control statement, commit it.
+ * We will start a new xact command for the next command
+ * (if any). Likewise if the statement required immediate
+ * commit, or if autocommit retry is active and the next
+ * client message is Sync (so commit-time serialization
+ * failures are caught inside the retry loop).
+ */
+ finish_xact_command();
+
+ /*
+ * Use saved_params if available, since portal storage
+ * went away during finish_xact_command.
+ */
+ portalParams = saved_params;
+ }
+ else
+ {
+ /*
+ * We need a CommandCounterIncrement after every query,
+ * except those that start or end a transaction block.
+ */
+ CommandCounterIncrement();
+
+ /*
+ * Set XACT_FLAGS_PIPELINING whenever we complete an
+ * Execute message without immediately committing the
+ * transaction.
+ */
+ MyXactFlags |= XACT_FLAGS_PIPELINING;
+
+ /*
+ * Disable statement timeout whenever we complete an
+ * Execute message. The next protocol message will start
+ * a fresh timeout.
+ */
+ disable_statement_timeout();
+ }
+
+ if (can_retry && whereToSendOutput == DestRemote)
+ pq_end_retry_buffer();
+
+ /* Send appropriate CommandComplete to client */
+ EndCommand(&qc, dest, false);
+ }
+ else
+ {
+ if (can_retry && whereToSendOutput == DestRemote)
+ pq_end_retry_buffer();
+
+ /* Portal run not complete, so send PortalSuspended */
+ if (whereToSendOutput == DestRemote)
+ pq_putemptymessage(PqMsg_PortalSuspended);
+
+ /*
+ * Set XACT_FLAGS_PIPELINING whenever we suspend an Execute
+ * message, too.
+ */
+ MyXactFlags |= XACT_FLAGS_PIPELINING;
+ }
+
+ retry_done = true;
}
- else
+ PG_CATCH();
{
- /*
- * We need a CommandCounterIncrement after every query, except
- * those that start or end a transaction block.
- */
- CommandCounterIncrement();
+ ErrorData *edata;
+ int errcode_val = geterrcode();
+
+ if (!can_retry ||
+ retry_attempt >= max_autocommit_retries ||
+ pq_retry_buffer_flushed() ||
+ IsTransactionBlock() ||
+ (MyProc != NULL && MyProc->vxid.lxid != initial_lxid) ||
+ !is_autocommit_error_retryable(errcode_val, retry_class))
+ {
+ pq_end_retry_buffer();
+ PG_RE_THROW();
+ }
- /*
- * Set XACT_FLAGS_PIPELINING whenever we complete an Execute
- * message without immediately committing the transaction.
- */
- MyXactFlags |= XACT_FLAGS_PIPELINING;
+ HOLD_INTERRUPTS();
+ disable_all_timeouts(false);
+ QueryCancelPending = false;
+ stmt_timeout_canceled = false;
- /*
- * Disable statement timeout whenever we complete an Execute
- * message. The next protocol message will start a fresh timeout.
- */
- disable_statement_timeout();
+ pq_discard_retry_buffer();
+ pq_end_retry_buffer();
+
+ MemoryContextSwitchTo(MessageContext);
+ edata = CopyErrorData();
+ retry_errcode = edata->sqlerrcode;
+ retry_errmsg = pstrdup(edata->message ? edata->message : "");
+ FreeErrorData(edata);
+ FlushErrorState();
+
+ AbortCurrentTransaction();
+ if (am_walsender)
+ WalSndErrorCleanup();
+ PortalErrorCleanup();
+ if (MyReplicationSlot != NULL)
+ ReplicationSlotRelease();
+ ReplicationSlotCleanup(false);
+ jit_reset_after_error();
+
+ xact_started = false;
+
+ MemoryContextSwitchTo(MessageContext);
+
+ RESUME_INTERRUPTS();
}
+ PG_END_TRY();
- /* Send appropriate CommandComplete to client */
- EndCommand(&qc, dest, false);
- }
- else
- {
- /* Portal run not complete, so send PortalSuspended */
- if (whereToSendOutput == DestRemote)
- pq_putemptymessage(PqMsg_PortalSuspended);
+ if (retry_done)
+ break;
- /*
- * Set XACT_FLAGS_PIPELINING whenever we suspend an Execute message,
- * too.
- */
- MyXactFlags |= XACT_FLAGS_PIPELINING;
+ retry_attempt++;
+ autocommit_retry_sleep(retry_attempt, retry_errcode,
+ retry_errmsg, sourceText);
+ if (retry_errmsg)
+ pfree(retry_errmsg);
}
/*
@@ -3001,6 +3369,173 @@ drop_unnamed_stmt(void)
}
}
+/*
+ * Classify a statement (RawStmt, PlannedStmt, or parse/utility Node) for
+ * autocommit automatic retry eligibility.
+ */
+static AutocommitRetryClass
+classify_autocommit_retry_stmt(Node *stmt)
+{
+ if (stmt == NULL)
+ return AUTOCOMMIT_RETRY_NONE;
+
+ if (IsA(stmt, RawStmt))
+ stmt = ((RawStmt *) stmt)->stmt;
+
+ if (IsA(stmt, PlannedStmt))
+ {
+ PlannedStmt *pstmt = (PlannedStmt *) stmt;
+
+ switch (pstmt->commandType)
+ {
+ case CMD_SELECT:
+ case CMD_INSERT:
+ case CMD_UPDATE:
+ case CMD_DELETE:
+ case CMD_MERGE:
+ return AUTOCOMMIT_RETRY_DML;
+ case CMD_UTILITY:
+ stmt = pstmt->utilityStmt;
+ if (stmt == NULL)
+ return AUTOCOMMIT_RETRY_NONE;
+ break;
+ default:
+ return AUTOCOMMIT_RETRY_NONE;
+ }
+ }
+
+ switch (nodeTag(stmt))
+ {
+ case T_SelectStmt:
+ if (((SelectStmt *) stmt)->intoClause != NULL)
+ return AUTOCOMMIT_RETRY_DDL;
+ return AUTOCOMMIT_RETRY_DML;
+
+ case T_InsertStmt:
+ case T_UpdateStmt:
+ case T_DeleteStmt:
+ case T_MergeStmt:
+ case T_CallStmt:
+ return AUTOCOMMIT_RETRY_DML;
+
+ /*
+ * Non-transactional / filesystem-level commands that cannot be
+ * rolled back cleanly or run inside a transaction.
+ */
+ case T_CreatedbStmt:
+ case T_DropdbStmt:
+ case T_CreateTableSpaceStmt:
+ case T_DropTableSpaceStmt:
+ case T_AlterSystemStmt:
+ return AUTOCOMMIT_RETRY_NONE;
+
+ /*
+ * Additional table-level utility commands that acquire heavyweight
+ * relation locks and are safe to retry if they fail before committing
+ * an internal transaction.
+ */
+ case T_TruncateStmt:
+ case T_ReindexStmt:
+ case T_VacuumStmt:
+ return AUTOCOMMIT_RETRY_DDL;
+
+ default:
+ if (GetCommandLogLevel(stmt) == LOGSTMT_DDL)
+ return AUTOCOMMIT_RETRY_DDL;
+ return AUTOCOMMIT_RETRY_NONE;
+ }
+}
+
+/*
+ * Return true if the given SQLSTATE error code is eligible for autocommit
+ * retry for the given statement class under the current GUC settings.
+ */
+static bool
+is_autocommit_error_retryable(int sqlerrcode, AutocommitRetryClass retry_class)
+{
+ if (max_autocommit_retries <= 0 || retry_class == AUTOCOMMIT_RETRY_NONE)
+ return false;
+
+ if (retry_class == AUTOCOMMIT_RETRY_DDL && !enable_ddl_autocommit_retry)
+ return false;
+
+ if (sqlerrcode == ERRCODE_T_R_SERIALIZATION_FAILURE ||
+ sqlerrcode == ERRCODE_T_R_DEADLOCK_DETECTED)
+ return true;
+
+ if (retry_class == AUTOCOMMIT_RETRY_DDL)
+ {
+ if (sqlerrcode == ERRCODE_LOCK_NOT_AVAILABLE)
+ return true;
+ if (sqlerrcode == ERRCODE_QUERY_CANCELED && stmt_timeout_canceled)
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Sleep with exponential backoff and jitter before an autocommit retry
+ * attempt, optionally logging the retry event. Interruptible via WaitLatch
+ * so user cancel (SIGINT) or backend termination (SIGTERM) aborts the sleep
+ * immediately.
+ */
+static void
+autocommit_retry_sleep(int retry_attempt, int sqlerrcode,
+ const char *errmsg_str, const char *query_string)
+{
+ int64 base_ms = Max(1, autocommit_retry_backoff_base_ms);
+ int64 max_ms = Max(base_ms, autocommit_retry_backoff_max_ms);
+ int shift = Min(retry_attempt - 1, 20);
+ int64 cap_ms = Min(max_ms, base_ms * (INT64CONST(1) << shift));
+ int64 half_ms = cap_ms / 2;
+ int64 jitter_ms = (cap_ms - half_ms > 0) ?
+ (int64) pg_prng_uint64_range(&pg_global_prng_state, 0, cap_ms - half_ms) : 0;
+ long sleep_ms = (long) Max(1, half_ms + jitter_ms);
+ TimestampTz end_time;
+
+ if (log_autocommit_retries)
+ {
+ char *truncated_stmt = truncate_query_log(query_string);
+
+ ereport(LOG,
+ (errmsg("autocommit retry %d/%d after %s (%s) in %ld ms: %s",
+ retry_attempt,
+ max_autocommit_retries,
+ unpack_sql_state(sqlerrcode),
+ errmsg_str ? errmsg_str : "",
+ sleep_ms,
+ (truncated_stmt != NULL) ? truncated_stmt : query_string),
+ errhidestmt(true)));
+
+ if (truncated_stmt != NULL)
+ pfree(truncated_stmt);
+ }
+
+ end_time = TimestampTzPlusMilliseconds(GetCurrentTimestamp(), sleep_ms);
+ for (;;)
+ {
+ TimestampTz now;
+ long rem_ms;
+
+ ResetLatch(MyLatch);
+ CHECK_FOR_INTERRUPTS();
+
+ now = GetCurrentTimestamp();
+ if (now >= end_time)
+ break;
+
+ rem_ms = (long) ((TimestampDifferenceMicroseconds(now, end_time) + 999) / 1000);
+ if (rem_ms <= 0)
+ break;
+
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ rem_ms,
+ PG_WAIT_TIMEOUT);
+ }
+}
+
/* --------------------------------
* signal handler routines used in PostgresMain()
@@ -3605,6 +4140,7 @@ ProcessInterrupts(void)
}
if (stmt_timeout_occurred)
{
+ stmt_timeout_canceled = true;
LockErrorCleanup();
ereport(ERROR,
(errcode(ERRCODE_QUERY_CANCELED),
@@ -4599,6 +5135,7 @@ PostgresMain(const char *dbname, const char *username)
*/
disable_all_timeouts(false); /* do first to avoid race condition */
QueryCancelPending = false;
+ stmt_timeout_canceled = false;
idle_in_transaction_timeout_enabled = false;
idle_session_timeout_enabled = false;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 380679a76c0..5a72a08b648 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -114,6 +114,24 @@
max => '600',
},
+{ name => 'autocommit_retry_backoff_base_ms', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+ short_desc => 'Sets the initial backoff delay in milliseconds before retrying an autocommit statement.',
+ flags => 'GUC_UNIT_MS',
+ variable => 'autocommit_retry_backoff_base_ms',
+ boot_val => '10',
+ min => '1',
+ max => '60000',
+},
+
+{ name => 'autocommit_retry_backoff_max_ms', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+ short_desc => 'Sets the maximum backoff delay in milliseconds between retries of an autocommit statement.',
+ flags => 'GUC_UNIT_MS',
+ variable => 'autocommit_retry_backoff_max_ms',
+ boot_val => '1000',
+ min => '1',
+ max => '300000',
+},
+
{ name => 'autovacuum', type => 'bool', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM',
short_desc => 'Starts the autovacuum subprocess.',
variable => 'autovacuum_start_daemon',
@@ -869,6 +887,12 @@
boot_val => 'true',
},
+{ name => 'enable_ddl_autocommit_retry', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+ short_desc => 'Enables automatic retry of autocommit DDL statements on lock or statement timeout.',
+ variable => 'enable_ddl_autocommit_retry',
+ boot_val => 'false',
+},
+
{ name => 'enable_distinct_reordering', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD',
short_desc => 'Enables reordering of DISTINCT keys.',
flags => 'GUC_EXPLAIN',
@@ -1627,6 +1651,12 @@
max => 'INT_MAX',
},
+{ name => 'log_autocommit_retries', type => 'bool', context => 'PGC_USERSET', group => 'LOGGING_WHAT',
+ short_desc => 'Logs each automatic retry of an autocommit statement.',
+ variable => 'log_autocommit_retries',
+ boot_val => 'false',
+},
+
{ name => 'log_autovacuum_min_duration', type => 'int', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT',
short_desc => 'Sets the minimum execution time above which vacuum actions by autovacuum will be logged.',
long_desc => '-1 disables logging vacuum actions by autovacuum. 0 means log all vacuum actions by autovacuum.',
@@ -1977,6 +2007,15 @@
max => 'MAX_BACKENDS',
},
+{ name => 'max_autocommit_retries', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT',
+ short_desc => 'Sets the maximum number of automatic retries for autocommit statements encountering transient errors.',
+ long_desc => '0 disables automatic retries.',
+ variable => 'max_autocommit_retries',
+ boot_val => '0',
+ min => '0',
+ max => '100',
+},
+
{ name => 'max_connections', type => 'int', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS',
short_desc => 'Sets the maximum number of concurrent connections.',
variable => 'MaxConnections',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 01f98de8a9f..c292314d91a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -635,6 +635,7 @@
# their durations, > 0 logs only
# actions running at least this number
# of milliseconds.
+#log_autocommit_retries = off # log automatic retries of autocommit statements
#log_checkpoints = on
#log_connections = '' # log aspects of connection setup
# options include receipt, authentication, authorization,
@@ -819,6 +820,10 @@
#lock_timeout = 0 # in milliseconds, 0 is disabled
#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
#idle_session_timeout = 0 # in milliseconds, 0 is disabled
+#max_autocommit_retries = 0 # max automatic retries for autocommit statements; 0 is disabled
+#enable_ddl_autocommit_retry = off # retry autocommit DDL on lock or statement timeout
+#autocommit_retry_backoff_base_ms = 10ms # initial backoff delay between autocommit retries
+#autocommit_retry_backoff_max_ms = 1s # maximum backoff delay between autocommit retries
#bytea_output = 'hex' # hex, escape
#xmlbinary = 'base64'
#xmloption = 'content'
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index d15073a0a93..6f751aadc42 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -85,6 +85,11 @@ extern int pq_getbyte_if_available(unsigned char *c);
extern ssize_t pq_buffer_remaining_data(void);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
extern bool pq_check_connection(void);
+extern void pq_begin_retry_buffer(void);
+extern void pq_end_retry_buffer(void);
+extern bool pq_retry_buffer_flushed(void);
+extern void pq_discard_retry_buffer(void);
+extern bool pq_is_next_msg_sync(void);
/*
* prototypes for functions in be-secure.c
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 5bc5bcfb20d..35d77c2295e 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -46,6 +46,12 @@ extern PGDLLIMPORT int log_statement;
extern PGDLLIMPORT int restrict_nonsystem_relation_kind;
+extern PGDLLIMPORT int max_autocommit_retries;
+extern PGDLLIMPORT bool enable_ddl_autocommit_retry;
+extern PGDLLIMPORT int autocommit_retry_backoff_base_ms;
+extern PGDLLIMPORT int autocommit_retry_backoff_max_ms;
+extern PGDLLIMPORT bool log_autocommit_retries;
+
extern List *pg_parse_query(const char *query_string);
extern List *pg_rewrite_query(Query *query);
extern List *pg_analyze_and_rewrite_fixedparams(RawStmt *parsetree,
diff --git a/src/test/isolation/expected/autocommit-retry.out b/src/test/isolation/expected/autocommit-retry.out
new file mode 100644
index 00000000000..32acdea38e7
--- /dev/null
+++ b/src/test/isolation/expected/autocommit-retry.out
@@ -0,0 +1,258 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s2_retries_0 s1_begin s1_upd2 s2_upd_ret s1_commit s2_check_tbl
+step s2_retries_0: SET max_autocommit_retries = 0;
+step s1_begin: BEGIN;
+step s1_upd2: UPDATE retry_tbl SET val = val + 100 WHERE id = 2;
+step s2_upd_ret: UPDATE retry_tbl SET val = val + 1 RETURNING id, val; <waiting ...>
+step s1_commit: COMMIT;
+step s2_upd_ret: <... completed>
+ERROR: could not serialize access due to concurrent update
+step s2_check_tbl: SELECT id, val FROM retry_tbl ORDER BY id;
+id|val
+--+---
+ 1| 10
+ 2|120
+(2 rows)
+
+
+starting permutation: s2_retries_3 s1_begin s1_upd2 s2_upd_ret s1_commit s2_check_tbl
+step s2_retries_3: SET max_autocommit_retries = 3;
+step s1_begin: BEGIN;
+step s1_upd2: UPDATE retry_tbl SET val = val + 100 WHERE id = 2;
+step s2_upd_ret: UPDATE retry_tbl SET val = val + 1 RETURNING id, val; <waiting ...>
+step s1_commit: COMMIT;
+step s2_upd_ret: <... completed>
+id|val
+--+---
+ 1| 11
+ 2|121
+(2 rows)
+
+step s2_check_tbl: SELECT id, val FROM retry_tbl ORDER BY id;
+id|val
+--+---
+ 1| 11
+ 2|121
+(2 rows)
+
+
+starting permutation: s2_retries_3 s1_begin s1_upd2 s2_begin s2_upd_ret s1_commit s2_rollback s2_check_tbl
+step s2_retries_3: SET max_autocommit_retries = 3;
+step s1_begin: BEGIN;
+step s1_upd2: UPDATE retry_tbl SET val = val + 100 WHERE id = 2;
+step s2_begin: BEGIN;
+step s2_upd_ret: UPDATE retry_tbl SET val = val + 1 RETURNING id, val; <waiting ...>
+step s1_commit: COMMIT;
+step s2_upd_ret: <... completed>
+ERROR: could not serialize access due to concurrent update
+step s2_rollback: ROLLBACK;
+step s2_check_tbl: SELECT id, val FROM retry_tbl ORDER BY id;
+id|val
+--+---
+ 1| 10
+ 2|120
+(2 rows)
+
+
+starting permutation: s2_retries_3 s1_begin s1_upd2 s2_multistmt s1_commit s2_check_tbl
+step s2_retries_3: SET max_autocommit_retries = 3;
+step s1_begin: BEGIN;
+step s1_upd2: UPDATE retry_tbl SET val = val + 100 WHERE id = 2;
+step s2_multistmt: SELECT 1; UPDATE retry_tbl SET val = val + 1 WHERE id = 2; <waiting ...>
+step s1_commit: COMMIT;
+step s2_multistmt: <... completed>
+?column?
+--------
+ 1
+(1 row)
+
+ERROR: could not serialize access due to concurrent update
+step s2_check_tbl: SELECT id, val FROM retry_tbl ORDER BY id;
+id|val
+--+---
+ 1| 10
+ 2|120
+(2 rows)
+
+
+starting permutation: s2_retries_3 s1_begin s1_upd2 s2_call_simple s1_commit s2_check_tbl
+step s2_retries_3: SET max_autocommit_retries = 3;
+step s1_begin: BEGIN;
+step s1_upd2: UPDATE retry_tbl SET val = val + 100 WHERE id = 2;
+step s2_call_simple: CALL retry_proc_simple(); <waiting ...>
+step s1_commit: COMMIT;
+step s2_call_simple: <... completed>
+step s2_check_tbl: SELECT id, val FROM retry_tbl ORDER BY id;
+id|val
+--+---
+ 1| 10
+ 2|121
+(2 rows)
+
+
+starting permutation: s2_retries_3 s1_begin s1_upd2 s2_call_commit s1_commit s2_check_tbl
+step s2_retries_3: SET max_autocommit_retries = 3;
+step s1_begin: BEGIN;
+step s1_upd2: UPDATE retry_tbl SET val = val + 100 WHERE id = 2;
+step s2_call_commit: CALL retry_proc_with_commit(); <waiting ...>
+step s1_commit: COMMIT;
+step s2_call_commit: <... completed>
+ERROR: could not serialize access due to concurrent update
+step s2_check_tbl: SELECT id, val FROM retry_tbl ORDER BY id;
+id|val
+--+---
+ 1| 11
+ 2|120
+(2 rows)
+
+
+starting permutation: s2_retries_2 s2_reset_seq s2_test_40001 s2_reset_seq s2_test_40p01 s2_reset_seq s2_exhaust s2_check_seq s2_ddl_setup s2_ddl_alter s2_check_seq s2_ddl_enable s2_ddl_alter s2_ddl_stmttimeout_setup s2_ddl_alter2 s2_check_seq s2_ddl_stmttimeout_enable s2_ddl_alter2 s2_ddl_cleanup
+step s2_retries_2: SET max_autocommit_retries = 2;
+step s2_reset_seq:
+ SET log_autocommit_retries = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+
+setval
+------
+ 1
+(1 row)
+
+step s2_test_40001: SELECT fail_n_times(2, '40001') AS attempts_40001;
+attempts_40001
+--------------
+ 3
+(1 row)
+
+step s2_reset_seq:
+ SET log_autocommit_retries = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+
+setval
+------
+ 1
+(1 row)
+
+step s2_test_40p01: SELECT fail_n_times(2, '40P01') AS attempts_40p01;
+attempts_40p01
+--------------
+ 3
+(1 row)
+
+step s2_reset_seq:
+ SET log_autocommit_retries = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+
+setval
+------
+ 1
+(1 row)
+
+step s2_exhaust: SELECT fail_n_times(3, '40001') AS should_fail;
+ERROR: simulated transient failure on attempt 3
+step s2_check_seq: SELECT last_value FROM retry_attempt_seq;
+last_value
+----------
+ 3
+(1 row)
+
+step s2_ddl_setup:
+ CREATE FUNCTION ddl_fail_once() RETURNS event_trigger LANGUAGE plpgsql AS $$
+ BEGIN
+ IF nextval('retry_attempt_seq') <= 1 THEN
+ RAISE EXCEPTION 'simulated DDL lock timeout' USING ERRCODE = '55P03';
+ END IF;
+ END;
+ $$;
+ CREATE EVENT TRIGGER ddl_retry_trig ON ddl_command_start
+ WHEN TAG IN ('ALTER TABLE') EXECUTE FUNCTION ddl_fail_once();
+ SET enable_ddl_autocommit_retry = off;
+ SELECT setval('retry_attempt_seq', 1, false);
+
+setval
+------
+ 1
+(1 row)
+
+step s2_ddl_alter: ALTER TABLE retry_tbl ADD COLUMN extra int DEFAULT 42;
+ERROR: simulated DDL lock timeout
+step s2_check_seq: SELECT last_value FROM retry_attempt_seq;
+last_value
+----------
+ 1
+(1 row)
+
+step s2_ddl_enable:
+ SET enable_ddl_autocommit_retry = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+
+setval
+------
+ 1
+(1 row)
+
+step s2_ddl_alter: ALTER TABLE retry_tbl ADD COLUMN extra int DEFAULT 42;
+step s2_ddl_stmttimeout_setup:
+ SELECT last_value AS ddl_lock_attempts FROM retry_attempt_seq;
+ DROP EVENT TRIGGER ddl_retry_trig;
+ DROP FUNCTION ddl_fail_once();
+
+ CREATE FUNCTION ddl_sleep_once() RETURNS event_trigger LANGUAGE plpgsql AS $$
+ BEGIN
+ IF nextval('retry_attempt_seq') <= 1 THEN
+ PERFORM pg_sleep(1);
+ END IF;
+ END;
+ $$;
+ CREATE EVENT TRIGGER ddl_stmt_timeout_trig ON ddl_command_start
+ WHEN TAG IN ('ALTER TABLE') EXECUTE FUNCTION ddl_sleep_once();
+ SET statement_timeout = '15ms';
+ SET enable_ddl_autocommit_retry = off;
+ SELECT setval('retry_attempt_seq', 1, false);
+
+ddl_lock_attempts
+-----------------
+ 2
+(1 row)
+
+setval
+------
+ 1
+(1 row)
+
+step s2_ddl_alter2: ALTER TABLE retry_tbl ADD COLUMN extra2 int DEFAULT 99;
+ERROR: canceling statement due to statement timeout
+step s2_check_seq: SELECT last_value FROM retry_attempt_seq;
+last_value
+----------
+ 1
+(1 row)
+
+step s2_ddl_stmttimeout_enable:
+ SET enable_ddl_autocommit_retry = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+
+setval
+------
+ 1
+(1 row)
+
+step s2_ddl_alter2: ALTER TABLE retry_tbl ADD COLUMN extra2 int DEFAULT 99;
+step s2_ddl_cleanup:
+ SET statement_timeout = 0;
+ SELECT last_value AS ddl_stmttimeout_attempts FROM retry_attempt_seq;
+ SELECT id, val, extra, extra2 FROM retry_tbl ORDER BY id;
+ DROP EVENT TRIGGER ddl_stmt_timeout_trig;
+ DROP FUNCTION ddl_sleep_once();
+
+ddl_stmttimeout_attempts
+------------------------
+ 2
+(1 row)
+
+id|val|extra|extra2
+--+---+-----+------
+ 1| 10| 42| 99
+ 2| 20| 42| 99
+(2 rows)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index bf9a2037c70..2b747f17be9 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -132,3 +132,4 @@ test: ddl-dependency-locking
test: tablespace-dependency-locking
test: pub-concurrent-drop
test: drop-owned-grant
+test: autocommit-retry
diff --git a/src/test/isolation/specs/autocommit-retry.spec b/src/test/isolation/specs/autocommit-retry.spec
new file mode 100644
index 00000000000..1ba382daf06
--- /dev/null
+++ b/src/test/isolation/specs/autocommit-retry.spec
@@ -0,0 +1,157 @@
+# Tests for automatic retry of autocommit-mode statements encountering
+# serialization failures, deadlocks, and (optionally) DDL lock/statement
+# timeouts.
+
+setup
+{
+ CREATE TABLE retry_tbl (id int PRIMARY KEY, val int NOT NULL);
+ INSERT INTO retry_tbl VALUES (1, 10), (2, 20);
+
+ CREATE SEQUENCE retry_attempt_seq;
+
+ CREATE FUNCTION fail_n_times(target_failures int, err_sqlstate text)
+ RETURNS int LANGUAGE plpgsql AS $$
+ DECLARE
+ cur_attempt int;
+ BEGIN
+ cur_attempt := nextval('retry_attempt_seq');
+ IF cur_attempt <= target_failures THEN
+ RAISE EXCEPTION 'simulated transient failure on attempt %', cur_attempt
+ USING ERRCODE = err_sqlstate;
+ END IF;
+ RETURN cur_attempt;
+ END;
+ $$;
+
+ CREATE PROCEDURE retry_proc_simple()
+ LANGUAGE plpgsql AS $$
+ BEGIN
+ UPDATE retry_tbl SET val = val + 1 WHERE id = 2;
+ END;
+ $$;
+
+ CREATE PROCEDURE retry_proc_with_commit()
+ LANGUAGE plpgsql AS $$
+ BEGIN
+ UPDATE retry_tbl SET val = val + 1 WHERE id = 1;
+ COMMIT;
+ SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
+ UPDATE retry_tbl SET val = val + 1 WHERE id = 2;
+ END;
+ $$;
+}
+
+teardown
+{
+ DROP PROCEDURE retry_proc_with_commit();
+ DROP PROCEDURE retry_proc_simple();
+ DROP FUNCTION fail_n_times(int, text);
+ DROP SEQUENCE retry_attempt_seq;
+ DROP TABLE retry_tbl;
+}
+
+session s1
+step s1_begin { BEGIN; }
+step s1_upd2 { UPDATE retry_tbl SET val = val + 100 WHERE id = 2; }
+step s1_commit { COMMIT; }
+
+session s2
+setup
+{
+ SET default_transaction_isolation = 'repeatable read';
+ SET autocommit_retry_backoff_base_ms = 1;
+ SET autocommit_retry_backoff_max_ms = 5;
+}
+step s2_retries_0 { SET max_autocommit_retries = 0; }
+step s2_retries_2 { SET max_autocommit_retries = 2; }
+step s2_retries_3 { SET max_autocommit_retries = 3; }
+step s2_upd_ret { UPDATE retry_tbl SET val = val + 1 RETURNING id, val; }
+step s2_begin { BEGIN; }
+step s2_rollback { ROLLBACK; }
+step s2_multistmt { SELECT 1; UPDATE retry_tbl SET val = val + 1 WHERE id = 2; }
+step s2_call_simple { CALL retry_proc_simple(); }
+step s2_call_commit { CALL retry_proc_with_commit(); }
+step s2_check_tbl { SELECT id, val FROM retry_tbl ORDER BY id; }
+step s2_reset_seq
+{
+ SET log_autocommit_retries = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+}
+step s2_test_40001 { SELECT fail_n_times(2, '40001') AS attempts_40001; }
+step s2_test_40p01 { SELECT fail_n_times(2, '40P01') AS attempts_40p01; }
+step s2_exhaust { SELECT fail_n_times(3, '40001') AS should_fail; }
+step s2_check_seq { SELECT last_value FROM retry_attempt_seq; }
+step s2_ddl_setup
+{
+ CREATE FUNCTION ddl_fail_once() RETURNS event_trigger LANGUAGE plpgsql AS $$
+ BEGIN
+ IF nextval('retry_attempt_seq') <= 1 THEN
+ RAISE EXCEPTION 'simulated DDL lock timeout' USING ERRCODE = '55P03';
+ END IF;
+ END;
+ $$;
+ CREATE EVENT TRIGGER ddl_retry_trig ON ddl_command_start
+ WHEN TAG IN ('ALTER TABLE') EXECUTE FUNCTION ddl_fail_once();
+ SET enable_ddl_autocommit_retry = off;
+ SELECT setval('retry_attempt_seq', 1, false);
+}
+step s2_ddl_alter { ALTER TABLE retry_tbl ADD COLUMN extra int DEFAULT 42; }
+step s2_ddl_enable
+{
+ SET enable_ddl_autocommit_retry = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+}
+step s2_ddl_stmttimeout_setup
+{
+ SELECT last_value AS ddl_lock_attempts FROM retry_attempt_seq;
+ DROP EVENT TRIGGER ddl_retry_trig;
+ DROP FUNCTION ddl_fail_once();
+
+ CREATE FUNCTION ddl_sleep_once() RETURNS event_trigger LANGUAGE plpgsql AS $$
+ BEGIN
+ IF nextval('retry_attempt_seq') <= 1 THEN
+ PERFORM pg_sleep(1);
+ END IF;
+ END;
+ $$;
+ CREATE EVENT TRIGGER ddl_stmt_timeout_trig ON ddl_command_start
+ WHEN TAG IN ('ALTER TABLE') EXECUTE FUNCTION ddl_sleep_once();
+ SET statement_timeout = '15ms';
+ SET enable_ddl_autocommit_retry = off;
+ SELECT setval('retry_attempt_seq', 1, false);
+}
+step s2_ddl_alter2 { ALTER TABLE retry_tbl ADD COLUMN extra2 int DEFAULT 99; }
+step s2_ddl_stmttimeout_enable
+{
+ SET enable_ddl_autocommit_retry = on;
+ SELECT setval('retry_attempt_seq', 1, false);
+}
+step s2_ddl_cleanup
+{
+ SET statement_timeout = 0;
+ SELECT last_value AS ddl_stmttimeout_attempts FROM retry_attempt_seq;
+ SELECT id, val, extra, extra2 FROM retry_tbl ORDER BY id;
+ DROP EVENT TRIGGER ddl_stmt_timeout_trig;
+ DROP FUNCTION ddl_sleep_once();
+}
+
+# 1. Default max_autocommit_retries = 0: serialization failure is reported
+permutation s2_retries_0 s1_begin s1_upd2 s2_upd_ret s1_commit s2_check_tbl
+
+# 2. Autocommit UPDATE ... RETURNING retries on 40001 and discards buffered rows from attempt 1
+permutation s2_retries_3 s1_begin s1_upd2 s2_upd_ret s1_commit s2_check_tbl
+
+# 3. Explicit transaction block (BEGIN ... COMMIT) does not auto-retry
+permutation s2_retries_3 s1_begin s1_upd2 s2_begin s2_upd_ret s1_commit s2_rollback s2_check_tbl
+
+# 4. Multi-statement simple query does not auto-retry
+permutation s2_retries_3 s1_begin s1_upd2 s2_multistmt s1_commit s2_check_tbl
+
+# 5. CALL procedure without internal transaction control auto-retries on 40001
+permutation s2_retries_3 s1_begin s1_upd2 s2_call_simple s1_commit s2_check_tbl
+
+# 6. CALL procedure that commits internally before failing does not auto-retry
+permutation s2_retries_3 s1_begin s1_upd2 s2_call_commit s1_commit s2_check_tbl
+
+# 7. Deterministic retry counts for 40001, 40P01, retry exhaustion, DDL 55P03, and DDL statement_timeout
+permutation s2_retries_2 s2_reset_seq s2_test_40001 s2_reset_seq s2_test_40p01 s2_reset_seq s2_exhaust s2_check_seq s2_ddl_setup s2_ddl_alter s2_check_seq s2_ddl_enable s2_ddl_alter s2_ddl_stmttimeout_setup s2_ddl_alter2 s2_check_seq s2_ddl_stmttimeout_enable s2_ddl_alter2 s2_ddl_cleanup
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index e8605dd041f..1c0fa8f6d7f 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -7012,3 +7012,55 @@ Policies |
\pset null ''
DROP TABLE defprivs;
+-- Test autocommit retry over Simple and Extended Query protocol (\bind)
+-- including discarding buffered partial DataRow messages on retry.
+CREATE SEQUENCE retry_ext_seq;
+CREATE FUNCTION retry_ext_fn(n int) RETURNS SETOF int LANGUAGE plpgsql AS $$
+DECLARE
+ attempt bigint;
+BEGIN
+ attempt := nextval('retry_ext_seq');
+ RETURN NEXT 100 + attempt::int;
+ IF attempt <= n THEN
+ RAISE SQLSTATE '40001' USING MESSAGE = 'simulated serialization failure';
+ END IF;
+ RETURN NEXT 200 + attempt::int;
+END;
+$$;
+SET max_autocommit_retries = 2;
+SET autocommit_retry_backoff_base_ms = 1;
+SET autocommit_retry_backoff_max_ms = 5;
+SELECT * FROM retry_ext_fn(2);
+ retry_ext_fn
+--------------
+ 103
+ 203
+(2 rows)
+
+SELECT setval('retry_ext_seq', 1, false);
+ setval
+--------
+ 1
+(1 row)
+
+SELECT * FROM retry_ext_fn($1) \bind 2 \g
+ retry_ext_fn
+--------------
+ 103
+ 203
+(2 rows)
+
+SELECT setval('retry_ext_seq', 1, false);
+ setval
+--------
+ 1
+(1 row)
+
+SELECT * FROM retry_ext_fn($1) \bind 3 \g
+ERROR: simulated serialization failure
+CONTEXT: PL/pgSQL function retry_ext_fn(integer) line 8 at RAISE
+RESET max_autocommit_retries;
+RESET autocommit_retry_backoff_base_ms;
+RESET autocommit_retry_backoff_max_ms;
+DROP FUNCTION retry_ext_fn(int);
+DROP SEQUENCE retry_ext_seq;
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 1e327c2afa4..10297a181a2 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -157,6 +157,7 @@ select name, setting from pg_settings where name like 'enable%';
--------------------------------+---------
enable_async_append | on
enable_bitmapscan | on
+ enable_ddl_autocommit_retry | off
enable_distinct_reordering | on
enable_eager_aggregate | on
enable_gathermerge | on
@@ -181,7 +182,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_seqscan | on
enable_sort | on
enable_tidscan | on
-(26 rows)
+(27 rows)
-- There are always wait event descriptions for various types. InjectionPoint
-- may be present or absent, depending on history since last postmaster start.
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index cd685b43e0f..7dd9012dffc 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1991,3 +1991,33 @@ CREATE TABLE defprivs (a int);
\zx defprivs
\pset null ''
DROP TABLE defprivs;
+
+-- Test autocommit retry over Simple and Extended Query protocol (\bind)
+-- including discarding buffered partial DataRow messages on retry.
+CREATE SEQUENCE retry_ext_seq;
+CREATE FUNCTION retry_ext_fn(n int) RETURNS SETOF int LANGUAGE plpgsql AS $$
+DECLARE
+ attempt bigint;
+BEGIN
+ attempt := nextval('retry_ext_seq');
+ RETURN NEXT 100 + attempt::int;
+ IF attempt <= n THEN
+ RAISE SQLSTATE '40001' USING MESSAGE = 'simulated serialization failure';
+ END IF;
+ RETURN NEXT 200 + attempt::int;
+END;
+$$;
+SET max_autocommit_retries = 2;
+SET autocommit_retry_backoff_base_ms = 1;
+SET autocommit_retry_backoff_max_ms = 5;
+SELECT * FROM retry_ext_fn(2);
+SELECT setval('retry_ext_seq', 1, false);
+SELECT * FROM retry_ext_fn($1) \bind 2 \g
+SELECT setval('retry_ext_seq', 1, false);
+SELECT * FROM retry_ext_fn($1) \bind 3 \g
+RESET max_autocommit_retries;
+RESET autocommit_retry_backoff_base_ms;
+RESET autocommit_retry_backoff_max_ms;
+DROP FUNCTION retry_ext_fn(int);
+DROP SEQUENCE retry_ext_seq;
+
--
2.56.0.212.g129ede20d9-goog