recomputeState() already locks the transaction row (SELECT ... FOR UPDATE) before it derives and writes the aggregate state, so two recomputes cannot race. But the explicit checkout transitions, lockTransaction(), placeTransaction(), unlockTransaction(), and the terminal stamp of cancelTransaction(), did not: each reloaded the order, checked its state, and wrote, with no row lock and no compare-and-set.

Two transitions that land at once can therefore both read a stale state and clobber each other. Today those transitions come only from serialized workflow steps, so it is latent, but any consumer that drives them concurrently (a settling payment and a cancellation arriving together, for example) would hit it.

This extracts the row-lock pattern into a withRowLock() helper: open a database transaction, lock the row with SELECT ... FOR UPDATE, reload from that snapshot, run the transition, then commit when the lock leaves scope or roll back if it throws. Every transition is routed through it, so all transitions on one order serialize on its row: the second waits for the first to commit, then reads the state it left. This gives the order state machine the same compare-and-set guarantee the kessai payment state machine already has.

Issue fork yoyaku-3611850

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

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review

Opened merge request !45 against 1.x. Every order state transition now serializes on the transaction row (SELECT ... FOR UPDATE via a withRowLock helper), so a settling payment and a cancellation landing together can no longer both act on a stale read. Foundational for the payment-driven order lock lifecycle coming next.

  • mably committed 5de7a8b0 on 1.x
    fix: #3611850 Serialize every order state transition under a row lock...
mably’s picture

Status: Needs review » Fixed

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.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.