Skip to content

Commit

Permalink
feat: AIP-162 – Resource revisions
Browse files Browse the repository at this point in the history
  • Loading branch information
Luke Sneeringer committed May 19, 2021
1 parent 17ef1fc commit ba39650
Show file tree
Hide file tree
Showing 3 changed files with 489 additions and 0 deletions.
280 changes: 280 additions & 0 deletions aip/general/0162/aip.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
# Resource Revisions

Some APIs need to have resources with a revision history, where users can
reason about the state of the resource over time. There are several reasons for
this:

- Users may want to be able to roll back to a previous revision, or diff
against a previous revision.
- An API may create data which is derived in some way from a resource at a
given point in time. In these cases, it may be desirable to snapshot the
resource for reference later.

**Note:** We use the word _revision_ to refer to a historical reference for a
particular resource, and intentionally avoid the term _version_, which refers
to the version of an API as a whole.

## Guidance

APIs **may** store a revision history for a resource if it is useful to users.

APIs implementing resources with a revision history **must** provide a
`revision_id` field on the resource:

```typescript
interface Book {
// The ID of the book.
id: string;

// Other fields…

// The revision ID of the book.
// A new revision is committed whenever the book is changed in any way.
// The format is an 8-character hexadecimal string.
readonly revisionId: string;

// The timestamp that the revision was created.
readonly revisionCreateTime: string; // ISO 8601
}
```

- The resource **must** contain a `revision_id` field, which **should** be a
string and contain a short, automatically-generated random string. A good
rule of thumb is the last eight characters of a UUID4.
- The `revision_id` field **must** document when new revisions are created
(see [committing revisions](#committing-revisions) below).
- The `revision_id` field **should** document the format of revision IDs.
- The resource **must** contain a `revision_create_time` field, which
**should** be a timestamp (see AIP-142).

**Note:** A randomly generated string is preferred over other operations, such
as an auto-incrementing integer, because there is often a need to delete or
revert revisions, and a randomly generated string holds up better in those
situations.

### Referencing revisions

When it is necessary to refer to a specific revision of a resource, APIs
**must** use the following syntax: `{resource_id}@{revision_id}`. For example:

publishers/123/books/les-miserables@c7cfa2a8

**Note:** The `@` character is selected because it is the only character
permitted by [RFC 1738 §2.2][] for special meaning within a URI scheme that is
not already used elsewhere.

APIs **should** generally accept a resource reference at a particular revision
in any place where they ordinarily accept the resource ID. However, they **must
not** accept a revision in situations that mutate the resource, and should
error with `400 Bad Request` if one is given.

**Important:** APIs **must not** require a revision ID, and **must** default to
the current revision if one is not provided, except in operations specifically
dealing with the revision history (such as rollback) where failing to require
it would not make sense (or lead to dangerous mistakes).

### Getting a revision

APIs implementing resource revisions **should** accept a resource ID with a
revision ID in the standard `Get` operation (AIP-131):

{% tab proto %}

{% sample 'revisions.proto', 'message GetBookRequest' %}

{% endtabs %}

If the user passes a revision ID that does not exist, the API **must** fail
with a `404 Not Found` error.

APIs **must** return a `id` value corresponding to what the user sent. If the
user sent a resource ID with no revision ID, the returned `id` string **must
not** include the revision ID either. Similarly, if the user sent a resource ID
with a revision ID, the returned `id` string **must** explicitly include it.

### Tagging revisions

APIs implementing resource revisions **may** provide a mechanism for users to
tag a specific revision with a user provided name by implementing a "Tag
Revision" custom operation:

{% tab proto %}

{% sample 'revisions.proto', 'rpc TagBookRevision', 'message TagBookRevisionRequest' %}

{% endtabs %}

- The `id` field **should** require an explicit revision ID to be provided.
- The field **should** be [annotated as required][aip-203].
- The field **should** identify the [resource type][aip-123] that it
references.
- The `tag` field **should** be [annotated as required][aip-203].
- Additionally, tags **should** restrict letters to lower-case.
- Once a revision is tagged, the API **must** support using the tag in place of
the revision ID in `id` fields.
- If the user sends a tag, the API **must** return the tag in the resource's
`id` field, but the revision ID in the resource's `revision_id` field.
- If the user sends a revision ID, the API **must** return the revision ID in
both the `id` field and the `revision_id` field.
- If the user calls the `Tag` operation with an existing tag, the request
**must** succeed and the tag updated to point to the new requested revision
ID. This allows users to write code against specific tags (e.g. `published`)
and the revision can change in the background with no code change.

### Listing revisions

APIs implementing resource revisions **should** provide a custom operation for
listing the revision history for a resource, with a structure similar to
standard `List` operations (AIP-132):

{% tab proto %}

{% sample 'revisions.proto', 'rpc ListBookRevisions', 'message ListBookRevisionsRequest' %}

{% endtabs %}

While revision listing operations are mostly similar to standard `List`
operations (AIP-132), the following important differences apply:

- The first field in the request message **must** be called `id` rather than
`parent` (this is listing revisions for a specific book, not a collection of
books).
- The URI **must** end with `:listRevisions`.
- Revisions **must** be ordered in reverse chronological order. An `order_by`
field **should not** be provided.
- The returned resources **must** include an explicit revision ID in the
resource's `id` field.
- If providing the full resource is expensive or infeasible, the revision
object **may** only populate the `id`, `revision_id`, and
`revision_create_time` fields instead. The `id` field **must** include the
resource ID and an explicit revision ID, which can be used for an explicit
`Get` request. The API **must** document that it will do this.

### Child resources

Resources with a revision history **may** have child resources. If they do,
there are two potential variants:

- Child resources where each child resource is a child of the parent resource
as a whole.
- Child resources where each child resource is a child of _a single revision
of_ the parent resource.

If a child resource is a child of a single revision, the child resource's name
**must** always explicitly include the parent's resource ID:

publishers/123/books/les-miserables@c7cfa2a8/pages/42

In `List` requests for such resources, the service **should** default to the
latest revision of the parent if the user does not specify one, but **must**
explicitly include the parent's revision ID in the `id` field of resources in
the response.

If necessary, APIs **may** explicitly support listing child resources across
parent revisions by accepting the `@-` syntax. For example:

GET /v1/publishers/123/books/les-miserables@-/pages

APIs **should not** include multiple levels of resources with revisions, as
this quickly becomes difficult to reason about.

### Committing revisions

Depending on the resource, different APIs may have different strategies for
when to commit a new revision, such as:

- Commit a new revision any time that there is a change
- Commit a new revision when something important happens
- Commit a new revision when the user specifically asks

APIs **may** use any of these strategies. APIs that want to commit a revision
on user request **should** handle this with a `Commit` custom operation:

{% tab proto %}

{% sample 'revisions.proto', 'rpc CommitBook', 'message CommitBookRequest' %}

{% endtabs %}

- The operation **must** use the `POST` HTTP method.
- The operation **should** return the resource, and the resource ID **must**
include the revision ID.
- The request message **must** include the `id` field.
- The field **should** be [annotated as required][aip-203].
- The field **should** identify the [resource type][aip-123] that it
references.

### Rollback

A common use case for a resource with a revision history is the ability to roll
back to a given revision. APIs **should** handle this with a `Rollback` custom
operation:

{% tab proto %}

{% sample 'revisions.proto', 'rpc RollbackBook', 'message RollbackBookRequest' %}

{% endtabs %}

- The operation **must** use the `POST` HTTP method.
- The operation **should** return the resource, and the resource ID **must**
include the revision ID.
- The request message **must** have a `id` field to identify the resource being
rolled back.
- The field **should** be [annotated as required][aip-203].
- The field **should** identify the [resource type][aip-123] that it
references.
- The request message **must** include a `revision_id` field.
- The API **must** fail the request with `NOT_FOUND` if the revision does not
exist on that resource.
- The field **should** be [annotated as required][aip-203].

**Note:** When rolling back, the API should return a _new_ revision of the
resource with a _new_ revision ID, rather than reusing the original ID. This
avoids problems with representing the same revision being active for multiple
ranges of time.

### Deleting revisions

Revisions are sometimes expensive to store, and there are valid use cases to
want to remove one or more revisions from a resource's revision history.

APIs **may** define a operation to delete revisions, with a structure similar
to `Delete` (AIP-135) operations:

{% tab proto %}

{% sample 'revisions.proto', 'rpc DeleteBookRevision', 'message DeleteBookRevisionRequest' %}

{% endtabs %}

- The request message **must** have a `id` field to identify the resource
revision being deleted.
- The explicit revision ID **must** be required (the operation **must** fail
with `INVALID_ARGUMENT` if it is not provided, and **must not** default to
the latest revision).
- The field **should** be [annotated as required][aip-203].
- The field **should** identify the [resource type][aip-123] that it
references.
- The API **must not** overload the `DeleteBook` operation to serve both
purposes (this could lead to dangerous or confusing mistakes).
- If the resource supports soft delete, then revisions of that resource
**should** also support soft delete.
- The operation **must not** cause the _resource_ to be deleted.
- Because a resource **should not** exist with zero revisions, the operation
**must** fail with `FAILED_PRECONDITION` if the user attempts to delete the
only revision.

### Character Collision

Most resource IDs have a restrictive set of characters, but some are very open.
For example, Google Cloud Storage allows the `@` character in filenames, which
are part of the resource ID, and therefore uses the `#` character to indicate a
revision.

APIs **should not** permit the `@` character in resource IDs, and if APIs that
do permit it need to support resources with revisions, they **should** pick an
appropriate separator depending on how and where the API is used, and **must**
clearly document it.

[rfc 1738 §2.2]: https://tools.ietf.org/html/rfc1738
7 changes: 7 additions & 0 deletions aip/general/0162/aip.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
id: 162
state: approved
created: 2019-09-17
placement:
category: design-patterns
order: 88
Loading

0 comments on commit ba39650

Please sign in to comment.