Problem/Motivation
At the sprint today we discussed an issue (which I do not have the nid for but will fill in later) with toolbar/Extend page - where after installing a module, the next request has a menu link available, but no router entry yet.
If we both enabled the module and rebuilt the router in a transaction (we don't at the moment), and always use the REPEATABLE READ (or more consistent than that) isolation level (we don't at the moment either) then it should be impossible to have a race condition.
As it is, the module is enabled in the database, before its routing entries are added to the database, so the Extend page could potentially be visited by another request in-between, and will fatal with a not-found route because it tries to generate one to the help/permissions pages.
Proposed resolution
Recommend REPEATABLE READ in both drupal.org/requirements and hook_requirements() somewhere.
Remaining tasks
User interface changes
Possibly a hook_requirements() addition. Changes to http://drupal.org/requirements
API changes
Possibly additions.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #64 | drupal-n2572283-64_incomplete.patch | 52.66 KB | damienmckenna |
| #59 | 2572283-fix-transactions.patch | 78.2 KB | david_garcia |
| #16 | interdiff.txt | 9.43 KB | mradcliffe |
| #16 | drupal-2572283-transaction-isolation-level-16.patch | 13.7 KB | mradcliffe |
| #12 | interdiff.txt | 2.25 KB | mradcliffe |
Comments
Comment #2
catchI knew we'd discussed this before.
Comment #3
mradcliffeI think it would be possible to set the transaction isolation level for a connection. Doctrine's DBAL allows this in Connection::setTransactionIsolation(). Then it could be possible to change between isolation levels depending on what's necessary.
Comment #4
mradcliffeHere is something I banged out in a short while. I am not sure if the function on Connection should be abstract. If in RC, then it would break drivers. A default function could be provided. Also not sure if the Trait has that much value. No tests yet and this patch isn't really tested at all.
Comment #5
mradcliffeI forgot to add the new file in.
Comment #6
regilero commentedHaving the ability to alter the isolation level is a must. Because it could certainly be very important to control this while installs, for example, or even on some very specific jobs (like going to serializable when manipulating monney on a real accountability module, some day).
But curently the way fields are saved with delete+insert is doing too much locks on MySQL in REPEATABLE READ mode. as referenced in #1650930. We could also say that the current way of handling locks in transation (suicide) is not very robust (one could instead try to restart failing transactions), and deadlocks are quite frequent on big Drupal websites, not only because of the fields saves. READ COMMITTED is maybe a better default for Drupal, IMHO, especially if you can elevate this level whil performing critical tasks like installations.
Comment #7
mradcliffeOkay, let's try adding a setter to the Transaction class (and move stuff from previous patch to Transaction). This will probably fail the tests and/or crash horribly.
I removed the system requirements as well.
Comment #8
mradcliffeFix namespace issue in test.
Comment #9
mradcliffeFlip back to needs work.
Comment #10
mradcliffeFix typos.
I guess since RC1 is imminent that means most of those abstracts need to be generic implementations so that contrib drivers that support transactions don't break post rc1.
Comment #12
mradcliffeMore typos as I run via my drupalci local instance.
Also found a bug in MySQL < 5.5 where set transaction affects the entire session even if it's only in a transaction.
Comment #13
catch@mradcliffe given the history of this issue (flip flopping between data loss via race conditions vs. deadlocks), and us not having viable 8.x contrib database drivers, we can probably just change the API during RC. If it's as simple to not break BC then that's fine too of course.
Comment #14
mradcliffeHmm... It looks like MySQL and MySQL-likes have the behavior of calling SET TRANSACTION outside of a transaction ("Active sql transaction: 1568 Transaction isolation level can't be changed while a transaction is in progress"). While PostgreSQL and SQL Server allow for setting isolation level within transactions.
So I think that we need to move this back to Connection class for the drivers, and that the mysql driver shouldn't allow attempting to change transaction isolation when inside a transaction.
Comment #16
mradcliffeLooking at the docs more closely, mysql and postgresql implement the opposite of each other, and so each driver must extend the constuctor of Transaction before (or after) the transaction is begun respectively. I am not sure about sqlite, so we'll see.
This should also be backwards-compatible and now startTransaction() can be called by the application. I like that better as the setter can be a protected method.
Comment #19
mradcliffeThis may not be doing what I expect it to do because it's the session variable. I do not think there is a way to get the isolation level that was set for a transaction in MySQL. :(
Possibilities:
I think that this should be an assert() instead of an Exception these days. Apologies to AkiTendo.
This will not work for SQLite. I should go back to bitwise operator to determine the level. Might be a good idea to add that to the Trait and add an assertion.
Comment #20
xjmLooks like this was filed with the rc target tag, but that seems wrong? https://groups.drupal.org/node/484788
Comment #21
mradcliffeI don't know if that still applies, but catch added it so I think it falls under "other issues at committer discretion" for performance/stability reasons.
Comment #22
catchDepending on which setting you have in MySQL, you can end up with either fatal errors or data loss, so the RC target tag was on purpose for me.
Comment #23
catchComment #24
david_garcia commentedFor a long time we've had this in the Drupal 7 Driver for Drupal (and it is also available in the D8 port).
The current DTBNG should be extended to consider 2 things:
- Allow to specifiy a desired isolation level when starting a transaction
- Allow to specify how transaction nesting should work when nesting transactions
I did a mimic of the .Net approach to transaction management in the MSSQL driver.
To the proposed patch I have a few weak points... (just made a quick read of the patch and remembered the work I did for MSSQL):
- There is no concept or treatment for default isolation levels. Databases can have different isolation levels by default that can be setup by the user.
- The DSN used to connect to the database can have a transaction level defined in it. This one can be the same or differ from the default transaction level configured in the database.
- Default transaction levels need to be restored after transaction commits and rollbacks, I can't see this in the current implementation. Code outside a transaction is also - soft of - transactional and can under some circumstances be affected by the ambient transaction mode.
- Nested transactions have not been taken into account. We should add flexibility on to how nested transactions work, and acknowledge that isolation levels might collide depending on how this nesting behaviour works. This is what I did in the MSSQL driver:
You can choose how nesting should work when starting a transaction, the default is Required.
- It would be good to somewhat fix the implicit commit behaviour of transactions in Drupal once and for all, even if it had to be explictly requested to not break BWC. Implicit commits are a danger, every other thing out there is using implicit rollbacks. This is also implemented in the MSSQL driver for Drupal.
I'd love to finally see this fixed to get some decent transaction management in Drupal :)
Comment #25
mradcliffeIt's already possible for a database administrator to set the global isolation level or for it to be configured in the DSN as a session isolation level. We want the database layer to change what the database admin or user configured so that Drupal doesn't crash or cause data loss because of the global or session configuration.
@davidgarcia, can you clarify what you mean with regard to this issue?
It looks like this is dependent on SQL implementation:
So I think that the driver for sqlite and sqlsrv will need to implement behavior as necessary to restore isolation level (if supporting isolation levels).
I think this is out of scope of the issue, and isolation level testing should follow ept/hermitage tests as well as the current tests in Drupal\system\Tests\Database\TransactionTest with regard to nesting.
Agreed, but Drupal's installer depends on implicit commits because it is ok to send queries when a table may or may not exist in a transaction and then try to create them in the same transaction. :( I'm not sure if changing that behavior should be in scope of this issue.
Comment #26
pounardPostgreSQL allows to set the transaction level for the transaction itself, and handles the reset itself (e.g. START TRANSACTION ISOLATION LEVEL [LEVEL]) and if I remember correctly, Oracle allows you to the same. Actually any SGBD if is fully ACID compliant should allow you to do the same. MySQL is fundamentally broken by design, so I guess that trying to fix it at the software level won't be an easy task, and should be a MySQL-only fix.
But in order to make things right, I think that the transaction level should be exposed and setable at the DBTNG transaction object instance creation time and chosen by the developer, and documentation should warn that MySQL does not handle this by itself and might be error prone. Then, the fix could be done in the MySQL-specific transaction class, the only thing we'd need is to change the session transaction level when it starts, and reset it when it when it rollbacks or commits. Thanksfully PHP being stateless and because it doesn't do any threading, it's not possible for any other thread or concurrent access to use the same session, so it would work.
I can see one only use case where changing the session transaction level for a single transaction would cause damage and it is when the software over-uses yield and yield from in order to provide asynchronous coroutines to emulate degraded pseudo threaded code (which today isn't what Drupal does, since yield from is PHP 7 only).
The right transaction level should always be explicitly set by the developer depending on how safe should be transaction at the moment he does write his piece of software, and leave alone the global context and other non-related code alone, if I remember correctly my hardcore Oracle and database lessons from when I was a student.
Comment #27
david_garcia commentedConsidering that I have all this already implemented in the MSSQL driver - and tested in production for quite a while now - and that we all know what happens to open source problems - the more niche it becomes, the less chance it will ever get done even if an individual tries to push it forward - and seeing there is real interest in properly fixing transaction once and forever in DTBNG and that we've got real code working, this might be the chance to get this done.
I'll be trying to port the MSSQL implentation to core and see what the result looks like.
I'll be widening the scope of the patch to:
Comment #28
david_garcia commentedThis is more or less what I had in mind, ported from the MSSQL driver. No tests yet.
I just tested that it at least passes install in MySQL, let's see what the test bot thinks about it.
Comment #30
pounard$this->connection->query('SET TRANSACTION ISOLATION LEVEL ' . $level_string);Such code should not be generic, drivers that supports
START TRANSACTION ISOLATION LEVEL ...should use native feature instead of application (Drupal) side code to emulate it.Comment #31
mradcliffeThis is a completely different feature request than improving transaction isolation support with vastly more tests and core functionality that need to be changed. I do not agree at all with adding this to the scope of this issue.
I think tests should be written first so that we know what part of the spec we're aiming for. The tests are probably the most complex part of this.
From #19, this is not going to work when inside of a transaction. MySQL does not provide a way to get the current transaction isolation level, only the session or global setting. :(
Should be DatabaseWrapperException, but I don't think this should throw an exception. It should return the SERIALIZABLE or READ UNCOMMITTED (if pragma is set).
Comment #32
david_garcia commentedI did some bugfixes, hope this one does not annoy the testbot so much.
Because we are using PDO, the prefered way of starting a transaction is PDO::beginTransaction() - instead of issuing a START TRANSACTION ... - that does not allow to specifiy the isolation level when being called.
I remember having issues when doing the MSSQL implementation because I wanted to bypass PDO::beginTransaction() in favor of issuing a START TRANSACTION... statement and after lots of trouble I remember reading somewhere that if you are using PDO you are expected to use PDO related transaction functions.
If we use PDO and want to start a transaction with a specific isolation level this is how I believe it should be done and how it is done in the proposed patch:
1. Change the ambient transaction isolation level (SET TRANSACTION ISOLATION LEVEL .... ) only if it is different from the ambient transaction isolation level
2. Call beingTransaction()
3. Do our stuff
4. call PDO::commit() or PDO::rollback()
5. Restore the ambient transacction level - if it was different from the one we specified we are using
On the other hand, if we intend to use START TRANSACTION ISOLATION LEVEL... statements then we cannot consume PDO's transaction related functions (commit, rollback, beginTransaction)
Something that I want to insist is on the fact the the ambient isolation level has an impact on application performance. Statements that are not in a transaction also have an isolation level - that determines how they interact with data being dealt with inside active transactions.
Not in the patch, but it would be interesting to determine what is the best ambient isolation level for Drupal and explictly tell the drivers to issue a DSN with that isolation level, and provide means for the user to override this, maybe in settings.php.
Comment #35
david_garcia commentedWhy does the testbot insist in running Sqlite code if I only want to test MySQL? :(
Will have to fix the Sqlite implementation I guess....
Comment #36
david_garcia commentedRight... I made sure that this can only be run when there is no active transaction. I am only using this to obtain the initial ambient transaction so that it can be restored.
Can be discussed... I believe all these changes are inextricably intertwined, but of course they could be scoped independantly. Just let me work on these as a whole, in the end it's just about removing stuff or going back to the original patch in #16.
I don't believe in test driven development for architectural things.... test driven development is to get things done fast by reducing scope as much as possible... but won't fit very well for architectural things that need a complete wide and complete vision of what is wrong and what needs to be right.
So I've writen quite some tests... not all finished... I had to modify the Connectionclass :( to be able to have two active independant connections in order to test transaction behaviour. ¿Is there any other way of getting two independent connection instances for testing purposes?
The patch and tests are not 100% finished, but very close to what should be a definitive implementation if we opt to go for the extended scope, and if not most of the code is still valid and reusable.
I'm not sure on how to write the repeatable read test, one of the connections should get locked until the other is finished, but without multithreading testing this will be a headache.
Back to @punard comment in #30, my answer is in #32.
Comment #37
david_garcia commentedI just want the test bot to run now... :(
Comment #39
pounardI think that the issue problem can not be solved without this. If a specific MySQL transaction level does not work for some code paths, and another for some other, then the whole transaction handling logic needs to be changed.
Comment #40
catchThat's mostly for things like cache/queue/key-value where we expect sites to use non-transactional storage (like memcache, redis) in production. #2347867: Race conditions with lock/cache, session storage - add a non-transactional database connection should remove that problem entirely.
Comment #41
david_garcia commentedThere you go, everything completed with full test coverage.
Backwards compatibility should be 100% guaranteed.
Now it's time to get the green light with the test bot with all database engines.
Comment #43
david_garcia commentedComment #45
david_garcia commentedComment #46
david_garcia commentedGetting this right is more difficult than what I was expecting :(
Comment #47
david_garcia commentedAnd this should fix Sqlite's crippled transaction support. And the patch gets bigger and bigger :(
Comment #49
david_garcia commentedAnd I hope to get two green lights with this one...
Comment #50
david_garcia commentedAnd this should fix postgre issues too.
Comment #51
david_garcia commentedComment #52
david_garcia commentedComment #53
david_garcia commentedComment #58
david_garcia commentedLet's see what the test bot thinks about this.
Comment #59
david_garcia commentedComment #60
david_garcia commentedJackpot! This is ready for real review and to decide if scope got too much out of hand.
Comment #62
damienmckennaBumping to 8.2.x.
Comment #64
damienmckennaRerolling the patch... there's already a core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php file, only it extends DatabaseTestBase instead of KernelTestBase. So this file is missing that file.
Comment #65
david_garcia commentedI believe the last patch is overlapping with:
#2605284: Testing framework does not work with contributed database drivers
That one should be fixed first, then this one rerolled.
Comment #70
wim leersFYI: MySQL made
READ-COMMITTEDits new default in version 5.7.Comment #72
niteman commented@Wim no, they didn't. According to docs
REPEATABLE READis the default for both:Comment #75
andypostAs 9.0 requires 5.7 as minimum this issue only about 8.x
Comment #77
geek-merlinThis related issue adds a transaction paramater, which sounds quite reasonable.
Comment #81
catchThe route rebuild issue was fixed by #2589967: Rebuild routes immediately when modules are installed and we haven't had any reports since with READ COMMITTED, so I think it was the lack of transaction that was the problem, not the isolation level.