Handle better implicit transaction state of pipeline mode
authorMichael Paquier <[email protected]>
Wed, 27 Nov 2024 00:31:22 +0000 (09:31 +0900)
committerMichael Paquier <[email protected]>
Wed, 27 Nov 2024 00:31:22 +0000 (09:31 +0900)
When using a pipeline, a transaction starts from the first command and
is committed with a Sync message or when the pipeline ends.

Functions like IsInTransactionBlock() or PreventInTransactionBlock()
were already able to understand a pipeline as being in a transaction
block, but it was not the case of CheckTransactionBlock().  This
function is called for example to generate a WARNING for SET LOCAL,
complaining that it is used outside of a transaction block.

The current state of the code caused multiple problems, like:
- SET LOCAL executed at any stage of a pipeline issued a WARNING, even
if the command was at least second in line where the pipeline is in a
transaction state.
- LOCK TABLE failed when invoked at any step of a pipeline, even if it
should be able to work within a transaction block.

The pipeline protocol assumes that the first command of a pipeline is
not part of a transaction block, and that any follow-up commands is
considered as within a transaction block.

This commit changes the backend so as an implicit transaction block is
started each time the first Execute message of a pipeline has finished
processing, with this implicit transaction block ended once a sync is
processed.  The checks based on XACT_FLAGS_PIPELINING in the routines
checking if we are in a transaction block are not necessary: it is
enough to rely on the existing ones.

Some tests are added to pgbench, that can be backed down to v17
when \syncpipeline is involved and down to v14 where \startpipeline and
\endpipeline are available.  This is unfortunately limited regarding the
error patterns that can be checked, but it provides coverage for various
pipeline combinations to check if these succeed or fail.  These tests
are able to capture the case of SET LOCAL's WARNING.  The author has
proposed a different feature to improve the coverage by adding similar
meta-commands to psql where error messages could be checked, something
more useful for the cases where commands cannot be used in transaction
blocks, like REINDEX CONCURRENTLY or VACUUM.  This is considered as
future work for v18~.

Author: Anthonin Bonnefoy
Reviewed-by: Jelte Fennema-Nio, Michael Paquier
Discussion: https://postgr.es/m/CAO6_XqrWO8uNBQrSu5r6jh+vTGi5Oiyk4y8yXDORdE2jbzw8xw@mail.gmail.com
Back-through: 13

doc/src/sgml/protocol.sgml
src/backend/access/transam/xact.c
src/backend/tcop/postgres.c
src/bin/pgbench/t/001_pgbench_with_server.pl

index d5a78694b99ac732879f5a20d4c296a920ca3f27..cff0c4099e98c16ccf99a85b7cc77c034bffecb6 100644 (file)
@@ -1070,16 +1070,17 @@ SELCT 1/0;<!-- this typo is intentional -->
 
    <para>
     If the client has not issued an explicit <command>BEGIN</command>,
-    then each Sync ordinarily causes an implicit <command>COMMIT</command>
-    if the preceding step(s) succeeded, or an
-    implicit <command>ROLLBACK</command> if they failed.  However, there
-    are a few DDL commands (such as <command>CREATE DATABASE</command>)
-    that cannot be executed inside a transaction block.  If one of
-    these is executed in a pipeline, it will fail unless it is the first
-    command in the pipeline.  Furthermore, upon success it will force an
-    immediate commit to preserve database consistency.  Thus a Sync
-    immediately following one of these commands has no effect except to
-    respond with ReadyForQuery.
+    then an implicit transaction block is started and each Sync ordinarily
+    causes an implicit <command>COMMIT</command> if the preceding step(s)
+    succeeded, or an implicit <command>ROLLBACK</command> if they failed.
+    This implicit transaction block will only be detected by the server
+    when the first command ends without a sync.  There are a few DDL
+    commands (such as <command>CREATE DATABASE</command>) that cannot be
+    executed inside a transaction block. If one of these is executed in a
+    pipeline, it will fail unless it is the first command after a Sync.
+    Furthermore, upon success it will force an immediate commit to preserve
+    database consistency. Thus a Sync immediately following one of these
+    commands has no effect except to respond with ReadyForQuery.
    </para>
 
    <para>
index b7ebcc2a557cc5b72139a228f7411f4b5b2f65d2..1eccb78ddc4b089e33ca2cf7b6562888d42eb3c2 100644 (file)
@@ -3659,16 +3659,6 @@ PreventInTransactionBlock(bool isTopLevel, const char *stmtType)
                 errmsg("%s cannot run inside a subtransaction",
                        stmtType)));
 
-   /*
-    * inside a pipeline that has started an implicit transaction?
-    */
-   if (MyXactFlags & XACT_FLAGS_PIPELINING)
-       ereport(ERROR,
-               (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
-       /* translator: %s represents an SQL statement name */
-                errmsg("%s cannot be executed within a pipeline",
-                       stmtType)));
-
    /*
     * inside a function call?
     */
@@ -3780,9 +3770,6 @@ IsInTransactionBlock(bool isTopLevel)
    if (IsSubTransaction())
        return true;
 
-   if (MyXactFlags & XACT_FLAGS_PIPELINING)
-       return true;
-
    if (!isTopLevel)
        return true;
 
index 184b83016870c7b6cb42b4da122eb83b62847173..bd4247671764dfd2c3fa01d5bd01a01b3e393065 100644 (file)
@@ -2780,6 +2780,17 @@ start_xact_command(void)
 
        xact_started = true;
    }
+   else if (MyXactFlags & XACT_FLAGS_PIPELINING)
+   {
+       /*
+        * When the first Execute message is completed, following commands
+        * will be done in an implicit transaction block created via
+        * pipelining. The transaction state needs to be updated to an
+        * implicit block if we're not already in a transaction block (like
+        * one started by an explicit BEGIN).
+        */
+       BeginImplicitTransactionBlock();
+   }
 
    /*
     * Start statement timeout if necessary.  Note that this'll intentionally
@@ -4991,6 +5002,13 @@ PostgresMain(const char *dbname, const char *username)
 
            case PqMsg_Sync:
                pq_getmsgend(&input_message);
+
+               /*
+                * If pipelining was used, we may be in an implicit
+                * transaction block. Close it before calling
+                * finish_xact_command.
+                */
+               EndImplicitTransactionBlock();
                finish_xact_command();
                valgrind_report_error_query("SYNC message");
                send_ready_for_query = true;
index 956e290f3efab9877ac575a0d74b3c3ee6661f1a..0bc5c0042ebd5b10d981f65d333a548f427df044 100644 (file)
@@ -968,6 +968,180 @@ $node->pgbench(
 }
    });
 
+# Try SET LOCAL as first pipeline command.  This succeeds and the first
+# command is not executed inside an implicit transaction block, causing
+# a WARNING.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   0,
+   [],
+   [qr{WARNING:  SET LOCAL can only be used in transaction blocks}],
+   'SET LOCAL outside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_set_local_1' => q{
+\startpipeline
+SET LOCAL statement_timeout='1h';
+\endpipeline
+}
+   });
+
+# Try SET LOCAL as second pipeline command.  This succeeds and the second
+# command does not cause a WARNING to be generated.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   0,
+   [],
+   [qr{^$}],
+   'SET LOCAL inside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_set_local_2' => q{
+\startpipeline
+SELECT 1;
+SET LOCAL statement_timeout='1h';
+\endpipeline
+}
+   });
+
+# Try SET LOCAL with \syncpipeline.  This succeeds and the command
+# launched after the sync is outside the implicit transaction block
+# of the pipeline, causing a WARNING.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   0,
+   [],
+   [qr{WARNING:  SET LOCAL can only be used in transaction blocks}],
+   'SET LOCAL and \syncpipeline',
+   {
+       '001_pgbench_pipeline_set_local_3' => q{
+\startpipeline
+SELECT 1;
+\syncpipeline
+SET LOCAL statement_timeout='1h';
+\endpipeline
+}
+   });
+
+# Try REINDEX CONCURRENTLY as first pipeline command.  This succeeds
+# as the first command is outside the implicit transaction block of
+# a pipeline.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   0,
+   [],
+   [],
+   'REINDEX CONCURRENTLY outside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_reindex_1' => q{
+\startpipeline
+REINDEX TABLE CONCURRENTLY pgbench_accounts;
+SELECT 1;
+\endpipeline
+}
+   });
+
+# Try REINDEX CONCURRENTLY as second pipeline command.  This fails
+# as the second command is inside an implicit transaction block.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   2,
+   [],
+   [],
+   'error: REINDEX CONCURRENTLY inside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_reindex_2' => q{
+\startpipeline
+SELECT 1;
+REINDEX TABLE CONCURRENTLY pgbench_accounts;
+\endpipeline
+}
+   });
+
+# Try VACUUM as first pipeline command.  Like REINDEX CONCURRENTLY, this
+# succeeds as this is outside the implicit transaction block of a pipeline.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   0,
+   [],
+   [],
+   'VACUUM outside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_vacuum_1' => q{
+\startpipeline
+VACUUM pgbench_accounts;
+\endpipeline
+}
+   });
+
+# Try VACUUM as second pipeline command.  This fails, as the second command
+# of a pipeline is inside an implicit transaction block.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   2,
+   [],
+   [],
+   'error: VACUUM inside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_vacuum_2' => q{
+\startpipeline
+SELECT 1;
+VACUUM pgbench_accounts;
+\endpipeline
+}
+   });
+
+# Try subtransactions in a pipeline.  These are forbidden in implicit
+# transaction blocks.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   2,
+   [],
+   [],
+   'error: subtransactions not allowed in pipeline',
+   {
+       '001_pgbench_pipeline_subtrans' => q{
+\startpipeline
+SAVEPOINT a;
+SELECT 1;
+ROLLBACK TO SAVEPOINT a;
+SELECT 2;
+\endpipeline
+}
+   });
+
+# Try LOCK TABLE as first pipeline command.  This fails as LOCK is outside
+# an implicit transaction block.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   2,
+   [],
+   [],
+   'error: LOCK TABLE outside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_lock_1' => q{
+\startpipeline
+LOCK pgbench_accounts;
+SELECT 1;
+\endpipeline
+}
+   });
+
+# Try LOCK TABLE as second pipeline command.  This succeeds as LOCK is inside
+# an implicit transaction block.
+$node->pgbench(
+   '-t 1 -n -M extended',
+   0,
+   [],
+   [],
+   'LOCK TABLE inside implicit transaction block of pipeline',
+   {
+       '001_pgbench_pipeline_lock_2' => q{
+\startpipeline
+SELECT 1;
+LOCK pgbench_accounts;
+\endpipeline
+}
+   });
+
 # Working \startpipeline in prepared query mode with serializable
 $node->pgbench(
    '-c4 -t 10 -n -M prepared',