$npx -y skills add Lonsdale201/wp-agent-skills --skill br-optimistic-lockingConfigure Better Route 1.1 optimistic locking for REST writes with If-Match or version parameters and an atomic per-resource critical section. Use when preventing stale updates, lost writes, or two cooperating Better Route requests from passing the same version check concurrently
| 1 | # Better Route optimistic locking |
| 2 | |
| 3 | Use optimistic locking on updates or deletes where overwriting a newer state is unsafe. Resolve the current version from storage while the critical section is held. |
| 4 | |
| 5 | ```php |
| 6 | use BetterRoute\Middleware\Write\CallbackOptimisticLockVersionResolver; |
| 7 | use BetterRoute\Middleware\Write\OptimisticLockMiddleware; |
| 8 | use BetterRoute\Middleware\Write\WpdbOptimisticLockCriticalSection; |
| 9 | |
| 10 | $lock = new OptimisticLockMiddleware( |
| 11 | versionResolver: new CallbackOptimisticLockVersionResolver( |
| 12 | static function ($context): string|int|null { |
| 13 | $id = (int) $context->request->get_param('id'); |
| 14 | return my_current_record_version($id); |
| 15 | } |
| 16 | ), |
| 17 | required: true, |
| 18 | headerName: 'if-match', |
| 19 | paramName: 'version', |
| 20 | criticalSection: new WpdbOptimisticLockCriticalSection(timeoutSeconds: 2) |
| 21 | ); |
| 22 | |
| 23 | $router->patch('/records/(?P<id>\d+)', $handler) |
| 24 | ->middleware([$auth, $lock]) |
| 25 | ->protectedByMiddleware('bearerAuth'); |
| 26 | ``` |
| 27 | |
| 28 | The middleware prefers `If-Match`, then falls back to the configured request parameter. It accepts quoted or weak ETag-like values by normalizing `W/"value"` to `value`; numeric values become strings. `*` accepts any available current version. |
| 29 | |
| 30 | ## Response contract |
| 31 | |
| 32 | - Missing precondition with `required: true` returns `428 Precondition Required`. |
| 33 | - A supplied version that differs from current storage returns `412 optimistic_lock_failed` with expected/current details. |
| 34 | - An unavailable current version returns `409 version_unavailable`. |
| 35 | - Lock acquisition failure throws and becomes an internal failure unless the application maps it deliberately. |
| 36 | - On success, context attribute `optimisticLock` contains `expected`, `current`, and `atomic: true`. |
| 37 | |
| 38 | ## Atomicity boundary |
| 39 | |
| 40 | The default `WpdbOptimisticLockCriticalSection` derives a MySQL named lock from route path plus canonicalized URL parameters. It holds that lock around both the current-version read and the downstream handler. Concurrent Better Route writers using the same route identity cannot both pass the same stale check. |
| 41 | |
| 42 | This is a cooperative lock, not a database-wide compare-and-swap: |
| 43 | |
| 44 | - External writers, direct SQL, background jobs, and different routes can still race unless they use the identical lock discipline. |
| 45 | - Route parameters must uniquely and consistently identify the stored resource. A write identity hidden only in body/query data is not included by the default lock name. |
| 46 | - The handler must actually advance the version after a successful mutation. |
| 47 | - MySQL named locks are connection-scoped. Keep the protected handler bounded and never perform slow remote I/O inside it. |
| 48 | |
| 49 | For storage shared with uncontrolled writers, implement a true conditional update such as `UPDATE ... WHERE id = ? AND version = ?` and verify one affected row, or provide a custom `OptimisticLockCriticalSectionInterface` aligned with that storage. |
| 50 | |
| 51 | ## Checks |
| 52 | |
| 53 | - Send no precondition, a matching version, a stale version, weak/quoted versions, and `*`. |
| 54 | - Run two concurrent requests with the same version and assert only one mutation succeeds. |
| 55 | - Verify two different resource IDs do not share a lock and equivalent parameter ordering does. |
| 56 | - Exercise lock timeout and handler exceptions; the named lock must release in `finally`. |
| 57 | - Verify every mutation path, including jobs and alternate endpoints, follows the chosen concurrency contract. |
| 58 | |
| 59 | Source references: `src/Middleware/Write/OptimisticLockMiddleware.php`, `src/Middleware/Write/WpdbOptimisticLockCriticalSection.php`, `src/Middleware/Write/CallbackOptimisticLockVersionResolver.php`. |
| 60 | |
| 61 | ## References |
| 62 | |
| 63 | - Official documentation: <https://lonsdale201.github.io/better-docs/docs/better-route/agents> |