API Conventions

API: Stable

In this document we cover the API conventions for UCloud along with giving guidance on how to implement them in UCloud.

Network Protocol

Before we start our discussion of API conventions and architecture, we will first take a brief moment to discuss the network protocols used for communication. All clients, e.g. the web-client, interact with UCloud by invoking remote procedure calls (RPC) over the network.

UCloud implements RPCs over the network using either an HTTP backend or a WebSocket backend. By default, RPCs use the HTTP backend. RPCs which require real-time output, or the ability to push from the server-side use the WebSocket backend.

GET /api/jobs/browse?
    includeProduct=true&
    itemsPerPage=50&
    sortBy=CREATED_AT HTTP/1.1
Authorization: Bearer <TOKEN>
Project: <Project>

---

HTTP/2 200 OK
content-type: application/json; charset=UTF-8
content-length: <LENGTH>
job-id: <ID>

{
  "itemsPerPage": 50,
  "items": [
    {
      "id": "<ID>",
      "updates": [],
      "billing": {},
      "parameters": {
        "application": {
          "name": "terminal-ubuntu",
          "version": "0.8.6"
        }
      }
    }
  ],
  "next": null
}

Figure 1: Simplified and formatted view of the HTTP backend

GET /api/jobs HTTP/1.1
Connection: keep-alive, Upgrade
Upgrade: websocket

C->S:
{
  "call": "jobs.follow",
  "streamId": "0",
  "payload": {
    "id": "<ID>"
  },
  "project": "<PROJECT>",
  "bearer": "<TOK>"
}

S->C:
{
  "type": "message",
  "streamId": "0",
  "payload": {
    "newStatus": {
      "state": "IN_QUEUE",
    }
  }
}

S->C:
{
  "type": "message",
  "streamId": "0",
  "payload": {
    "log": [
      {
        "rank": 0,
        "stdout": "Application is now running\n"
      }
    ],
  }
}

Figure 2: Simplified and formatted view of the WebSocket backend

Both network backends take a simple approach to the problem while remaining familiar to most who have used other HTTP or WebSocket based APIs. UCloud has taken a few nontraditional approaches for certain problems, we aim to only do this when the traditional approaches have significant flaws, such as performance and reliability. We will attempt to cover the differences later.

Authentication

You can read more about authentication using the different network protocols here.

Data Serialization

As is visible from Figure 1 and Figure 2, UCloud uses JSON for data serialization. This format has been picked because of its familiarity to most developers. While also being easy to read by humans and machines.


📝 NOTE: UCloud generally ignores the Accept and Content-Type headers in request payloads. UCloud does not perform any type of content-type negotiation. This significantly simplifies our server-side code. We do not believe there are significant benefits in supporting multiple representation of the same data through this mechanism.

UCloud will include a valid Content-Type header in response payloads. This content-type will only describe the format used to serialize the data. It will not describe the contents of the data. That is, UCloud assumes that clients are already aware of the data that any specific RPC produces.


UCloud attempts to follow the principal of relaxed requirements for request payloads while following a more strict and consistent approach for response payloads. You can read more about how we implement this in practice here.

RPC Interfaces

The API design of UCloud is centered around resources. A resource is any entity which clients can query information about or interact with. For example, clients of UCloud can ask about the status of a compute job. Clients can also ask UCloud to cancel an existing compute job.

All resources in UCloud have a chunk of the URL path carved out for them. This ‘namespace’ serves as their area and all requests are generally routed in the same direction. For example, avatars uses /api/avatar for all their requests. This namespace is commonly referred to as the baseContext inside of UCloud.

All RPCs aim to either describe the state of resources in UCloud or interact with one or more resources. RPCs of UCloud fall into one of the following categories:

Category Description
Retrieve Requests a specific resource from UCloud.
Browse Requests a set of resources from UCloud defined by some criteria. The data is returned in a predictable and deterministic way.
Search Requests a set of resources from UCloud defined by a search criteria. Data is ranked by an undefined criteria. Results are not predictable or deterministic.
Create Instructs UCloud to create one or more resources.
Delete Instructs UCloud to delete one or more resources.
Update Interacts with a UCloud resource which may cause an update to the resource's data model.
Subscribe (WebSocket only) Subscribes for real-time updates of a resource.
Verify Category used for calls which verify state between two decentralized services.

📝 NOTE: Resources don’t have to implement one of each call category. For example, some resources cannot be created explicitly by a client and can only be interacted with. For example, all UCloud users have exactly one avatar. Clients can choose to update their avatar, but they cannot create a new avatar in the system.


Retrieve

Used for RPCs which indicate a request for a specific resource from UCloud.

Calls using this category must fetch a single resource. This resource is normally fetched by its ID but it may also be retrieved by some other uniquely identifying piece of information. If more than one type of identifier is supported then the call must reject requests (with 400 Bad Request) which does not include exactly one identifier.

Results for this call type should always be deterministic. If the resource cannot be found then calls must return 404 Not Found.

On HTTP this will apply the following routing logic:

  • Method: GET

  • Path (no sub-resource): ${baseContext}/retrieve

  • Path (with sub-resource): ${baseContext}/retrieve${subResource}

The entire request payload will be bound to the query parameters of the request.

Example usage:

private fun CallDescriptionContainer.httpRetrieveExample() {
    data class MyResource(val id: String, val number: Int)

    val baseContext = "/api/myresources"

    run {
        // Retrieve by unique identifier
        data class ResourcesRetrieveRequest(val id: String)

        val retrieve = call<ResourcesRetrieveRequest, MyResource, CommonErrorMessage>("retrieve") {
            httpRetrieve(baseContext)
        }
    }

    run {
        // Retrieve by one or more unique parameters
        data class ResourcesRetrieveRequest(
            val id: String? = null, // optional parameters should have a default value in Kotlin code
            val number: Int? = null,
        ) {
            init {
                // In this case `number` also uniquely identifies the resource. The code must reject requests
                // that do not include exactly one query criteria.

                if ((id == null && number == null) || (id != null && number != null)) {
                    throw RPCException("Request must unique include either 'id' or 'number'", HttpStatusCode.BadRequest)
                }
            }
        }

        val retrieve = call<ResourcesRetrieveRequest, MyResource, CommonErrorMessage>("retrieve") {
            httpRetrieve(baseContext)
        }
    }
}

Browse

Used for RPCs which requests a set of resources from UCloud defined by some criteria.

Browse RPCs are typically used for pagination of a resource, defined by some criteria.

All data returned by this API must be returned in a predictable and deterministic way. In particular, this means that implementors need to take care and implement a consistent sort order of the items. This is unlike the [httpSearch] endpoints which are allowed to sort the items by any criteria. Results from a search RPC is suitable only for human consumption while results from a browse RPC are suitable for human and machine consumption.

On HTTP this will apply the following routing logic:

  • Method: GET

  • Path: ${baseContext}/browse

The entire request payload will be bound to the query parameters of the request.

Example usage:

private fun CallDescriptionContainer.httpBrowseExample() {
    data class MyResource(val id: String, val number: Int)

    val baseContext = "/api/myresources"

    run {
        // Browse via the pagination v2 API
        data class ResourcesBrowseRequest(
            override val itemsPerPage: Int? = null,
            override val next: String? = null,
            override val consistency: PaginationRequestV2Consistency? = null,
            override val itemsToSkip: Long? = null,
        ) : WithPaginationRequestV2

        val browse = call<ResourcesBrowseRequest, PageV2<MyResource>, CommonErrorMessage>("browse") {
            httpBrowse(baseContext)
        }
    }
}

Create

Used for RPCs which request the creation of one or more resources in UCloud.

RPCs in this category should accept request payloads in the form of a bulk request.

Calls in this category should respond back with a list of newly created IDs for every resource that has been created. A client can choose to use these to display information about the newly created resources.

On HTTP this will apply the following routing logic:

  • Method: POST

  • Path: ${baseContext}

The request payload will be read, fully, from the HTTP request body.


⚠ WARNING: All request items listed in the bulk request must be treated as a single transaction. This means that either the entire request succeeds, or the entire request fails.


Example usage:

private fun CallDescriptionContainer.httpCreateExample() {
    data class MyResource(val id: String, val number: Int)

    val baseContext = "/api/myresources"

    // NOTE: We use a separate data model which only contains the model specification (i.e. without additional
    // metadata about the resource, such as unique id)
    data class MyResourceSpecification(val number: Int)
    data class ResourcesCreateResponse(val ids: List<String>)

    val create = call<BulkRequest<MyResourceSpecification>, ResourcesCreateResponse, CommonErrorMessage>("create") {
        httpCreate(baseContext)
    }
}

Delete

Used for RPCs which request the deletion of one or more resources in UCloud.

RPCs in this category should accept request payloads in the form of a bulk request.

Calls in this category should choose to accept as little information about the resources, while still uniquely identifying them.

On HTTP this will apply the following routing logic:

  • Method: DELETE

  • Path: ${baseContext}

The request payload will be read, fully, from the HTTP request body.


⚠ WARNING: All request items listed in the bulk request must be treated as a single transaction. This means that either the entire request succeeds, or the entire request fails.


Example usage:

private fun CallDescriptionContainer.httpDeleteExample() {
    data class MyResource(val id: String, val number: Int)

    val baseContext = "/api/myresources"

    val delete = call<BulkRequest<FindByStringId>, Unit, CommonErrorMessage>("delete") {
        httpDelete(baseContext)
    }
}

Update

Interacts with a UCloud resource, typically causing an update to the resource itself.

RPCs in this category should accept request payloads in the form of a bulk request.

This category of calls can be used for any type of interaction with a UCloud resource. Many resources in UCloud are represented by some underlying complex logic. As a result, it is typically not meaningful or possible to simple patch the resource’s data model directly. Instead, clients must interact with the resources to change them.

For example, if a client wishes to suspend a virtual machine, they would not send back a new representation of the virtual machine stating that it is suspended. Instead, clients should send a suspend interaction. This instructs the backend services to correctly initiate suspend procedures.

All update operations have a name associated with them. Names should clearly indicate which underlying procedure will be triggered by the RPC. For example, suspending a virtual machine would likely be called suspend.

On HTTP this will apply the following routing logic:

  • Method: POST (always)

  • Path: ${baseContext}/${op} where op is the name of the operation, e.g. suspend

The request payload will be read, fully, from the HTTP request body.


⚠ WARNING: All request items listed in the bulk request must be treated as a single transaction. This means that either the entire request succeeds, or the entire request fails.


Example usage:

private fun CallDescriptionContainer.httpUpdateExample() {
    data class MyResource(val id: String, val number: Int)

    val baseContext = "/api/myresources"

    data class ResourcesIncrementRequestItem(val incrementStep: Int = 1)
    data class ResourcesCreateResponse(val ids: List<String>)

    val increment = call<BulkRequest<ResourcesIncrementRequestItem>, ResourcesCreateResponse, CommonErrorMessage>(
        "increment"
    ) {
        httpUpdate(baseContext, "increment")
    }
}

Subscribe

No documentation.

Verify

Category used for calls which need to verify state between two decentralized services.

This category can allows the services to make sure that their state are consistent between two versions which have different ideas of the state. This can happen between, for example: service <=> service or service <=> provider. This category is typically found in the provider APIs and are periodically invoked by the UCloud side.

Verify calls do not have to send the entire database in a single call. Instead it should generally choose to send only a small snapshot of the database. This allows both sides to process the request in a reasonable amount of time. The recipient of this request should notify the other end through other means if these are not in sync (a call is typically provided for this).

On HTTP this will apply the following routing logic:

  • Method: POST

  • Path (no sub-resource): ${baseContext}/verify

  • Path (with sub-resource): ${baseContext}/verfiy${subResource}

The request payload will be read from the HTTP request body.

Example usage:

private fun CallDescriptionContainer.httpVerifyExample() {
    data class MyResource(val id: String, val number: Int)

    val baseContext = "/api/myresources"

    // Note: We send the entire resource in the request to make sure both sides have enough information to verify
    // that state is in-sync.
    val verify = call<BulkRequest<MyResource>, Unit, CommonErrorMessage>("verify") {
        httpVerify(baseContext)
    }
}

Bulk Request

A base type for requesting a bulk operation.

Property Type Description
items Array<Any> No documentation

⚠ WARNING: All request items listed in the bulk request must be treated as a single transaction. This means that either the entire request succeeds, or the entire request fails.

There are two exceptions to this rule:

  1. Certain calls may choose to only guarantee this at the provider level. That is if a single call contain request for multiple providers, then in rare occasions (i.e. crash) changes might not be rolled back immediately on all providers. A service MUST attempt to rollback already committed changes at other providers.

  2. The underlying system does not provide such guarantees. In this case the service/provider MUST support the verification API to cleanup these resources later.


Pagination

PaginationRequestV2

The base type for requesting paginated content.

Property Type Description
itemsPerPage Int Requested number of items per page. Supported values: 10, 25, 50, 100, 250.
next String? A token requesting the next page of items
consistency ("PREFER" or "REQUIRE")? Controls the consistency guarantees provided by the backend
itemsToSkip Long? Items to skip ahead

Paginated content can be requested with one of the following consistency guarantees, this greatly changes the semantics of the call:

Consistency Description
PREFER Consistency is preferred but not required. An inconsistent snapshot might be returned.
REQUIRE Consistency is required. A request will fail if consistency is no longer guaranteed.

The consistency refers to if collecting all the results via the pagination API are consistent. We consider the results to be consistent if it contains a complete view at some point in time. In practice this means that the results must contain all the items, in the correct order and without duplicates.

If you use the PREFER consistency then you may receive in-complete results that might appear out-of-order and can contain duplicate items. UCloud will still attempt to serve a snapshot which appears mostly consistent. This is helpful for user-interfaces which do not strictly depend on consistency but would still prefer something which is mostly consistent.

The results might become inconsistent if the client either takes too long, or a service instance goes down while fetching the results. UCloud attempts to keep each next token alive for at least one minute before invalidating it. This does not mean that a client must collect all results within a minute but rather that they must fetch the next page within a minute of the last page. If this is not feasible and consistency is not required then PREFER should be used.


📝 NOTE: Services are allowed to ignore extra criteria of the request if the next token is supplied. This is needed in order to provide a consistent view of the results. Clients should provide the same criterion as they paginate through the results.


PageV2

Represents a single ‘page’ of results

Property Type Description
itemsPerPage Int The expected items per page, this is extracted directly from the request
items Array<Any> The items returned in this page
next String The token used to fetch additional items from this result set

Every page contains the items from the current result set, along with information which allows the client to fetch additional information.

Error Messages

A generic error message

Property Type Description
why String Human readable description of why the error occurred. This value is generally not stable.
errorCode String? Machine readable description of why the error occurred. This value is stable and can be relied upon.

UCloud uses HTTP status code for all error messages. In addition and if possible, UCloud will include a message using a common format. Note that this is not guaranteed to be included in case of a failure somewhere else in the network stack. For example, UCloud’s load balancer might not be able to contact the backend at all. In such a case UCloud will not include a more detailed error message.

Data Models

In this section, we will cover conventions used for the data models of UCloud. It covers general naming conventions along with a list of common terminology which should be used throughout UCloud. We also make links to other relevant data models which are used as a base throughout of UCloud.

We start by covering the naming conventions.This covers the code used in the UCloud api sub-projects. That is, the code which is publicly facing through our RPC interface. These conventions should also be adopted as much as possible internally in the UCloud code.

Naming Conventions: Properties

1: Use camelCase for all properties


2: All abbreviations must use be lower-cased while following the camelCase rule

For example:

  • Including a URL:

    • Correct: includeUrl

    • Incorrect: includeURL

  • Referencing an ID:

    • Correct: id

    • Incorrect: ID


3: Boolean flags that indicate that data should be included must use the include prefix

Examples:

  • Correct: includeBalance

    • This should cause the balance property to be populated in the returned data model

  • Incorrect: withBalance


4: Properties used to filter data must use the filter prefix

Examples:

  • Correct: filterArea

  • Correct: filterProvider

  • Incorrect: searchByArea (should use filter prefix)

  • Incorrect: area (prefix missing)


Naming Conventions: Classes

1: Use PascalCase for all classes


2: CallDescriptionContainers should be placed in a file named after the container

  • Correct: Jobs.kt containing Jobs

  • Incorrect: JobDescriptions.kt containing Jobs