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 resource must contain a revision_create_time field, which should be a google.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 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's revision_id field.
    • If the user sends a revision ID, the API must return the revision ID in both the name field and the revision_id field.
  • 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 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 name field.
    • If providing the full resource is expensive or infeasible, the revision object may only populate the string name, string revision_id, and google.protobuf.Timestamp revision_create_time fields instead. The string name field must include the resource name 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 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.

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 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.

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 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.
  • 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.