br-atomic-idempotency
Configure better-route 1.1 AtomicIdempotencyMiddleware for side-effectful POST, PUT, PATCH, or DELETE routes where concurrent duplicate execution must be prevented. Use for WpdbAtomicIdempotencyStore, lease-aware reservations, reservation_token schema migration, Idempotency-Key v
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/better-route/br-atomic-idempotency
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
better-route: atomic idempotency
Use atomic idempotency for writes whose side effect must not execute twice under concurrent retries. It reserves before invoking the handler; classic IdempotencyMiddleware only stores after completion.
Production setup
use BetterRoute\Middleware\Write\AtomicIdempotencyMiddleware;
use BetterRoute\Middleware\Write\WpdbAtomicIdempotencyStore;
register_activation_hook(__FILE__, static function (): void {
(new WpdbAtomicIdempotencyStore())->installSchema();
});
$atomic = new AtomicIdempotencyMiddleware(
store: new WpdbAtomicIdempotencyStore(),
ttlSeconds: 86400,
requireKey: true,
);
$router->post('/payments', $handler)
->protectedByMiddleware('bearerAuth')
->middleware([$auth, $atomic]);
Run installSchema() for both new installs and 1.0 upgrades. The 1.1 table has a per-reservation reservation_token; the installer creates or migrates that column.
Use ArrayAtomicIdempotencyStore only in tests or non-WordPress single-process checks.
Behavior
- First request reserves the canonical route + identity + client key and runs the handler.
- Same key and fingerprint while reserved returns
409 idempotency_in_progress. - Same key with a different fingerprint returns
409 idempotency_conflict. - Completed identical retry replays status/body/headers and adds
Idempotency-Replayed: true. - Missing key returns
400 idempotency_key_requiredwhen required. - Invalid, non-printable, or overlong keys return
400 idempotency_key_invalid; default maximum length is 200.
The default fingerprint deeply canonicalizes route, method, identity, and request params. Associative key order does not change it; list order remains meaningful. Native logged-in WordPress users, auth middleware identities, and HMAC key identities scope defaults safely.
Route scope and 1.1.1 migration
Scope default keys and fingerprints to the router namespace, registered template, concrete request path and separately captured URL parameters, alongside identity and the existing payload fields. Query/body parameters must not mask URL IDs. routePath remains the template. Test two namespaces and two URL targets with the same client key and shadowing query/body IDs; they must remain isolated.
Existing default records cannot safely translate and are not replayed through a legacy fallback. Before switching, pause writers/retries, drain requests, reconcile uncertain business operations and retire old retries across the full client retry horizon. TTL expiry alone and deleting records are insufficient. Switch all workers together; apply the same coordination on rollback. Follow br-install-and-migrate for the full procedure.
Custom key/fingerprint resolvers own equivalent isolation. A custom key alone still uses the changed default fingerprint. After uncertain failures, reconcile before retrying with any key, even after expiry; keep business-level deduplication for irreversible effects.
Failure semantics
In 1.1 releaseOnThrowable defaults to false. A thrown exception can occur after an irreversible external side effect but before the response is recorded, so the reservation remains in_progress until TTL rather than allowing a dangerous automatic retry.
Set releaseOnThrowable: true only if the complete operation is transactional/rollback-safe or the handler is known to fail before any side effect:
new AtomicIdempotencyMiddleware(
store: $store,
releaseOnThrowable: true,
);
Returned WP_Error values are not serialized and leave the reservation uncertain. They are normalized after the middleware pipeline.
Stored response contract
The wpdb store serializes a data-only envelope with allowed_classes => false:
- arrays, scalars, and null are supported;
BetterRoute\Http\Responseis decomposed into body/status/string headers;WP_REST_Responseis normalized into the same safe Response form;- arbitrary objects/resources inside the body are rejected.
Do not return domain objects from idempotent handlers; map them to data first.
Lease-aware completion and release include the unpredictable reservation token. A stale request therefore cannot complete or delete a newer reservation that reused the same key after expiry.
WooCommerce registrar
When Woo idempotency is enabled in WordPress, WooRouteRegistrar uses atomic idempotency across orders, products, customers, and coupons. Without a custom store it installs/migrates the wpdb store once per schema version. Installation failure is surfaced; it does not silently degrade to an array store.
'idempotency' => [
'enabled' => true,
'requireKey' => true,
'ttlSeconds' => 86400,
// 'store' => $customAtomicStore,
],
Any custom Woo store must implement AtomicIdempotencyStoreInterface.
Review checklist
- Put authentication before atomic idempotency.
- Install/migrate the wpdb schema before serving traffic.
- Choose TTL for the longest realistic uncertain/retry window.
- Keep the default fail-closed
releaseOnThrowable: falseunless retry safety is proven. - Return data-only responses.
- Load-test two simultaneous identical requests; exactly one handler may run.
- Test same key with a changed body and verify
idempotency_conflict. - Use a custom key/fingerprint resolver only when tenant/domain scope cannot be expressed by the default identity-aware canonical form.
Related skills
- Use
br-idempotencyfor lower-risk replay-cache semantics. - Use
br-woo-routesfor registrar integration. - Use
br-error-contractfor 400/409 response shapes.
References
- Verified source paths:
src/Middleware/Write/AtomicIdempotencyMiddleware.phpsrc/Middleware/Write/WpdbAtomicIdempotencyStore.phpsrc/Middleware/Write/LeaseAwareAtomicIdempotencyStoreInterface.phpsrc/Middleware/Write/StoredResponseCodec.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 261 B
interface: display_name: "Better Route Atomic Idempotency" short_description: "Prevent concurrent duplicate write execution." default_prompt: "Use $br-atomic-idempotency for concurrent write safety and review namespace/URL replay scope before migration."
-
-
SKILL.md 6.5 KB
--- name: br-atomic-idempotency description: Configure better-route 1.1 AtomicIdempotencyMiddleware for side-effectful POST, PUT, PATCH, or DELETE routes where concurrent duplicate execution must be prevented. Use for WpdbAtomicIdempotencyStore, lease-aware reservations, reservation_token schema migration, Idempotency-Key validation, idempotency_in_progress/conflict/replay behavior, safe stored responses, releaseOnThrowable, Woo write idempotency, or retry-safe payment/order/subscription/account operations. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "better-route" wp-skills-plugin-version-tested: "1.1.1" wp-skills-php-min: "8.1" wp-skills-last-updated: "2026-09-21" --- # better-route: atomic idempotency Use atomic idempotency for writes whose side effect must not execute twice under concurrent retries. It reserves before invoking the handler; classic `IdempotencyMiddleware` only stores after completion. ## Production setup ```php use BetterRoute\Middleware\Write\AtomicIdempotencyMiddleware; use BetterRoute\Middleware\Write\WpdbAtomicIdempotencyStore; register_activation_hook(__FILE__, static function (): void { (new WpdbAtomicIdempotencyStore())->installSchema(); }); $atomic = new AtomicIdempotencyMiddleware( store: new WpdbAtomicIdempotencyStore(), ttlSeconds: 86400, requireKey: true, ); $router->post('/payments', $handler) ->protectedByMiddleware('bearerAuth') ->middleware([$auth, $atomic]); ``` Run `installSchema()` for both new installs and 1.0 upgrades. The 1.1 table has a per-reservation `reservation_token`; the installer creates or migrates that column. Use `ArrayAtomicIdempotencyStore` only in tests or non-WordPress single-process checks. ## Behavior - First request reserves the canonical route + identity + client key and runs the handler. - Same key and fingerprint while reserved returns `409 idempotency_in_progress`. - Same key with a different fingerprint returns `409 idempotency_conflict`. - Completed identical retry replays status/body/headers and adds `Idempotency-Replayed: true`. - Missing key returns `400 idempotency_key_required` when required. - Invalid, non-printable, or overlong keys return `400 idempotency_key_invalid`; default maximum length is 200. The default fingerprint deeply canonicalizes route, method, identity, and request params. Associative key order does not change it; list order remains meaningful. Native logged-in WordPress users, auth middleware identities, and HMAC key identities scope defaults safely. ## Route scope and 1.1.1 migration Scope default keys and fingerprints to the router namespace, registered template, concrete request path and separately captured URL parameters, alongside identity and the existing payload fields. Query/body parameters must not mask URL IDs. `routePath` remains the template. Test two namespaces and two URL targets with the same client key and shadowing query/body IDs; they must remain isolated. Existing default records cannot safely translate and are not replayed through a legacy fallback. Before switching, pause writers/retries, drain requests, reconcile uncertain business operations and retire old retries across the full client retry horizon. TTL expiry alone and deleting records are insufficient. Switch all workers together; apply the same coordination on rollback. Follow `br-install-and-migrate` for the full procedure. Custom key/fingerprint resolvers own equivalent isolation. A custom key alone still uses the changed default fingerprint. After uncertain failures, reconcile before retrying with any key, even after expiry; keep business-level deduplication for irreversible effects. ## Failure semantics In 1.1 `releaseOnThrowable` defaults to `false`. A thrown exception can occur after an irreversible external side effect but before the response is recorded, so the reservation remains `in_progress` until TTL rather than allowing a dangerous automatic retry. Set `releaseOnThrowable: true` only if the complete operation is transactional/rollback-safe or the handler is known to fail before any side effect: ```php new AtomicIdempotencyMiddleware( store: $store, releaseOnThrowable: true, ); ``` Returned `WP_Error` values are not serialized and leave the reservation uncertain. They are normalized after the middleware pipeline. ## Stored response contract The wpdb store serializes a data-only envelope with `allowed_classes => false`: - arrays, scalars, and null are supported; - `BetterRoute\Http\Response` is decomposed into body/status/string headers; - `WP_REST_Response` is normalized into the same safe Response form; - arbitrary objects/resources inside the body are rejected. Do not return domain objects from idempotent handlers; map them to data first. Lease-aware completion and release include the unpredictable reservation token. A stale request therefore cannot complete or delete a newer reservation that reused the same key after expiry. ## WooCommerce registrar When Woo idempotency is enabled in WordPress, `WooRouteRegistrar` uses atomic idempotency across orders, products, customers, and coupons. Without a custom store it installs/migrates the wpdb store once per schema version. Installation failure is surfaced; it does not silently degrade to an array store. ```php 'idempotency' => [ 'enabled' => true, 'requireKey' => true, 'ttlSeconds' => 86400, // 'store' => $customAtomicStore, ], ``` Any custom Woo store must implement `AtomicIdempotencyStoreInterface`. ## Review checklist - Put authentication before atomic idempotency. - Install/migrate the wpdb schema before serving traffic. - Choose TTL for the longest realistic uncertain/retry window. - Keep the default fail-closed `releaseOnThrowable: false` unless retry safety is proven. - Return data-only responses. - Load-test two simultaneous identical requests; exactly one handler may run. - Test same key with a changed body and verify `idempotency_conflict`. - Use a custom key/fingerprint resolver only when tenant/domain scope cannot be expressed by the default identity-aware canonical form. ## Related skills - Use `br-idempotency` for lower-risk replay-cache semantics. - Use `br-woo-routes` for registrar integration. - Use `br-error-contract` for 400/409 response shapes. ## References - Verified source paths: - `src/Middleware/Write/AtomicIdempotencyMiddleware.php` - `src/Middleware/Write/WpdbAtomicIdempotencyStore.php` - `src/Middleware/Write/LeaseAwareAtomicIdempotencyStoreInterface.php` - `src/Middleware/Write/StoredResponseCodec.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.