Optimistic Lock: Iteration & Rollback
Extended the lock to the ordering portal API (v3.7.0), then rolled back the over-aggressive form-side check (v3.7.1)
Overview
Release 3.6 added three layers of stale-write protection on the Project entity. In production the third layer - the explicit $em->lock() check in ProjectCrudController::updateEntity() - fired on 70-90% of admin saves because async workers and hourly crons were bumping the version between form render and submit. This release extends the API-side protection (v3.7.0) and then rolls back the over-aggressive form-side check (v3.7.1).
Context: The Three Layers
From release 3.6:
- Entity-level
#[ORM\Version]- Doctrine emitsUPDATE ... WHERE version = X; zero affected rows throws. Catches true in-flight concurrent saves at the DB layer. - EasyAdmin form hidden-version +
$em->lock()- Catches the "long-held form" race where a user holds an edit form open and something else modifies the row before they submit. - Missing in 3.6: API Platform path - The ordering portal uses API Platform resources, not EasyAdmin forms, so layer 2 didn't cover it.
v3.7.0: Extend to Ordering Portal API
The ordering portal API needed the same stale-write protection as the EasyAdmin form flow.
Changes
ProjectResourceDTO gains a nullableversionfield (project:read+project:write).ProjectResource::fromEntity()populates it soGETresponses include it.ProjectProcessor::updateProject()calls$em->lock($project, LockMode::OPTIMISTIC, $data->version)when the submitted payload includes a version. Clients that omit the field keep existing behaviour.OptimisticLockExceptionListenerreturns an HTTP 409 JSON response for API / XHR / JSON-accepting requests (HTML flows keep flash + redirect).
Why It Stayed
API clients opt in by submitting a version field. Those that don't send one are not subject to the check, so the false-positive rate observed on the admin form doesn't apply here.
v3.7.1: Remove the Admin Form Lock
Users reported the "This record was modified by another user while you had it open" error on 6-9 out of every 10 saves. The root cause:
- Every admin save dispatches a
ProcessProjectasync message. - The messenger worker processes it almost immediately and touches the
Projectentity - bumping the version. - The form reloaded after save gets rendered with
version = N+1, but the messenger worker then bumps toN+2before the user's next interaction. - The next
$em->lock($project, OPTIMISTIC, N+1)call fails because the DB is atN+2.
Hourly crons (update-title-status, update-expired-expected-bp) compound this by bumping versions on every projects they touch.
Changes
- Removed the
$em->lock()block fromProjectCrudController::updateEntity(). - Removed the
submittedVersionhidden field from bothconfigureFields()methods (no longer read).
What Still Protects Admin Saves
The entity-level #[ORM\Version] annotation remains. Doctrine still emits:
UPDATE project SET ..., version = v+1 WHERE id = ? AND version = vIf another session updated the row between load and flush, the WHERE version = v matches zero rows and OptimisticLockException is thrown. The narrow "two requests in flight" race is still caught.
What was removed is the proactive probe that checked a form-round-tripped version against the DB at save time - the one that was firing false positives.
Follow-up Work (Not Done)
Items flagged in the v3.7.1 commit message for later:
ProcessProjectHandlershould avoid bumpingProject.versionfor derived/calculated data. Consider moving computed state to a side-table or using raw SQL that bypasses versioning.- Hourly cron endpoints (
update-title-status,update-expired-expected-bp) should change-check before persisting.
If those land, the explicit form-side lock can be safely re-introduced without the false-positive storm.