Skip to content

Commit f7d100a

Browse files
rofrankeltoumorokoshi
authored andcommitted
Adopt AEP-155: Idempotency (aep-dev#77)
* Adopt AEP-155: Idempotency * Address Yusuke's comments. * Update aep/general/0155/aep.md * lint * Qualify proto-specific guidance. * Update to reflect new common components and live discussion. * Minor rephrasing of list. * Clarify invalid `first_sent`. * Remove section about stale success responses.
1 parent 181cb7f commit f7d100a

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed

aep/general/0155/aep.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Idempotency
2+
3+
It is sometimes useful for an API to have a unique, customer-provided
4+
identifier for particular requests. This can be useful for several purposes,
5+
such as:
6+
7+
- De-duplicating requests from parallel processes
8+
- Ensuring the safety of retries
9+
- Auditing
10+
11+
The purpose of idempotency keys is to provide idempotency guarantees: allowing
12+
the same request to be issued more than once without subsequent calls having
13+
any effect. In the event of a network failure, the client can retry the
14+
request, and the server can detect duplication and ensure that the request is
15+
only processed once.
16+
17+
## Guidance
18+
19+
APIs **may** add a `aep.api.IdempotencyKey idempotency_key` parameter to
20+
request messages (including those of standard methods) in order to uniquely
21+
identify particular requests. API servers **must not** execute requests with
22+
the same `idempotency_key` more than once.
23+
24+
```proto
25+
message CreateBookRequest {
26+
// The parent resource where this book will be created.
27+
// Format: publishers/{publisher}
28+
string parent = 1 [
29+
(google.api.field_behavior) = REQUIRED,
30+
(google.api.resource_reference) = {
31+
child_type: "library.example.com/Book"
32+
}];
33+
34+
// The book to create.
35+
Book book = 2 [(google.api.field_behavior) = REQUIRED];
36+
37+
// This request is only idempotent if `idempotency_key` is provided.
38+
//
39+
// This key will be honored for at least one hour after the first time it is
40+
// seen by the server.
41+
//
42+
// The key is restricted to 36 ASCII characters. A random UUID is recommended.
43+
aep.api.IdempotencyKey idempotency_key = 3 [
44+
(aep.api.field_info).minimum_lifetime = { seconds: 3600 }
45+
];
46+
}
47+
```
48+
49+
- [`aep.api.IdempotencyKey`][] has a `key` and a `first_sent` timestamp.
50+
51+
- `key` is simply a unique identifier.
52+
53+
- Providing an idempotency key **must** guarantee idempotency.
54+
55+
- If a duplicate request is detected, the server **must** return one of:
56+
57+
- A response equivalent to the response for the previously successful
58+
request, because the client most likely did not receive the previous
59+
response.
60+
- An error indicating that the `first_sent` field of the idempotency key is
61+
invalid or cannot be honored (expired, in the future, or differs from a
62+
previous `first_sent` value with the same `key`).
63+
- An error, if returning an equivalent response is not possible.
64+
65+
For example, if a resource was created, then deleted, and then a
66+
duplicate request to create the resource is received, the server **may**
67+
return an error if returning the previously created resource is not
68+
possible.
69+
70+
- APIs **should** honor idempotency keys for at least an hour.
71+
- When using protocol buffers, idempotency keys that are UUIDs **must** be
72+
annotated with a minimum lifetime using the extension
73+
[`(aep.api.field_info).minimum_lifetime`][].
74+
75+
- The `idempotency_key` field **must** be provided on the request message to
76+
which it applies (and it **must not** be a field on resources themselves).
77+
78+
- The `first_sent` field can be used by API servers to determine if a key is
79+
expired. API servers **must** reject requests with expired keys, and
80+
**must** reject requests with keys that are in the future. When feasible,
81+
API servers **should** reject requests that use the same `key` but have a
82+
different `first_sent` timestamp.
83+
- The `key` field **must** be able to be a UUID, and **may** allow UUIDs to
84+
be the only valid format. The format restrictions for idempotency keys
85+
**must** be documented.
86+
87+
- Idempotency keys **should** be optional.
88+
89+
## Further reading
90+
91+
- For which codes to retry, see [AEP-194](https://aep.dev/194).
92+
- For how to retry errors in client libraries, see
93+
[AEP-4221](https://aep.dev/4221).
94+
95+
## Rationale
96+
97+
### Naming the field `idempotency_key`
98+
99+
The original content from which this AEP is derived defines a `request_id`
100+
field; we define `idempotency_key` instead for two reasons:
101+
102+
1. There is an [active Internet-Draft][idempotency-key-draft] to standardize an
103+
HTTP header named `Idempotency-Key`.
104+
1. There may be edge cases in which separately identifying idempotent requests
105+
is useful; `request_id` would be more appropriate for such use cases. For
106+
example, an API producer might be testing the idempotency behavior of the
107+
API server, and might want to issue multiple requests with the same
108+
`idempotency_key` and trace the behavior of each request separately.
109+
110+
<!-- prettier-ignore-start -->
111+
[idempotency-key-draft]: https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/
112+
[`aep.api.IdempotencyKey`]: https://buf.build/aep/api/file/main:aep/api/idempotency_key.proto#L21
113+
[`(aep.api.field_info).minimum_lifetime`]: https://buf.build/aep/api/file/main:aep/api/field_info.proto#L35
114+
<!-- prettier-ignore-end -->
115+
116+
### Using UUIDs for request identification
117+
118+
When a value is required to be unique, leaving the format open-ended can lead
119+
to API consumers incorrectly providing a duplicate identifier. As such,
120+
standardizing on a universally unique identifier drastically reduces the chance
121+
for collisions when done correctly.
122+
123+
## Changelog
124+
125+
- **2023-23-20**: Adopt AEP from from Google's AIP with the following changes:
126+
- Rename field from `request_id` to `idempotency_key` (plus some minor
127+
releated rewording).
128+
- Add a common component [`aep.api.IdempotencyKey`][] and use this rather
129+
than `string` for the `idempotency_key` field; add related guidance about
130+
`IdempotencyKey.first_seen`.
131+
- Remove guidance about annotating `idempotency_key` with
132+
`(google.api.field_info).format`.
133+
- Add guidance about annotating `idempotency_key` with
134+
[`(aep.api.field_info).minimum_lifetime`].
135+
- Update guidance about responses to be more explicit about success and error
136+
cases, while allowing "equivalent" rather than identical responses for
137+
subsequent requests.
138+
- Temporarily removed the section about stale success responses, pending
139+
further discussion.
140+
- **2023-10-02**: Add UUID format extension guidance.
141+
- **2019-08-01**: Changed the examples from "shelves" to "publishers", to
142+
present a better example of resource ownership.

aep/general/0155/aep.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
id: 155
3+
state: approved
4+
slug: idempotency
5+
created: 2019-05-06
6+
placement:
7+
category: requests
8+
order: 60

0 commit comments

Comments
 (0)