3

I found out that postgres + PDO auto rollbacks previous changes when an exception is thrown (EVEN WHEN THE EXCEPTION IS CAUGHT AND SWALLOWED!). Example (in pseudo-code):

  $transaction->begin();
  try {
     $manager->insert("INSERT ...");
     try {
       $manager->exec("A QUERY BREAKING SOME DB CONSTRAINT LIKE A UNIQUE INDEX ...");
     } catch (\Exception $ex) {
        // IT IS CAUGHT AND SWALLOWED!
     }
     $transaction->commit();
  } catch (Exception $ex) {
     $transaction->rollback(); // THIS CLEARLY DOES NOT RUN!
  }

In postgres the first insert gets reverted. In mysql no.

Can anyone throws some light on the matter? Is it possible to change this ridiculous behaviour? I would like to perform my rollbacks myself and not get pg to do it when he thinks it is appropriate.

nourdine
  • 7,407
  • 10
  • 46
  • 58
  • Well, after second thought @samitha's question could be interpreted in a quite sensible way. With myisam engine no inserts will be reverted, no matter if there was a rollback or no. – Your Common Sense Mar 30 '14 at 11:26
  • I have no light at hand to shed, but I have a friendly advise. Instead of sketch of pseudocode full of assumptions, create a consistent and error-proof test case. – Your Common Sense Mar 30 '14 at 11:28
  • @YourCommonSense it is innodb (who uses myisam anymore?) – nourdine Mar 30 '14 at 13:01

1 Answers1

3

That's not PDO's fault, it's inherent to PostgreSQL's transaction management. See:

PostgreSQL doesn't roll the transaction back, but it sets it to an aborted state where it can only roll back, and where all statements except ROLLBACK report an error:

ERROR: current transaction is aborted, commands ignored until end of transaction block

(I'm surprised not to find this referred to in the official documentation; think I'll need to write a patch to improve that.)

So. When you try/catch and swallow the exception in PDO, you're trapping a PHP-side exception, but you're not changing the fact that the PostgreSQL transaction is in an aborted state.

If you wanted to be able to swallow exceptions and keep on using the transaction, you must create a SAVEPOINT before each statement that might fail. If it fails, you must ROLLBACK TO SAVEPOINT ...;. If it succeeds you may RELEASE SAVEPOINT ...;. This imposes extra overhead on the database for transaction management, adds round-trips, and burns through transaction IDs faster (which means PostgreSQL has to do more background cleanup work).

It is generally preferable to instead design your SQL so it won't fail under normal circumstances. For example, you can validate most constraints client-side, treating the server side constraints as a second level of assurance while trapping most errors client-side.

Where that's impractical, make your application fault tolerant, so it can retry a failed transaction. Sometimes this is necessary anyway - for example, you can't generally use savepoints to recover from deadlocks transaction aborts or serialization failures. It can also be useful to keep failure-prone transactions as short as possible, doing just the minimum work required, so you have less to keep track of and repeat.

So: Where possible, instead of swallowing an exception, run failure-prone database code in a retry loop. Make sure your code keeps a record of the information it needs to retry the whole transaction on error, not just the most recent statement.

Remember, any transaction can fail: The DBA might restart the database to apply a patch, the system might run out of RAM due to a runaway cron job, etc. So failure tolerant apps are a good design anyway.

Props to you for at least using PDO exceptions and handling exceptions - you're way ahead of most devs already.

Community
  • 1
  • 1
Craig Ringer
  • 307,061
  • 76
  • 688
  • 778
  • So the only method that could be executed when we get into an aborted state (which is the case every time an exception is thrown) is `$transaction->rollback()` but in my case I am not executing it as it is in the `catch` and the interpreter does not go there as the exception in swallowed. Is that it? Therefore bottom line is: never hide exception from the catch block where the rollback will happen. Can you confirm? I am looking out for reassurance today :) – nourdine Mar 30 '14 at 14:05
  • 1
    Strictly, the transaction enters the aborted state only when an exception in PHP is thrown *as a result of an error `SQLSTATE` returned by PostgreSQL*. It's possible for *client-side-only* exceptions like invalid query parameters to leave the transaction valid and intact, because nothing actually went to the server. – Craig Ringer Mar 31 '14 at 00:52
  • As for the specifics of the client-side exception handling, I cannot usefully reason from incomplete psueodocode examples. In any case, I don't know the details and quirks of PHP's client side exceptions as I rarely do anything with PHP. From what you've sketched out above it *looks* like the rollback is unreachable when your constraint-violating exception occurs, though, yes. – Craig Ringer Mar 31 '14 at 00:54
  • One last thing that follows from your answer; you said: "PostgreSQL doesn't roll the transaction back, but it sets it to an aborted state where it can only roll back ...". That is understandable. I am happy with all PHP statements (but a rollback) to report that error. The only thing I cannot understand is why, in actual facts, the transaction is rolled back even though you do not do it programmatically via PHP. Is it maybe because: "When the script ends or when a connection is about to be closed ..." (taken from: http://www.php.net/manual/en/pdo.transactions.php) – nourdine Mar 31 '14 at 09:29
  • @nourdine Yes, that's the most likely explanation. Also, if you close a connection that has an open transaction without an explicit commit or rollback, PostgreSQL its self will always roll it back, never commit it. – Craig Ringer Mar 31 '14 at 12:02
  • @CraigRinger I'm experiencing the same issue, but I'm running SQL right from pgAdmin III. After an error inside a transaction, the transaction does _not_ enter the ABORT STATE but silently rolls back. Then, if I issue a `ROLLBACK`, it says there is no transaction. Should I create a new question? – Adam Nov 17 '14 at 22:27
  • @Adam Yes; link to this one in the new question – Craig Ringer Nov 18 '14 at 03:47
  • @CraigRinger Never mind :) pgAdmin has a "Enable Auto ROLLBACK" option, enabled by default... – Adam Nov 19 '14 at 22:11