Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Instead of using a transaction only in the outer-most scope, we could be using savepoints to simulate native transaction nesting and partial rollback. I knew MySQL supported this, but I discovered earlier today that all of our supported databases do.
Basically, we would create a new named savepoint with each nested "transaction." On each nested "commit," we tell the DB to retire the savepoint (this is just for performance). On rollback, we roll back to the current level's savepoint. To preserve transaction atomicity, the outermost operation would still need to be a real transaction.
Comment | File | Size | Author |
---|---|---|---|
#99 | 669794-log-rollback-d7.patch | 2.74 KB | andypost |
#98 | trans.diff | 1.04 KB | moshe weitzman |
#96 | trans.diff | 443 bytes | moshe weitzman |
#91 | 669794-followup-d7.patch | 1.23 KB | andypost |
#89 | savepoint_revert.patch | 17.13 KB | catch |
Comments
Comment #1
Crell CreditAttribution: Crell commentedDo all 3 databases with core support use the same syntax?
And is there a performance benefit here? I just want to be sure it's worth spending time on this issue when core barely uses transactions and there are 340 critical issues remaining in the D7 queue. :-)
Comment #2
Josh Waihi CreditAttribution: Josh Waihi commentedAs far as PostgreSQL is concerned, savepoints require a real transaction.
So the transaction API when then work something like:
$txn = db_transaction()
is called andBEGIN
is sent to the database$txn2 = db_transaction()
is called again before the previous $txn loses scope,SAVEPOINT savepoint_1
is then sent to the databaseCOMMIT
s$txn->rollback()
will sendROLLBACK TO SAVEPOINT savepoint_n
to the database where n is the last savepoint to be created.So in application:
I like this idea, it can make transaction support more similar from database to database. For example, InnoDB can paritally commit failed transactions, using savepoints instead means all three databases can can commit intentional partially successful transactions.
Comment #3
David StraussThe syntax is similar if not identical across the databases. All also seem very flexible in the syntax, so it's likely we can find one set of commands to support them all.
Comment #4
Josh Waihi CreditAttribution: Josh Waihi commentedyes, they all use the same syntax - don't know about performance - I defer to David Strauss for that.
Comment #5
David Strauss@Crell The impetus for this issue is the elegance of using save points to fix remaining bugs in support for sequence generation. It's better for us to add the support to the transaction layer rather than write a one-off use of them for sequences. I do not know the performance impact, but I imagine savepoints use the same facilities as MVCC and transactions.
Comment #6
Crell CreditAttribution: Crell commentedHm. This is outside my knowledge scope, but I'm game to try it if Drieschick is. I'm pretty sure it would have to be in by the 7th, though, and then we'd still need to address the sequence issue and all of the others that depend on it. Gah.
I say go for it. I'll try to prioritize reviewing this chain of patches, as the dependency chain here is getting rather long.
Comment #7
Josh Waihi CreditAttribution: Josh Waihi commented:S only saw this comment now. IMO, I think its a better way to do embedded transactions but its not critical for D7. I say put it in D8.
Comment #8
Josh Waihi CreditAttribution: Josh Waihi commentedI'm going to bring this back into play because I think PostgreSQL (and most likely SQLite) needs this for atomicity (#301036: Optimize Merge queries in Postgres driver) here is a starter patch that introduces savepoints rather than false transaction nesting.
Because savepoints require a name, you can now optionally pass a name to db_transaction() which means there is the potential to rollback to a savepoint that wasn't the last savepoint introduced:
But the real beauty of this fix means we can now have a query fail but still have a successful transaction:
We are atomic here because we're only executing one query according to the transaction.
Comment #9
Josh Waihi CreditAttribution: Josh Waihi commentedfound spelling mistake already.
Comment #11
andypostSubscribe, looks very promising! I was using savepoints with oracle and there was no performance troubles.
Comment #12
Josh Waihi CreditAttribution: Josh Waihi commentedHere are my latest changes, there are still two failures in the transaction test though.
Comment #13
David StraussI don't understand why the savepoint names are stored. Can't we determine them at will knowing our nested transaction depth?
Comment #14
Josh Waihi CreditAttribution: Josh Waihi commented@David Strauss it gives you an option to identify a transaction point, say for example you have several transactions in the same scope, lets call then savepoints A, B anc C where C is the most recent savepoint established. Say for some reason in your code, you need to rollback to savepoint B rather than C you can simple call
Database::getConnection()->rollback('B')
which will rollback to that savepoint.Does that make sense? Is it a huge performance problem to remember them? From the logs, there only ever seems to be a depth of 2 at any one time in core.
Comment #15
andypostLet's test last patch...
Comment #17
Josh Waihi CreditAttribution: Josh Waihi commentedreroll against current head
Comment #19
andypost@Josh You lost a "}" after generateTemporaryTableName(), so here new reroll
Comment #21
andypostSuppose this fixes are last :)
1) transactionOuterLayer() was forget to make rollback if $rollback parameter passed
2) popTransaction() has no break after releasing savepoint so pops all savepoints
Comment #22
andypostMinimum version 3.6.8 is required for savepoint under sqlite http://www.sqlite.org/changes.html
pgsql works fine!
need testing under mysql without transactions :(
Comment #23
Crell CreditAttribution: Crell commentedLooking good overall. A few issues from visual inspection:
Perhaps I'm missing it, but transactions start out as optional-named (there is a default of empty string on DatabaseConnection::startTransaction()), but later down in DatabaseConnection::pushTransaction() it's required with no default.
Any new database exception classes should be prefixed with Database. Really, the existing ones should be, too, but they were written before we established that convention and I don't know if Drieschick will let us change them. :-) (If we're changing the transaction system anyway, I'd change the transaction exceptions to be DatabaseTransactionBlahBlahException at least.)
Protected properties on a class should be lowerCamelCase, not underscore_casel. (Eg, DatabaseTransaction::has_rolled_back)
Comment #24
andypostNew patch
- renamed all (4) transaction-related exceptions
- has_rolled_back replaced with rolledBack
- a bit changed php-docs
api change
I think we should remove $required argument because it useless - patch adds $name argument to database::startTransaction() but there's no $required!
savepoints naming
- savepoints should have unique names
- rollback possible to any savepoint in stack
- api should provide ability to set own name and rollback to any named savepoint
By default first savepoint receive "drupal_transaction" name
Next savepoints get incremental "savepoint_$depth" name
Else developer could define own name
If name already used so DatabaseTransactionNameNonUniqueException will be thrown
PS: Josh, please help with documentation and my english grammar.
docs
http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
http://www.postgresql.org/docs/8.3/static/sql-savepoint.html
http://www.sqlite.org/lang_savepoint.html
Comment #25
Josh Waihi CreditAttribution: Josh Waihi commentedThanks for the work Andy! the patch looks good, test bot likes it. I think the comments in the patch are fine. So we need to put docs somewhere else?
Comment #26
Dries CreditAttribution: Dries commentedI want Crell to sign-off on this patch.
Comment #27
Crell CreditAttribution: Crell commentedIt looks like the wrapper functions still have the $required parameter, but the methods simply drop it. If my sieve-like memory serves, we decided to remove the required property entirely so we should just get rid of that parameter to the wrapper functions.
The exception "DatabaseTransactionsNoActiveException" seems like a screwy name to me. I don't know if DatabaseTransactionNoActiveTransactionException would be better or worse, though. :-)
Actually, since we've removed the required property, that exception isn't needed. The only place we throw it is in the overridden Database::commit() method, where it's technically wrong anyway.
So let's do this:
1) Remove the $required parameter.
2) Rename DatabaseTransactionsNoActiveException to DatabaseTransactionExplicitCommitNotAllowedException (or something like that), since that's the only time it's used.
Comment #28
andypostI think we should save $required as part of $options there are some modules that could require explicit transactions
Comment #29
David StraussJust to throw another option out there: we could offer an option in .info files for modules to say they require transactions. Transaction support is effectively a global option that we can test for at module install-time.
Then, we can play fast and loose (no tests/exceptions) with transaction capability at run-time.
Comment #30
andypostI leave 'required' as optional parameter for startTransaction() and a part of $options parameter for db_transaction()
When transactions disabled and $required is TRUE a DatabaseTransactionsNotSupportedException exception will be thrown.
About exceptions:
DatabaseTransactionsNoActiveException already have nice description and make no sense for commit() - thrown when trying to rollback but there's nothing to rollback!
For commit() now used DatabaseTransactionExplicitCommitNotAllowedException exception
Comment #31
andypost@Crell Please point why $required should be totally dropped?
As I found #616650: Default MySQL to transactions ON just removes a $required parameter but having this with FALSE much reasonable.
Because this could be helpful with modules which require transactions!
Comment #32
Crell CreditAttribution: Crell commented@andypost: Because the functionality of $required was already removed. See HEAD right now; it looks like the issue you linked simply missed some bits of it, and what's left is unused flotsam that we should remove anyway since $required does nothing to begin with. If it doesn't get removed here, we'll just need to clean it up in another patch.
And no, the use of DatabaseTransactionsNoActiveException exception is incorrect. It means, per the name, "you're trying to roll back and there is nothing to roll back". What the commit() override method is saying is "dude, you're not allowed to roll back transactions that way, use the other way we gave you." Those are not the same error message.
Comment #33
andypost@Crell so in last patch - 2 different exceptions - exactly as you said:
DatabaseTransactionsNoActiveException - nothing to rollback
DatabaseTransactionExplicitCommitNotAllowedException - commit is not allowed by this way
About $required - do you really think that core does not needed this functionality at all?
We could save this at DB API as core rely on old-users-with-myisam-tables... Optional parameter is not hard to save.
Comment #34
Crell CreditAttribution: Crell commentedYes, I can't actually think of a case where a *module* would absolutely require transactions. I can think of use cases for sites, but that's up to the site/DB admin to setup MySQL properly with InnoDB. I actually rather like David's idea of pushing that check to the module's info file instead of we really really need to have it.
Ah, looking at the patch now I see the separate exception classes, thanks.
Comment #35
Damien Tournoud CreditAttribution: Damien Tournoud commentedWe don't actually need to do anything, it's easy enough for a module to implement a hook_requirements() that checks:
Let's just remove that completely.
Comment #36
Crell CreditAttribution: Crell commentedAhso, right you are! OK, so we already support module-level "I really need it" checking, so there's no need to have it baked into the API. Let's finish removing that and then get the rest of this patch in.
Comment #37
andypostThis patch removes $required and it's exception also unified doc-blocks
Comment #39
andypostNow patch against current HEAD
Comment #40
Josh Waihi CreditAttribution: Josh Waihi commentedFrom Crell:
@joshwaihi When I last looked at it I think it was pretty good. Now that the $required stuff is fully gone, we're probably good to go.
Comment #41
Crell CreditAttribution: Crell commentedSince I don't think twitter is the best venue for a code review, consider this a +1 let's commit things thing. :-) 4 of the 5 DB team members have been in this thread and all support it, so let's get 'er done.
Comment #42
andypostRe-roll after #711682: Style issues in includes/database/database.inc
Also updateв doc-block for DatabaseConnection::rollback() to point using 'drupal_transaction' as name
We could make this parameter optional and check for !isset() and assign this default
Comment #43
jhodgdonDoc review of the above patch... For reference, here is the guidelines page: http://drupal.org/node/1354
a)
I don't think the API module supports the @var tag at all -- should this even be there?
b)
First line of doc header should end with a period.
c)
I don't think "savepoint" is a word (unless it is some transaction jargon I might not be familiar with)... Maybe "save point" or "transaction save point" or just "transaction" would be clearer? I am willing to be told I'm wrong on this, if you can point to where in official PHP or MySQL doc they use the term "savepoint" for this, as I'm not a transaction expert.
d)
First line is awkward. In @param, verb should be "roll back" not "rollback". Suggestion for first line:
Rolls a transaction or save point back.
e)
This function, after your patch, has no doc block. It should, even if it's just a one-liner.
f)
Needs "a" or "the" in there, like "thrown when a commit()".
g)
Indentation is incorrect. Check the Lists section of the guidelines page above.
Comment #44
andyposta) used everywhere to describe class members
b) fixed
c) savepoint is a term (details at comment #24)
d) changed a bit
e-g) fixed
Suppose we could fix docs with follow-up patch
Comment #45
andypostg) changed - no quotes
Comment #46
jhodgdonThe problem with fixing docs in follow-up patches is that no one is interested in doing the follow-up patches, so we end up with a zillion issues in the documentation queue and no one interested in fixing them.
It is best instead never to commit code that violates our coding/doc standards, in my opinion.
So thanks for making the fixes!
One more in the latest patch:
No S on rollback.
Comment #47
andypost@jhodgdon should be rollbacks - plural, because it could write a set of operations. Also I'm not good with English to write good comments so glad any help!
Comment #48
jhodgdonIf it should be rollbacks (plural), then the "a" should be removed. The comment should say "Logs failed rollbacks."
Comment #49
aspilicious CreditAttribution: aspilicious commentedin plural you don't need to write the a.
==> Logs failed rollbacks
or
logs the failed rollbakcs
jhodgdon will decide ;)
Comment #50
jhodgdonUmmmm....
It looks to me as though what this function does is:
a) Logs that the rollback failed.
b) Logs each stored error in the rollback... though I am not really sure what this means since I have not read the whole transaction/rollback code base...
So I think the doc header should explain that? It looks like it is logging the rollback of one transaction, so if it's not going to explain about the playback (whatever that is), I would vote for
"Logs a failed rollback."
Comment #51
andypostPatch with "Logs failed rollbacks." thanks for explanation!
@jhodgdon This patch brings possibility to use nested transactions. Every nested transaction has it's own 'savepoint'. A mess caused by using words transaction and savepoint because transaction is a wrapper for set of savepoints!
About logger: suppose in the most of cases the $this->rollbackLogs array will hold a one record but sometimes could hold more records. It's all because now transaction have more then one level of nesting.
Comment #52
Josh Waihi CreditAttribution: Josh Waihi commented* removed $willRollback
* made drupal_transaction the default name of the transaction
* made $savepoint_name optional in DatabaseConnection::rollback. Will rollback entire transaction if no parameter given.
* updated documentation
Comment #53
Josh Waihi CreditAttribution: Josh Waihi commentedwhoops, hunk from another patch got in there.
Comment #54
andypostI think now it's more clear. All changes reasonable and discussed at IRC.
Let's wait final word of Crell :)
Comment #55
jhodgdonThere are still some comments in this patch that need some work, to conform with our coding/doc standards:
a)
b)
I think this function logs a single rollback, so the doc should be "Logs a failed rollback.". Either that or the function should be renamed to logFailedRollbacks() [with an S].
c)
All in-code comments are supposed to end with a .
d)
Retrives -> Retrieves
e)
Needs operates: at the end of the line, not operates.
Comment #56
andypostFixed all
about a) - suppose "need to roll back" on a second line
Comment #57
andypostChanged descriptions of logRollback() to
Logs a rollback() messages.
Seems more clear that it logs any messages that stored in $this->rollbackLogs array while rollback() called for nested levels of the transaction.
This array accumulates messages because they could not be written to logger (mostly in DB) until the transaction completes.
Comment #58
jhodgdonGrammar: You can't say "logs a" with "messages". It needs to be either "logs a message" or "logs messages".
Comment #59
jhodgdonActually, I think in this case it should be "Logs rollback messages." or "Logs messages from rollback()." And we can hope that the API module will use this to generate a link to the rollback() method on the same class.
Also just noticed a spelling error: propogate -> propagate
Comment #60
andypostI choose "Logs messages from rollback()."
Also both "propagate" fixed in tests.
EDIT: API module does not handle classes so links is not possible
Comment #61
jhodgdonThe API module is working on handling classes.
I have no further objections to the comments in this patch. Will leave the code review to others.
Comment #62
andypostCode was already reviewed, so back to RTBC
Crell confirmed this patch at #41
At #52 Josh has cleaned some unused variable and made default parameter value
Comment #63
aspilicious CreditAttribution: aspilicious commentedJust chasing head.
While browsing through the critical issue list I noticed that this one is important to fix some other issues.
So it's best to get this in soon :)
Comment #65
aspilicious CreditAttribution: aspilicious commentedHate windows endings
Comment #66
Dries CreditAttribution: Dries commentedLooks great, and we got sign-off from Crell per my request. Committed to CVS HEAD.
Comment #67
mfer CreditAttribution: mfer commentedJust tried to install sqlite and get the error "SQLSTATE[HY000]: General error: 1 near "SAVEPOINT": syntax error"
http://img.skitch.com/20100325-x9wckdjebt4e32h82jj481f5gc.png
Comment #68
andypost@mfer could you look at docs #22 and #24
Minimum sqlite version 3.6.8 is required for savepoint support http://www.sqlite.org/changes.html
Comment #69
andypostLast patch does not take into account changes from #748982: Allow Database logging_callback to be a class method
Comment #70
andypostFollow up patch to fix #748982: Allow Database logging_callback to be a class method
Comment #71
mikeryanLooks good, thanks Andy!
Comment #72
mfer CreditAttribution: mfer commented@andypost This creates an interesting problem. 3.6.8 is not shipped with many versions of PHP 5.2. So, while Drupal 7 is stated to require php 5.2 the sqlite version that ships with php 5.2 is not new enough. Isn't that a contradiction? It will be confusing for many PHP 5.2 users and difficult for people to test.
Currently sqlite support blows up under tests. Yet, MAMP doesn't even come with sqlite 3.6.8 so it's not easy to ask people to test to help get it all working.
Comment #73
andypost@mfer I think sqlite maintainers could help with this. I know about this but I think using sqlite is limited cases so it require some docs how to build php with required sqlite version.
Also I see no reason of using sqlite in multiuser environment.
EDIT: suppose sqlite could provide it's own overriden method for transactions
Comment #74
mfer CreditAttribution: mfer commented@andypost You can't build your own version of SQLite on shared hosting. Or, on managed VPS. Or, if you aren't at all a server admin. Because, then you need to maintain your own custom build which is another step as well.
Requiring a newer version of SQLite than what ships with PHP 5.2 severely cripples its usage and the people willing to develop for it.
Comment #75
mfer CreditAttribution: mfer commentedFYI, the first version of PHP to contain SQLite 3.6.XX is PHP 5.3. See http://www.php.net/ChangeLog-5.php#5.3.0
So, for SQLite support someone either needs PHP 5.3 or a custom version of SQLite compiled in.
Comment #76
Crell CreditAttribution: Crell commentedI agree that we need to support "out of the box" SQLite in 5.2. That probably means an SQLite-specific transaction implementation that is more like the previous implementation that just fakes savepoints and rolls back everything.
Ugh.
Comment #77
mfer CreditAttribution: mfer commentedI'm marking this needs work. Right now you cannot test Drupal 7 in SQLite with PHP 5.2. This is a problem :)
Comment #78
mfer CreditAttribution: mfer commentedComment #79
mikeryanThe RTBC is for the patch in #70, which shouldn't be held up by the SQLite issue. Once that fix is committed, this can go back to "needs work".
Comment #80
Damien Tournoud CreditAttribution: Damien Tournoud commentedI'm getting tired of big patches like this one breaking support for SQLite.
Comment #81
andypostLet's file another issue about sqlite support and close this after commit.
Comment #82
Josh Waihi CreditAttribution: Josh Waihi commented@damian, if all db enviroments were tested on the testbeds this wouldn't be an issue. Didn't Dries commit #70?
Comment #83
Dries CreditAttribution: Dries commentedI did commit #70.
Comment #84
Dries CreditAttribution: Dries commentedI guess this means that status is 'needs work' instead.
Comment #85
andypost@Dries follow up still not commited!
Comment #86
andypostConfirm that current ubuntu ships with SQLite 3.6.16 - and it works!
Comment #87
mfer CreditAttribution: mfer commented@andypost Your argument that ubuntu shipping with a different version of SQLite than the default version shipped with php 5.2 will not work. This needs to work for the default PHP 5.2 or we need to change the system requirements required to run drupal.
Comment #88
andypost@mfer I've changed status to point that Dries forget to commit #70
Comment #89
catchLet's revert #65 then do this without breaking sqlite support or not at all, it's too late in the game to be breaking major features then deferring fixing them to followup patches.
Comment #90
andypost@catch I think it's better to make patch for sqlite than break nested transactions.
Also this approach is needed to finish #715108: Make Merge queries more consistent and robust
Comment #91
andypostI file a critical issue #754192: Transaction support for old sqlite versions
Suppose it's much easy to check version of sqlite and fall back to limited transaction support for single-user database then stay without atomic operations for the most common databases.
So let's commit this follow up and cotinue in #754192: Transaction support for old sqlite versions
Comment #92
Crell CreditAttribution: Crell commented+1 to #91. Let's commit that and deal with SQLite in the other issue so that we don't continue to block other issues.
Comment #93
Dries CreditAttribution: Dries commentedI committed the patch in #91. I thought I had committed it because it looked a lot like a watchdog patch that I had committed earlier in the day. Sorry about that.
I'm not inclined to rollback this patch entirely. It sounds like we might be able to make SQLite work by simply overwriting a few functions with empty stubs so that transactions don't do anything useful.
Comment #94
Crell CreditAttribution: Crell commentedThere's already another issue for dealing with SQLite transactions linked above, so this should be marked fixed.
Comment #95
mfer CreditAttribution: mfer commented#754192: Transaction support for old sqlite versions is a very important issue. D7 currently fails A LOT of tests in SQLite. Until that patch lands we cannot test SQLite. This could be a mean a RC before Drupalcon either FAILS tests in SQLite (which means there are bugs) or doesn't come out.
Comment #96
moshe weitzman CreditAttribution: moshe weitzman commentedI'm seeing lots of new errors: "Explicit rollback failed: not supported on active connection.". I think this logRollback method is getting called even for successful rollbacks which appears to be unintended. I can't work out the real intentions from looking at the code. Anyway, here is a patch which seems to work. If we really do want to call this method unconditionally, then it shouldn't always report a failed rollback.
Comment #98
moshe weitzman CreditAttribution: moshe weitzman commentedHopefully this properly repairs the minor butchering added in this issue.
Comment #99
andypost@moshe weitzman This change required for non transactional Dbs. So sqlite should be fixed too because it mirrors this logic #754192: Transaction support for old sqlite versions
Comment #100
David Strauss@andypost No. SQLite always supports transactions.
Comment #101
andypost@David Strauss: So do you think that we should re-open #754192: Transaction support for old sqlite versions and remove all checks for transactions? I think we should keep database.inc in-sync
Comment #102
moshe weitzman CreditAttribution: moshe weitzman commentedIMO, its fine for this code to mirror as andypost has suggested. Thats not really the intent of this patch though. We're fixing a critical new bug in HEAD.
Comment #103
Dries CreditAttribution: Dries commentedCommitted to CVS HEAD. Thanks.
Comment #104
Dries CreditAttribution: Dries commentedCommitted to CVS HEAD. Thanks.