AIP-162
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:
message Book {
// The name of the book.
string name = 1;
// 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.
string revision_id = 5 [
(google.api.field_behavior) = IMMUTABLE,
(google.api.field_behavior) = OUTPUT_ONLY];
// The timestamp that the revision was created.
google.protobuf.Timestamp revision_create_time = 6
[(google.api.field_behavior) = OUTPUT_ONLY];
}
- 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 UUID version 4 (random).- The
revision_id
field must document when new revisions are created (see committing revisions below). - The
revision_id
field should document the format of revision IDs.
- The
- The resource must contain a
revision_create_time
field, which should be agoogle.protobuf.Timestamp
(see AIP-142).
Note: A randomly generated string is preferred over other methods, 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_name}@{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 name. However, they
must not accept a revision in situations that mutate the resource, and
should error with INVALID_ARGUMENT
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 methods 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 name with a
revision ID in the standard Get
method (AIP-131):
message GetBookRequest {
// The name of the book.
// Example: publishers/123/books/les-miserables
//
// In order to retrieve a previous revision of the book, also provide
// the revision ID.
// Example: publishers/123/books/les-miserables@c7cfa2a8
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "library.googleapis.com/Book"
}];
}
If the user passes a revision ID that does not exist, the API must fail
with a NOT_FOUND
error.
APIs must return a name
value corresponding to what the user sent. If the
user sent a name with no revision ID, the returned name
string must not
include the revision ID either. Similarly, if the user sent a name with a
revision ID, the returned name
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 method:
rpc TagBookRevision(TagBookRevisionRequest) returns (Book) {
option (google.api.http) = {
post: "/v1/{name=publishers/*/books/*}:tagRevision"
body: "*"
};
}
message TagBookRevisionRequest {
// The name of the book to be tagged, including the revision ID.
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "library.googleapis.com/Book"
}];
// The tag to apply.
// The tag should be at most 40 characters, and match `[a-z][a-z0-9-]{3,38}[a-z0-9]`.
string tag = 2 [(google.api.field_behavior) = REQUIRED];
}
- The
name
field should require an explicit revision ID to be provided.- The field should be annotated as required.
- The field should identify the resource type that it references.
- The
tag
field should be annotated as required.- 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
name
fields.- If the user sends a tag, the API must return the tag in the resource's
name
field, but the revision ID in the resource'srevision_id
field. - If the user sends a revision ID, the API must return the revision ID in
both the
name
field and therevision_id
field.
- If the user sends a tag, the API must return the tag in the resource's
- If the user calls the
Tag
method 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 method for
listing the revision history for a resource, with a structure similar to
standard List
methods (AIP-132):
rpc ListBookRevisions(ListBookRevisionsRequest)
returns (ListBookRevisionsResponse) {
option (google.api.http) = {
get: "/v1/{name=publishers/*/books/*}:listRevisions"
};
}
message ListBookRevisionsRequest {
// The name of the book to list revisions for.
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "library.googleapis.com/Book"
}];
// The maximum number of revisions to return per page.
int32 page_size = 2;
// The page token, received from a previous ListBookRevisions call.
// Provide this to retrieve the subsequent page.
string page_token = 3;
}
message ListBookRevisionsResponse {
// The revisions of the book.
repeated Book books = 1;
// A token that can be sent as `page_token` to retrieve the next page.
// If this field is omitted, there are no subsequent pages.
string next_page_token = 2;
}
While revision listing methods are mostly similar to standard List
methods
(AIP-132), the following important differences apply:
- The first field in the request message must be called
name
rather thanparent
(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
name
field.- If providing the full resource is expensive or infeasible, the revision
object may only populate the
string name
,string revision_id
, andgoogle.protobuf.Timestamp revision_create_time
fields instead. Thestring name
field must include the resource name and an explicit revision ID, which can be used for an explicitGet
request. The API must document that it will do this.
- If providing the full resource is expensive or infeasible, the revision
object may only populate the
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 revision 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 name
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 method:
rpc CommitBook(CommitBookRequest) returns (Book) {
option (google.api.http) = {
post: "/v1/{name=publishers/*/books/*}:commit"
body: "*"
};
}
message CommitBookRequest {
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "library.googleapis.com/Book"
}];
}
- The method must use the
POST
HTTP verb. - The method should return the resource, and the resource name must include the revision ID.
- The request message must include the
name
field.- The field should be annotated as required.
- The field should identify the resource type 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
method:
rpc RollbackBook(RollbackBookRequest) returns (Book) {
option (google.api.http) = {
post: "/v1/{name=publishers/*/books/*}:rollback"
body: "*"
};
}
- The method must use the
POST
HTTP verb. - The method should return the resource, and the resource name must include the revision ID.
message RollbackBookRequest {
// The book being rolled back.
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "library.googleapis.com/Book"
}];
// The revision ID to roll back to.
// It must be a revision of the same book.
//
// Example: c7cfa2a8
string revision_id = 2 [(google.api.field_behavior) = REQUIRED];
}
- The request message must have a
name
field to identify the resource being rolled back.- The field should be annotated as required.
- The field should identify the resource type 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.
- The API must fail the request with
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 method to delete revisions, with a structure similar to
Delete
(AIP-135) methods:
rpc DeleteBookRevision(DeleteBookRevisionRequest)
returns (Book) {
option (google.api.http) = {
delete: "/v1/{name=publishers/*/books/*}:deleteRevision"
};
}
message DeleteBookRevisionRequest {
// The name of the book revision to be deleted, with a revision ID explicitly
// included.
//
// Example: publishers/123/books/les-miserables@c7cfa2a8
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "library.googleapis.com/Book"
}];
}
- The request message must have a
name
field to identify the resource revision being deleted.- The explicit revision ID must be required (the method 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.
- The field should identify the resource type that it references.
- The explicit revision ID must be required (the method must fail
with
- The API must not overload the
DeleteBook
method 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 method must not cause the resource to be deleted.
- Because a resource should not exist with zero revisions, the method
must fail with
FAILED_PRECONDITION
if the user attempts to delete the only revision.
- Because a resource should not exist with zero revisions, the method
must fail with
- The response message must be the resource itself. There is no
DeleteBookRevisionResponse
.- The response should include the fully-populated resource unless it is infeasible to do so.
Character Collision
Most resource names 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 name, and therefore uses the #
character to
indicate a revision.
APIs should avoid permitting the @
character in resource names, 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.
Changelog
- 2021-04-27: Added guidance on returning the resource from Delete Revision.