Problem/Motivation

On Drupal 11, EntityStorageBase::loadMultiple() suspends the current fiber when entities aren't statically cached, so that loads can be batched, and BigPipe renders each placeholder in its own fiber.

OrderStorage::postLoad() saves orders that need a refresh. That save opens a database transaction, and the refresh loads the order items (getItems() → loadMultiple()), so the fiber gets suspended while the transaction is still open. Another placeholder that loads the customer's carts (e.g. add to cart forms on a product listing) then refreshes and saves a cart on the same connection. All fibers share the connection's transaction stack, so when the first fiber resumes and commits, its savepoint is no longer on the stack:

Drupal\Core\Database\TransactionOutOfOrderException: Error attempting commit of d7d3bc8aa9f6e57690406ed3\savepoint_2. Active stack: c25f8c9e8203ce4543f2492e\drupal_transaction in Drupal\Core\Database\Transaction\TransactionManagerBase->unpile()

Depending on the timing, this can also show up as an order version mismatch: Attempted to save order 2 with version 1. Current version is 2.

Drupal 10 isn't affected, since entity loads don't suspend fibers there.

Steps to reproduce

  1. On Drupal 11, have an authenticated customer with one or more draft carts that need a refresh (e.g. last changed yesterday).
  2. Render a page with several BigPipe placeholders that each load the customer's carts (e.g. a product listing with add to cart forms).
  3. A TransactionOutOfOrderException is thrown when committing the refresh save.

The attached kernel test reproduces this by loading 3 orders that need a refresh from 3 fibers, resumed in turn like BigPipe does.

Proposed resolution

When OrderStorage::postLoad() already runs inside a fiber, perform the refresh save in a nested fiber that is resumed to completion. Suspensions during the save then stay inside the nested fiber, and no other fiber can run while the transaction is open. This is the same isolation pattern core uses in AccessPolicyProcessor::processAccessPolicies() and EntityReferenceItemBase::__get().

Outside a fiber, the behavior is unchanged. Only the PHP 8.1 \Fiber API is used (no FiberResumeType), so it is safe on Drupal 10.3.

The underlying problem (a fiber suspended mid-transaction lets other fibers corrupt the shared transaction stack) affects any entity save performed from a lazy builder, and should ideally be addressed in core as well.

Remaining tasks

  • Review the patch.
  • Link the related core issue.

User interface changes

None.

API changes

None.

Data model changes

None.

Issue fork commerce-3634141

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

jsacksick created an issue. See original summary.

jsacksick’s picture

Status: Active » Needs review

  • jsacksick committed aadb8326 on 3.x
    fix: #3634141 Refresh-on-load order save can corrupt the transaction...
jsacksick’s picture

Status: Needs review » Fixed

Merged!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.