AEP 2: ArmoniK APIs Definition Guidelines

ArmoniK Enhancement Proposal

AEP

2

Title

ArmoniK APIs Definition Guidelines

Author

Jérôme Gurhem <jgurhem@aneo.fr>

Status

Draft

Type

Process

Creation Date

2023-09-14

Abstract

This AEP describes the guidelines that should be followed when updating ArmoniK APIs. This mainly pertains to gRPC APIs that are defined in ArmoniK.Api.

Motivation

In order to create stable API, their development should have guidelines to make sure that they have expected qualities.

Rationale

Current APIs are not following any guidelines and this led to API which are complicated, not uniform and does not follow any style guide such as Google Style Guide. This AEP provides guidelines and suggests tools that can be used to ensure that the guidelines are correctly implemented.

Specification

APIs should be easy and simple to use

One of the first principles in software development, the KISS principle, should be used to define these APIs. We want API to be as simple possible with the least complexity. This will diminish the difficulty to start using ArmoniK for other developers.

APIs should have comments

Service, Messages, Enums and RPC methods should be commented so that documentation can be automatically generated. These comments should also include noteworthy implementation details such as what is the intended behavior and the expected result.

Use linter to validate proto files

A protobuf linter must be used to validate that the proto files are properly written. We recommend Buf Linter. The following configuration could be used.

version: v1
lint:
  use:
    - DEFAULT
    - COMMENTS

It includes the following rules:

  • Best practices suggest that request and response message for every RPC should be unique as changing one of these message impact every RPC that uses it. It is recommended to have a wrapper message for RPC request and response types.

  • Files should declare a package.

  • Files should be in a directory according to their package name and files in the same directory should have the same package.

  • Services and Messages should be named appropriately.

  • Enums should have itself as prefix.

  • Only leading comments are considered by Buf, so proto files should include leading comments for everything.

See Buf Linter Rules for an extensive description of each rule used.

Avoid using oneof

gPRC oneof means that a value can be one of several messages. It highly increase the complexity of using a message when they are used in streams and/or responses.

When used in streams, oneof messages are complicated to use since messages could need to be sent in a specific order. The usage of this kind of method could be very difficult without appropriate documentation and state machines.

When used in responses, every case should be accounted for even for the nominal case. It adds a lot of complexity especially in cases of errors included in oneof responses. Clients have to manage every case with the possibility of most of the cases being impossible to recover from or being useless.

Sometimes, it is unavoidable to use oneof. In these cases, oneof messages should be wrapped in their own messages. An example of such guidelines can be found below.

// Avoid this
message MyMessage {
  string id = 0;
  oneof foo {
    string name = 1;
    int id = 2;
  }
  oneof bar {
    string name = 3;
    int id = 4;
  }
}


// Do this instead
message MyMessage {
  message Foo {
    oneof foo {
      string name = 0;
      int id = 1;
    }
  }
  message Bar {
    oneof bar {
      string name = 0;
      int id = 1;
    }
  }

  string id = 0;
  Foo foo = 1;
  Bar bar = 2;
}

Avoid using streams or provide unitary alternative

Some systems does not support streaming such as gRPC web (gRPC web does not support streaming from client to server). Therefore we should avoid stream based RPC methods especially in the control plane. Stream based methods can still be added if an alternative with unitary calls is also added. They can be useful for some optimizations so they are not completely banned.

RPC definition and error handling

For each RPC method, provide a dedicated request type, a dedicated response type, and a dedicated error type. This should be the case even if the message is empty or if the message has the same fields as other messages. The status code of the response must indicate if the query has been successful or not. In case of error, the appropriate status code should be used (please refer to the doc) and the error message will be sent in the response metadata to give further details. This aims to simplify response messages as much as possible for the nominal case.

Request/Response/Error Messages and services should be provided in the same file for a better readability. Other messages that might be reused accross several services must be in a separate file.

// My service that does things
service MyService {
    // My first RPC method
    rpc MyRpc1(MyRpc1Request) return MyRpc1Response;
}

// Exemple request to send to the server
message MyRpc1Request {
    // An example ID the client send to the server to process
    string id = 1;
}

// Exemple response when call is sucessful
message MyRpc1Response {
    // The internal data that is sent client-side
    string my_string_data = 1;
}

// Example error that will be sent to client as response metadata included in the rpc error
// It may include oneofs when there are several possibilities
// This should be used in conjunction with gRPC status codes and exceptions
message MyRpc1Error {
    // error that occured
    string error_details = 1;
}

gRPC JSON transcoding

gRPC JSON transcoding is a capability that allows to use JSON REST-like APIs to interact with a service implemented in gRPC. It is suggested to make it available for the control plane so that clients can use REST-like request to interact with ArmoniK as it will simplify the use of ArmoniK in environments where gRPC clients are not available or difficult to use.

Backwards Compatibility

If breaking changes are introduced in new proposal, API version should be increased.

Reference Implementation

A reference implementation will come as a Pull Request in ArmoniK.Api repository and this will be a first draft of our APIs that follow these guidelines.

Rejected Ideas

A few of the ideas that were rejected when designing our gRPC APIs.

Implicit actions in services

Implicit creation of data and metadata in ArmoniK should be avoided.

“Explicit is better than implicit” – The Zen of Python

Simpler filter system

Actual filters are quite complicated with several levels of oneof that we would rather avoid but the alternatives were not great either:

  • Using a string containing the filter request means losing gRPC typing AND maintaining a completely new and custom query system. We did not want either of them.

oneof in response to manage errors

In an earlier attempt, we tried to manage errors using oneofs inside of the response message to indicate if the call was successful or not. We observed that it made the response handling much more complex and verbose: user should manage every case even the cases that they cannot recover from. Part of the complexity reason was the sole presence of oneof.

Using status codes to handle errors is much simpler and more explicit on the client side and is therefore the prefer way to handle errors (see: error handling)