I've run into a strange bug/problem that causes transactions to fail silently (neither rollback or commit is performerd). This seems to be partially caused by the pessimistic locking introduced in Commerce. First some background. I have a Services module that logs complete orders (originating from an external system) into Commerce, including line items and payments. I want to wrap this whole shebang in a transaction to make it all succeed or all fail. So I do $transaction = db_transaction() early on, creating the outermost transaction (the "drupal_transaction").
I then proceed creating the order using commerce_order_new and saving it using commerce_order_save (in order to obtains its order ID). I create line items using commerce_line_item_new, and payments using commerce_payment_transaction_new and commerce_payment_transaction_save.
Along the way, DrupalCommerceEntityController creates a transaction in its buildQuery function, which is stored in its controllerTransaction instance variable. This is, in my case, a nested transaction (ie, a save-point). Other save points are created as a result of the various other calls mentioned above, in an orderly manner.
However, when I attempt to add the payments, strange things start to happen. The function commerce_order_commerce_payment_transaction_insert calls commerceOrder save (through rules, if I recall correctly), which calls entity_load_unchanged, which causes DrupalCommerceEntityController resetCache to be called. The resetCache function calls unset($this->controllerTransaction), thus causing the transaction's destructor to be called. However, at this point, there are still several stack-based transactions in progress, with higher save-point indices in the transaction stack. Those save-points get popped off as a result of unset($this->controllerTransaction). Soon thereafter, the first stack-based transaction goes out of scope, causing its destructor to attempt to pop it off of the stack. However, it's no longer there since the removal of the lower-in-the-stack transaction held in the controllerTransaction have already discarded it. This causes ALL remaining transactions on the stack to be discarded by the following code inside popTransaction in database.inc:
while($savepoint = array_pop($this->transactionLayers)) {
if ($savepoint != $name) continue;
due to the fact that the savepoint with the sought-after name is no longer on the stack (since it was popped off when unset($this->controllerTransaction) was performed.
Since there are no longer *any* transactions left on the stack, commit will never be called, causing the entire transaction to fail silently.
Hopefully, someone in the know have made it all the way down here without becoming 100% confused. Although I'm beginning to underdstand what's happening, I can't really figure out how to fix it. It seems the pessistic locking mechanism (which stores the transaction in an instance variable) is at odds with the stack-based transaction handling used in most other places, since it may cause its transaction to be destroyed "out of sequence", hence wreaking havoc with the entire transaction stack.
Any suggestions on how to get out of this pickel are most welcome!
-JM
Comments
Comment #1
TheWizz commentedOK, I wrestled some more with this, and I'm now even more convinced that the problem is caused by the "pessimistic locking" introduced in commerce. This can cause transactions to be popped in a different order from the (reversed) order they're pushed in, which hoses he transaction stack (see above for all the gory details). It seems I'm not the only one having trouble with this:
http://drupal.org/node/1252278
I used the same approach as suggested the patch provided that issue to disable the pessimistic locking for Commerce Order, and my problem immediately went away.
Needless to say, I'm not entirely happy with this solution. Although it solves my problem in this particular case, I'm sure the pessimistic locking went in for very good reasons, and I'm now turning it off entirely. Preferably, I would only want to disable it during the course of my service function, and then have it back on for "normal" commerce use. But I've found no way in which I can disable the pessimistick locking only across a single service call.
Any suggestions are most welcome.
-JM
Comment #2
rszrama commentedVery interesting. I'll have to get Damien to weigh in on this issue, as his implementation goes a little over my head. : )
I bet it would be possible to disable for a particular purpose, though. The entity info is stored in the cache with a cid of entity_info:[language code]. You could load the entity info array, alter it, save it to the cache, perform your operation, and then set it back when you're done. Again a little hackish but at least temporarily workable.
Comment #3
TheWizz commentedThanks, Ryan!
Is it possible to change the in-memory idea about this setting only? Using cache_get("entity_info:en") to get the current setting, change the setting, and updating the cache with cache_set presumably changes the setting also in the database. Seems quite wsteful and unnecessary only for a temporary change like this, and stands the risk of permanently altering the setting if something goes wrong before I restore the original setting. But if that's the only way you can think of to temporarily disable persimistic locking, I guess that's what I have to do. At least it woud be less of a sledgehammer approach than permanently disabling it using the patch from http://drupal.org/node/1252278.
Also, there seems to be something fundamentally wrong with the way pessimistic locking is implemented if it can result in neither commit nor rollback being performed, despite the fact that I initiate a transaction at the top of my service function. I'd call this a bug. It is of course quite possible that I do something stupid, which causes this. But as far as I can tell, I only call reasonable Commerce functions to store order data, line items and payments, wrapping it all in a transaction. I'd be happy to provide an rundown of the funtions I call, and where I see the problem rearing its ugly head.
-JM
Comment #4
damien tournoud commentedThis is completely expected if you are not using Drupal 7.8 (see #1185780: Make transactions more flexible and useful).
Requalifying as a support request until we know more.
Comment #5
damien tournoud commentedWhat's happening is that your transaction is silently committed, but Drupal doesn't know about it. The transaction itself doesn't go into limbo, don't worry :)
Comment #6
TheWizz commentedCorrect, I'm on 7.7 due to some problems with tokens I encountered under 7.8 (see https://drupal.org/node/1265848 ). I'm using the commerce_bpc which depends on token, and was also broken by this token problem. I don't know if this issue has been resolved since, but I immediately went back to 7.7, not knowing that this would have other undesired side effects. Guess I will have to look into this token issue again to move to 7.8, if 7.8 will fix the transaction problem, as you suggest.
Well, the data doesn't appear in the database, so I'm quite sure it isn't committed. Also, I put breakpoints on the commit and rollback calls, and neither was reached in my case (due to the "inversion" problem I describe in the original posting). Presumably, not calling commit on a transaction will cause the database to not be updated.
-JM
Comment #7
mglamanWe're well beyond 7.8.