Problem/Motivation
Once a query fails as part of a transaction Postgres (unlike Mysql, SQLite and Oracle) will prevent further queries from using the transaction. See https://stackoverflow.com/questions/10399727/psqlexception-current-trans... for more information about how postgres behaves. Allowing inserts to fail within a transaction is especially important because of the way we automatically create cache bins on demand - see \Drupal\Core\Cache\DatabaseBackend for how this works.
Proposed resolution
Add a savepoint to the postgres insert query - this is already done for other query types, for example, updates and selects.
Remaining tasks
User interface changes
None
API changes
None
Data model changes
None
Original issue summary
PDO randomly throws errors for any table (mostly seen for queue and flood during normal site operation) when using PGBouncer in transaction mode on PostgreSQL:
PDOException: SQLSTATE[55000]: Object not in prerequisite state: 7 ERROR: currval of sequence "queue_item_id_seq" is not yet defined in this session: INSERT INTO queue (name, data, created) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2);
When doing lots of inserts (e.g. setting up a new site) it will break the installation.
| Comment | File | Size | Author |
|---|---|---|---|
| #28 | 2487269-28.patch | 7.75 KB | alexpott |
| #28 | 21-28-interdiff.txt | 1.7 KB | alexpott |
| #21 | 2487269-21.patch | 7.7 KB | alexpott |
| #21 | 20-21-interdiff.txt | 1.63 KB | alexpott |
| #20 | 2487269-20.patch | 7.59 KB | alexpott |
Comments
Comment #1
David_Rothstein commentedThe existing code looks the same in Drupal 8, so I think this needs to be fixed there too (then backported to Drupal 7)?
Comment #2
zhuber commentedI've patched this for Drupal 8.
Comment #3
jhedstromThis approach makes sense, but I'm not sure how to manually reproduce the issue in order to test. I'm guessing automated tests won't be able to reproduce this issue either?
Comment #4
Oliver Sommersberg commentedIt was pretty hard to find and only testable in live environment by checking error logs the next day. I can not think of any reasonable way to test it.
I can just say that we use it on our pages since then and we did not have any issues. (I know - this is not a good method to patch Core and thats why i havent pushed it to D7 master)
For reproduction you need at least Postgres, PGBouncer (with pretty low client connection count), some page requests that use DB connections while some other requests do Insert Queries (with lastinsert). In the end it is pretty random.
The more complicated patch (which i tried first) was changing the querystring by adding "RETURNING [serial-field-name]". I can put the code for it somewhere if needed - might be a better solution but more complicated.
Comment #5
mradcliffeSpelling and maybe a re-word.
"The transaction will commit after $transaction loses scope here."
I'm okay with explanatory comments. :-)
Should this catch \PDOException, and then rethrow DatabaseExceptionWrapper in Drupal 8?
Comment #6
hazaI just got those kind of errors in a Postgres with a master/slave architecture, when loging/deloging a user (through a CAS Server).
We are not using PbBounce.
The patch above solves the issue.
I'm wondering if we are solving an issue with this patch, or if we are implementing a workaround for a bug in PG ?
Comment #7
alexpottThis is also causing problems for auto creation of tables like cache. See #2664466: url_alias table is only used by a core service but it depends on system install. Basically postgres behaves differently from both sqlite and mysql in the once a transaction has an error it can not continue using that transaction. All of the database actions create a savepoint to protect us against this but insert does not.
Comment #8
alexpottI've added a test and ensure that this works and now the patch works the same way as all of the other postgres query types with the exception of the NativeUpsert which needs fixing too. No interdiff to #2 because the patch was developed without knowledge of this issue.
Comment #9
alexpottre-titling to explain the actual bug and adding info to the summary.
Comment #10
alexpottAs well as NativeUpsert queries it looks like Merge queries might have the issue too.
Comment #11
alexpottSo postgres merge queries don't have the issue because they are just combinations of select, insert and update queries. So the only one that will fail is the NativeUpsert - but I can't test that because I don't have postgres 9.5 installed... However I've added tests for all the query types so at least we have the test in place.
Comment #12
alexpottHere's a patch for the NativeUpsert - it'd be great if someone can run it against a postgres db on 9.5 or above... the test to run is
Drupal\system\Tests\Database\TransactionTest::testQueryFailureInTransactionComment #13
alexpottOops forgot to release the savepoint in the NativeUpsert - I've asked @jsmith to test this.
Comment #14
alexpottNow for the even more complex situation with the query method...
Comment #15
mradcliffeI don't have a vm running 9.5 handy at the moment, but probably can get that up and running to test some time this week.
Comment #16
jaredsmith commentedI have tested this on a Fedora 24 (rawhide) installation with PostgreSQL 9.5.0 and PHP 5.6.18.
With the test-only patch from comment 11, the test on Drupal\system\Tests\Database\TransactionTest fails with one exception:
With the patch from comment 12, the tests pass with no exceptions.
Comment #17
jaredsmith commentedI have also tested the patch in comment 14, and it passes with no errors or exceptions.
Comment #19
alexpottI'm not entirely sure about the changes in #14. We need to identify savepoint related queries in order to make postrgres work the same way as the other databases. If we don't identify the savepoint queries we end up in a infinite loop :( But adding the option feels weird and unreliable. For example Mysql connection contains:
So in theory we'd have to add the option to this as well to be consistent....
Setting back to "needs review" to do some more thinking about this.
Comment #20
alexpottI think this is a cleaner solution - there is no point wrapping savepoint queries in savepoints :) but also postgres implementation details should not bleed into the main database code.
Comment #21
alexpottActually we can improve this since we only need to worry about this if we're in a transaction.
Comment #22
mradcliffeAlso there is #2572283: Neither REPEATABLE READ nor READ COMMITTED transaction isolation levels are always appropriate, in which the latest patch makes changes to the implicit commit handling and makes it more flexible for all database drivers. Unfortunately nobody has reviewed @david_garcia's work yet for all database drivers.
Comment #23
alexpott@mradcliffe that issue is addressed something different afaics. This issue is addressing a difference between postgres and the other database drivers.
Comment #24
dawehnerOne thing I don't understand about this patch. For insert/merge etc. queries we now create two savepoints, all the time. Is this actually needed or would it be enough to create just one
around the entire
insert(for example) and skip the query ones?On top of that I'm also wondering how costly it is to set savepoints for potentially many queries, when you are in a transaction? I guess this would be basically for free?
is there a reason we use case insensitive comparison here?
Comment #25
alexpottWe only create savepoints if we're in a transaction - ie. where it is necessary.
And release and rollback only work if the savepoint exists
Comment #26
Oliver Sommersberg commentedPlease keep the original bug report in mind that without a transaction "insert" with "last-insert-id" can break (when using pgbouncer in transaction mode).
Comment #27
alexpottBut @dawehner you are on to something - we shouldn't be creating a 'query_mimic_implicit_commit' savepoint if a 'mimic_implicit_commit' already exists.
Comment #28
alexpottComment #29
dawehnerI'm not sure, I would have passed along an additional option to stop adding the savepoint instead.
Comment #30
alexpott@dawehner see #14 and the comments up to the new patch. I didn't like the way that that was going.
Comment #31
alexpottBasically that would mean if you add your own savepoint you'd need to indicate that it is a savepoint query.
Comment #32
catchI'm not sure that's such a bad requirement?
Comment #33
dawehnerWell, the question is whether this would be so easy to do. This basically makes it impossible to call out to other code, as that code cannot know whether you have added your own savepoint or not.
Comment #34
alexpottTo me adding an option to say this is a save point query is breaking the idea of database encapsulation - people writing cross database code have to know stuff to make Postgres work - with the current patch they don't.
Comment #35
dawehnerAlex made some convincing arguments.
Comment #38
catchYes that's fair enough. Committed/pushed to 8.1.x and cherry-picked to 8.0.x. Thanks!
Moving to 7.x for backport.
Comment #43
poker10 commentedI have created a D7 issue for backporting this: #3384672: [D7] PostgreSQL queries that fail in a transaction break the entire transaction
Closing as Fixed for D8, so that credits get assigned.