Context
It is possible for retries to arrive while a previous request is still being processed – for example because the client received an indefinite error (a timeout or crash) and retried without knowing whether the first request was still running. Some operations need protecting against concurrent retries, either to avoid race conditions or to prevent duplicate work.
Example
Exchanging an OAuth authorization code. An app lets users connect their bank account. To do this, the app exchanges an authorization code with the bank for an access token. Codes are single-use. If a code is used twice, the bank should revoke any tokens already issued for it. A retry that arrives after the first request has finished can return the stored token. But a retry that arrives during the first request will also, concurrently, try to perform the exchange, and that second exchange can revoke the first request’s token. See single-use token recovery.
Problem
How do we prevent retries from attempting to do work concurrently?
Solution
Take a lock on the idempotency key:
- Try to acquire a lock on the idempotency key.
- If the lock is held, either wait for it, or return an error telling the client to retry later, e.g. HTTP 409 Conflict.
- Perform the operation.
- Release the lock.
The lock only stops retries overlapping. A retry that arrives after the lock is released will perform the operation again. Combine the lock with something that makes sequential retries safe, such as a response record or a recovery point.
Doing this automatically for every top-level write operation can reduce the cognitive load of needing to think through possible concurrency problems. However, it is often not necessary.
Alternatives
There are often alternatives to idempotency key locks, especially if correctness is your only goal and your storage system supports atomic read-then-write operations, and if operations on other systems are idempotent.
See also
Related
- At-most-once guard – Write to a system at most once
- Atomic read-then-write – Concurrently write data based on current state
- Completer – Complete unfinished operations, even if clients give up retrying
- Idempotency key – Identify identical requests
- Recovery point – Record current progress to allow recovery with minimal rework
- Response record – Return the same response for every retry
- Single-use token recovery – Handle concurrent use of a single-use token