Further refine _SPI_execute_plan's rule for atomic execution.
authorTom Lane <[email protected]>
Wed, 16 Oct 2024 21:36:29 +0000 (17:36 -0400)
committerTom Lane <[email protected]>
Wed, 16 Oct 2024 21:36:40 +0000 (17:36 -0400)
Commit 2dc1deaea turns out to have been still a brick shy of a load,
because CALL statements executing within a plpgsql exception block
could still pass the wrong snapshot to stable functions within the
CALL's argument list.  That happened because standard_ProcessUtility
forces isAtomicContext to true if IsTransactionBlock is true, which
it always will be inside a subtransaction.  Then ExecuteCallStmt
would think it does not need to push a new snapshot --- but
_SPI_execute_plan didn't do so either, since it thought it was in
nonatomic mode.

The best fix for this seems to be for _SPI_execute_plan to operate
in atomic execution mode if IsSubTransaction() is true, even when the
SPI context as a whole is non-atomic.  This makes _SPI_execute_plan
have the same rules about when non-atomic execution is allowed as
_SPI_commit/_SPI_rollback have about when COMMIT/ROLLBACK are allowed,
which seems appropriately symmetric.  (If anyone ever tries to allow
COMMIT/ROLLBACK inside a subtransaction, this would all need to be
rethought ... but I'm unconvinced that such a thing could be logically
consistent at all.)

For further consistency, also check IsSubTransaction() in
SPI_inside_nonatomic_context.  That does not matter for its
one present-day caller StartTransaction, which can't be reached
inside a subtransaction.  But if any other callers ever arise,
they'd presumably want this definition.

Per bug #18656 from Alexander Alehin.  Back-patch to all
supported branches, like previous fixes in this area.

Discussion: https://postgr.es/m/18656-cade1780866ef66c@postgresql.org

src/backend/executor/spi.c
src/pl/plpgsql/src/expected/plpgsql_call.out
src/pl/plpgsql/src/sql/plpgsql_call.sql

index 90d98345764ba454a23a8608237c7af0bf989a8d..2fb2e73604eed826edabf1ee03a0ee6b489e267a 100644 (file)
@@ -334,13 +334,13 @@ _SPI_rollback(bool chain)
    MemoryContext oldcontext = CurrentMemoryContext;
    SavedTransactionCharacteristics savetc;
 
-   /* see under SPI_commit() */
+   /* see comments in _SPI_commit() */
    if (_SPI_current->atomic)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
                 errmsg("invalid transaction termination")));
 
-   /* see under SPI_commit() */
+   /* see comments in _SPI_commit() */
    if (IsSubTransaction())
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
@@ -582,8 +582,11 @@ SPI_inside_nonatomic_context(void)
 {
    if (_SPI_current == NULL)
        return false;           /* not in any SPI context at all */
+   /* these tests must match _SPI_commit's opinion of what's atomic: */
    if (_SPI_current->atomic)
        return false;           /* it's atomic (ie function not procedure) */
+   if (IsSubTransaction())
+       return false;           /* if within subtransaction, it's atomic */
    return true;
 }
 
@@ -2411,9 +2414,12 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 
    /*
     * We allow nonatomic behavior only if options->allow_nonatomic is set
-    * *and* the SPI_OPT_NONATOMIC flag was given when connecting.
+    * *and* the SPI_OPT_NONATOMIC flag was given when connecting and we are
+    * not inside a subtransaction.  The latter two tests match whether
+    * _SPI_commit() would allow a commit; see there for more commentary.
     */
-   allow_nonatomic = options->allow_nonatomic && !_SPI_current->atomic;
+   allow_nonatomic = options->allow_nonatomic &&
+       !_SPI_current->atomic && !IsSubTransaction();
 
    /*
     * Setup error traceback support for ereport()
index 0a63b1d44ef1655e25412625a123d4061ca05784..ea7107dca0d901f3fd3f64b1b1b82c53fbde3f8f 100644 (file)
@@ -597,6 +597,26 @@ NOTICE:  f_get_x(1)
 NOTICE:  f_print_x(1)
 NOTICE:  f_get_x(2)
 NOTICE:  f_print_x(2)
+-- test in non-atomic context, except exception block is locally atomic
+DO $$
+BEGIN
+ BEGIN
+  UPDATE t_test SET x = x + 1;
+  RAISE NOTICE 'f_get_x(%)', f_get_x();
+  CALL f_print_x(f_get_x());
+  UPDATE t_test SET x = x + 1;
+  RAISE NOTICE 'f_get_x(%)', f_get_x();
+  CALL f_print_x(f_get_x());
+ EXCEPTION WHEN division_by_zero THEN
+   RAISE NOTICE '%', SQLERRM;
+ END;
+  ROLLBACK;
+END
+$$;
+NOTICE:  f_get_x(1)
+NOTICE:  f_print_x(1)
+NOTICE:  f_get_x(2)
+NOTICE:  f_print_x(2)
 -- test in atomic context
 BEGIN;
 DO $$
index 4cbda0382e95776fb3c36bbf567ef9de426a42c7..08c1659ef1564dc80f531d8a6e144901871df970 100644 (file)
@@ -557,6 +557,23 @@ BEGIN
 END
 $$;
 
+-- test in non-atomic context, except exception block is locally atomic
+DO $$
+BEGIN
+ BEGIN
+  UPDATE t_test SET x = x + 1;
+  RAISE NOTICE 'f_get_x(%)', f_get_x();
+  CALL f_print_x(f_get_x());
+  UPDATE t_test SET x = x + 1;
+  RAISE NOTICE 'f_get_x(%)', f_get_x();
+  CALL f_print_x(f_get_x());
+ EXCEPTION WHEN division_by_zero THEN
+   RAISE NOTICE '%', SQLERRM;
+ END;
+  ROLLBACK;
+END
+$$;
+
 -- test in atomic context
 BEGIN;