Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit cfc86f9

Browse files
committed
Fix SPI's handling of errors during transaction commit.
SPI_commit previously left it up to the caller to recover from any error occurring during commit. Since that's complicated and requires use of low-level xact.c facilities, it's not too surprising that no caller got it right. Let's move the responsibility for cleanup into spi.c. Doing that requires redefining SPI_commit as starting a new transaction, so that it becomes equivalent to SPI_commit_and_chain except that you get default transaction characteristics instead of preserving the prior transaction's characteristics. We can make this pretty transparent API-wise by redefining SPI_start_transaction() as a no-op. Callers that expect to do something in between might be surprised, but available evidence is that no callers do so. Having made that API redefinition, we can fix this mess by having SPI_commit[_and_chain] trap errors and start a new, clean transaction before re-throwing the error. Likewise for SPI_rollback[_and_chain]. Some cleanup is also needed in AtEOXact_SPI, which was nowhere near smart enough to deal with SPI contexts nested inside a committing context. While plperl and pltcl need no changes beyond removing their now-useless SPI_start_transaction() calls, plpython needs some more work because it hadn't gotten the memo about catching commit/rollback errors in the first place. Such an error resulted in longjmp'ing out of the Python interpreter, which leaks Python stack entries at present and is reported to crash Python 3.11 altogether. Add the missing logic to catch such errors and convert them into Python exceptions. This is a back-patch of commit 2e51781. That's now aged long enough to reduce the concerns about whether it will break something, and we do need to ensure that supported branches will work with Python 3.11. Peter Eisentraut and Tom Lane Discussion: https://postgr.es/m/3375ffd8-d71c-2565-e348-a597d6e739e3@enterprisedb.com Discussion: https://postgr.es/m/17416-ed8fe5d7213d6c25@postgresql.org
1 parent 419c727 commit cfc86f9

File tree

17 files changed

+535
-142
lines changed

17 files changed

+535
-142
lines changed

doc/src/sgml/spi.sgml

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,9 @@ int SPI_connect_ext(int <parameter>options</parameter>)
9999
<listitem>
100100
<para>
101101
Sets the SPI connection to be <firstterm>nonatomic</firstterm>, which
102-
means that transaction control calls <function>SPI_commit</function>,
103-
<function>SPI_rollback</function>, and
104-
<function>SPI_start_transaction</function> are allowed. Otherwise,
105-
calling these functions will result in an immediate error.
102+
means that transaction control calls (<function>SPI_commit</function>,
103+
<function>SPI_rollback</function>) are allowed. Otherwise,
104+
calling those functions will result in an immediate error.
106105
</para>
107106
</listitem>
108107
</varlistentry>
@@ -4414,15 +4413,17 @@ void SPI_commit_and_chain(void)
44144413
<para>
44154414
<function>SPI_commit</function> commits the current transaction. It is
44164415
approximately equivalent to running the SQL
4417-
command <command>COMMIT</command>. After a transaction is committed, a new
4418-
transaction has to be started
4419-
using <function>SPI_start_transaction</function> before further database
4420-
actions can be executed.
4416+
command <command>COMMIT</command>. After the transaction is committed, a
4417+
new transaction is automatically started using default transaction
4418+
characteristics, so that the caller can continue using SPI facilities.
4419+
If there is a failure during commit, the current transaction is instead
4420+
rolled back and a new transaction is started, after which the error is
4421+
thrown in the usual way.
44214422
</para>
44224423

44234424
<para>
4424-
<function>SPI_commit_and_chain</function> is the same, but a new
4425-
transaction is immediately started with the same transaction
4425+
<function>SPI_commit_and_chain</function> is the same, but the new
4426+
transaction is started with the same transaction
44264427
characteristics as the just finished one, like with the SQL command
44274428
<command>COMMIT AND CHAIN</command>.
44284429
</para>
@@ -4467,14 +4468,13 @@ void SPI_rollback_and_chain(void)
44674468
<para>
44684469
<function>SPI_rollback</function> rolls back the current transaction. It
44694470
is approximately equivalent to running the SQL
4470-
command <command>ROLLBACK</command>. After a transaction is rolled back, a
4471-
new transaction has to be started
4472-
using <function>SPI_start_transaction</function> before further database
4473-
actions can be executed.
4471+
command <command>ROLLBACK</command>. After the transaction is rolled back,
4472+
a new transaction is automatically started using default transaction
4473+
characteristics, so that the caller can continue using SPI facilities.
44744474
</para>
44754475
<para>
4476-
<function>SPI_rollback_and_chain</function> is the same, but a new
4477-
transaction is immediately started with the same transaction
4476+
<function>SPI_rollback_and_chain</function> is the same, but the new
4477+
transaction is started with the same transaction
44784478
characteristics as the just finished one, like with the SQL command
44794479
<command>ROLLBACK AND CHAIN</command>.
44804480
</para>
@@ -4498,7 +4498,7 @@ void SPI_rollback_and_chain(void)
44984498

44994499
<refnamediv>
45004500
<refname>SPI_start_transaction</refname>
4501-
<refpurpose>start a new transaction</refpurpose>
4501+
<refpurpose>obsolete function</refpurpose>
45024502
</refnamediv>
45034503

45044504
<refsynopsisdiv>
@@ -4511,17 +4511,12 @@ void SPI_start_transaction(void)
45114511
<title>Description</title>
45124512

45134513
<para>
4514-
<function>SPI_start_transaction</function> starts a new transaction. It
4515-
can only be called after <function>SPI_commit</function>
4516-
or <function>SPI_rollback</function>, as there is no transaction active at
4517-
that point. Normally, when an SPI-using procedure is called, there is already a
4518-
transaction active, so attempting to start another one before closing out
4519-
the current one will result in an error.
4520-
</para>
4521-
4522-
<para>
4523-
This function can only be executed if the SPI connection has been set as
4524-
nonatomic in the call to <function>SPI_connect_ext</function>.
4514+
<function>SPI_start_transaction</function> does nothing, and exists
4515+
only for code compatibility with
4516+
earlier <productname>PostgreSQL</productname> releases. It used to
4517+
be required after calling <function>SPI_commit</function>
4518+
or <function>SPI_rollback</function>, but now those functions start
4519+
a new transaction automatically.
45254520
</para>
45264521
</refsect1>
45274522
</refentry>

src/backend/executor/spi.c

Lines changed: 157 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ SPI_connect_ext(int options)
150150
* XXX It could be better to use PortalContext as the parent context in
151151
* all cases, but we may not be inside a portal (consider deferred-trigger
152152
* execution). Perhaps CurTransactionContext could be an option? For now
153-
* it doesn't matter because we clean up explicitly in AtEOSubXact_SPI().
153+
* it doesn't matter because we clean up explicitly in AtEOSubXact_SPI();
154+
* but see also AtEOXact_SPI().
154155
*/
155156
_SPI_current->procCxt = AllocSetContextCreate(_SPI_current->atomic ? TopTransactionContext : PortalContext,
156157
"SPI Proc",
@@ -208,20 +209,26 @@ SPI_finish(void)
208209
return SPI_OK_FINISH;
209210
}
210211

212+
/*
213+
* SPI_start_transaction is a no-op, kept for backwards compatibility.
214+
* SPI callers are *always* inside a transaction.
215+
*/
211216
void
212217
SPI_start_transaction(void)
213218
{
214-
MemoryContext oldcontext = CurrentMemoryContext;
215-
216-
StartTransactionCommand();
217-
MemoryContextSwitchTo(oldcontext);
218219
}
219220

220221
static void
221222
_SPI_commit(bool chain)
222223
{
223224
MemoryContext oldcontext = CurrentMemoryContext;
224225

226+
/*
227+
* Complain if we are in a context that doesn't permit transaction
228+
* termination. (Note: here and _SPI_rollback should be the only places
229+
* that throw ERRCODE_INVALID_TRANSACTION_TERMINATION, so that callers can
230+
* test for that with security that they know what happened.)
231+
*/
225232
if (_SPI_current->atomic)
226233
ereport(ERROR,
227234
(errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
@@ -234,40 +241,74 @@ _SPI_commit(bool chain)
234241
* top-level transaction in such a block violates that idea. A future PL
235242
* implementation might have different ideas about this, in which case
236243
* this restriction would have to be refined or the check possibly be
237-
* moved out of SPI into the PLs.
244+
* moved out of SPI into the PLs. Note however that the code below relies
245+
* on not being within a subtransaction.
238246
*/
239247
if (IsSubTransaction())
240248
ereport(ERROR,
241249
(errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
242250
errmsg("cannot commit while a subtransaction is active")));
243251

244-
/*
245-
* Hold any pinned portals that any PLs might be using. We have to do
246-
* this before changing transaction state, since this will run
247-
* user-defined code that might throw an error.
248-
*/
249-
HoldPinnedPortals();
252+
/* XXX this ain't re-entrant enough for my taste */
253+
if (chain)
254+
SaveTransactionCharacteristics();
250255

251-
/* Start the actual commit */
252-
_SPI_current->internal_xact = true;
256+
/* Catch any error occurring during the COMMIT */
257+
PG_TRY();
258+
{
259+
/* Protect current SPI stack entry against deletion */
260+
_SPI_current->internal_xact = true;
253261

254-
/* Release snapshots associated with portals */
255-
ForgetPortalSnapshots();
262+
/*
263+
* Hold any pinned portals that any PLs might be using. We have to do
264+
* this before changing transaction state, since this will run
265+
* user-defined code that might throw an error.
266+
*/
267+
HoldPinnedPortals();
256268

257-
if (chain)
258-
SaveTransactionCharacteristics();
269+
/* Release snapshots associated with portals */
270+
ForgetPortalSnapshots();
259271

260-
CommitTransactionCommand();
272+
/* Do the deed */
273+
CommitTransactionCommand();
261274

262-
if (chain)
263-
{
275+
/* Immediately start a new transaction */
264276
StartTransactionCommand();
265-
RestoreTransactionCharacteristics();
277+
if (chain)
278+
RestoreTransactionCharacteristics();
279+
280+
MemoryContextSwitchTo(oldcontext);
281+
282+
_SPI_current->internal_xact = false;
266283
}
284+
PG_CATCH();
285+
{
286+
ErrorData *edata;
267287

268-
MemoryContextSwitchTo(oldcontext);
288+
/* Save error info in caller's context */
289+
MemoryContextSwitchTo(oldcontext);
290+
edata = CopyErrorData();
291+
FlushErrorState();
269292

270-
_SPI_current->internal_xact = false;
293+
/*
294+
* Abort the failed transaction. If this fails too, we'll just
295+
* propagate the error out ... there's not that much we can do.
296+
*/
297+
AbortCurrentTransaction();
298+
299+
/* ... and start a new one */
300+
StartTransactionCommand();
301+
if (chain)
302+
RestoreTransactionCharacteristics();
303+
304+
MemoryContextSwitchTo(oldcontext);
305+
306+
_SPI_current->internal_xact = false;
307+
308+
/* Now that we've cleaned up the transaction, re-throw the error */
309+
ReThrowError(edata);
310+
}
311+
PG_END_TRY();
271312
}
272313

273314
void
@@ -287,6 +328,7 @@ _SPI_rollback(bool chain)
287328
{
288329
MemoryContext oldcontext = CurrentMemoryContext;
289330

331+
/* see under SPI_commit() */
290332
if (_SPI_current->atomic)
291333
ereport(ERROR,
292334
(errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
@@ -298,34 +340,68 @@ _SPI_rollback(bool chain)
298340
(errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
299341
errmsg("cannot roll back while a subtransaction is active")));
300342

301-
/*
302-
* Hold any pinned portals that any PLs might be using. We have to do
303-
* this before changing transaction state, since this will run
304-
* user-defined code that might throw an error, and in any case couldn't
305-
* be run in an already-aborted transaction.
306-
*/
307-
HoldPinnedPortals();
343+
/* XXX this ain't re-entrant enough for my taste */
344+
if (chain)
345+
SaveTransactionCharacteristics();
308346

309-
/* Start the actual rollback */
310-
_SPI_current->internal_xact = true;
347+
/* Catch any error occurring during the ROLLBACK */
348+
PG_TRY();
349+
{
350+
/* Protect current SPI stack entry against deletion */
351+
_SPI_current->internal_xact = true;
311352

312-
/* Release snapshots associated with portals */
313-
ForgetPortalSnapshots();
353+
/*
354+
* Hold any pinned portals that any PLs might be using. We have to do
355+
* this before changing transaction state, since this will run
356+
* user-defined code that might throw an error, and in any case
357+
* couldn't be run in an already-aborted transaction.
358+
*/
359+
HoldPinnedPortals();
314360

315-
if (chain)
316-
SaveTransactionCharacteristics();
361+
/* Release snapshots associated with portals */
362+
ForgetPortalSnapshots();
317363

318-
AbortCurrentTransaction();
364+
/* Do the deed */
365+
AbortCurrentTransaction();
319366

320-
if (chain)
321-
{
367+
/* Immediately start a new transaction */
322368
StartTransactionCommand();
323-
RestoreTransactionCharacteristics();
369+
if (chain)
370+
RestoreTransactionCharacteristics();
371+
372+
MemoryContextSwitchTo(oldcontext);
373+
374+
_SPI_current->internal_xact = false;
324375
}
376+
PG_CATCH();
377+
{
378+
ErrorData *edata;
325379

326-
MemoryContextSwitchTo(oldcontext);
380+
/* Save error info in caller's context */
381+
MemoryContextSwitchTo(oldcontext);
382+
edata = CopyErrorData();
383+
FlushErrorState();
327384

328-
_SPI_current->internal_xact = false;
385+
/*
386+
* Try again to abort the failed transaction. If this fails too,
387+
* we'll just propagate the error out ... there's not that much we can
388+
* do.
389+
*/
390+
AbortCurrentTransaction();
391+
392+
/* ... and start a new one */
393+
StartTransactionCommand();
394+
if (chain)
395+
RestoreTransactionCharacteristics();
396+
397+
MemoryContextSwitchTo(oldcontext);
398+
399+
_SPI_current->internal_xact = false;
400+
401+
/* Now that we've cleaned up the transaction, re-throw the error */
402+
ReThrowError(edata);
403+
}
404+
PG_END_TRY();
329405
}
330406

331407
void
@@ -340,38 +416,55 @@ SPI_rollback_and_chain(void)
340416
_SPI_rollback(true);
341417
}
342418

343-
/*
344-
* Clean up SPI state. Called on transaction end (of non-SPI-internal
345-
* transactions) and when returning to the main loop on error.
346-
*/
347-
void
348-
SPICleanup(void)
349-
{
350-
_SPI_current = NULL;
351-
_SPI_connected = -1;
352-
/* Reset API global variables, too */
353-
SPI_processed = 0;
354-
SPI_tuptable = NULL;
355-
SPI_result = 0;
356-
}
357-
358419
/*
359420
* Clean up SPI state at transaction commit or abort.
360421
*/
361422
void
362423
AtEOXact_SPI(bool isCommit)
363424
{
364-
/* Do nothing if the transaction end was initiated by SPI. */
365-
if (_SPI_current && _SPI_current->internal_xact)
366-
return;
425+
bool found = false;
367426

368-
if (isCommit && _SPI_connected != -1)
427+
/*
428+
* Pop stack entries, stopping if we find one marked internal_xact (that
429+
* one belongs to the caller of SPI_commit or SPI_abort).
430+
*/
431+
while (_SPI_connected >= 0)
432+
{
433+
_SPI_connection *connection = &(_SPI_stack[_SPI_connected]);
434+
435+
if (connection->internal_xact)
436+
break;
437+
438+
found = true;
439+
440+
/*
441+
* We need not release the procedure's memory contexts explicitly, as
442+
* they'll go away automatically when their parent context does; see
443+
* notes in SPI_connect_ext.
444+
*/
445+
446+
/*
447+
* Restore outer global variables and pop the stack entry. Unlike
448+
* SPI_finish(), we don't risk switching to memory contexts that might
449+
* be already gone.
450+
*/
451+
SPI_processed = connection->outer_processed;
452+
SPI_tuptable = connection->outer_tuptable;
453+
SPI_result = connection->outer_result;
454+
455+
_SPI_connected--;
456+
if (_SPI_connected < 0)
457+
_SPI_current = NULL;
458+
else
459+
_SPI_current = &(_SPI_stack[_SPI_connected]);
460+
}
461+
462+
/* We should only find entries to pop during an ABORT. */
463+
if (found && isCommit)
369464
ereport(WARNING,
370465
(errcode(ERRCODE_WARNING),
371466
errmsg("transaction left non-empty SPI stack"),
372467
errhint("Check for missing \"SPI_finish\" calls.")));
373-
374-
SPICleanup();
375468
}
376469

377470
/*

0 commit comments

Comments
 (0)