Skip to content

gRPC and OpenAPI Integration #1201

@jdegoes

Description

@jdegoes

This specification defines a new feature whereby Golem workers will be able to interact with gRPC and OpenAPI services in a type-safe way, similar to how Golem workers interact with other workers in a type-safe way.

This feature will greatly expand the number of applications that can be built and deployed on Golem and simplify usability of existing Golem applications currently written using lower-level libraries, such as HTTP client libraries.

When fully implemented, Golem users will be able to simply add new gRPC and OpenAPI dependencies, and then immediately begin interacting with them in a type-safe way, through automatically generated WIT definitions that structure the interacts with the remote services, and through automatically and dynamically implemented stubs that provide implementation of these WIT interfaces and which communicate to the underlying gRPC / OpenAPI dependencies.

Background

In Golem's current worker-to-worker communication, WIT interfaces describe the public interface of components, with wasm-rpc handling the transformation of these interfaces to support the addressing of specific workers. For external service communication, we need similar but stateless transformations from Protobuf and OpenAPI specifications to WIT.

The current process for worker-to-worker communication is as follows:

  1. The user is writing a component A, which depends on a component B. The user adds component B as a dependency of component A, by editing the Golem application manifest file (a YAML file read by golem-cli).
  2. The public interface of component B is described by WIT (WASM Interface Types).
  3. golem-cli, using the wasm-rpc project, can generate a transformed WIT file that is the stateful version of the original WIT exported by component B. This stateful aspect allows developers to target specific workers, which is necessary for worker-to-worker communication--but which is not generally necessary for gRPC or OpenAPI, because these protocols are stateless.
  4. wasm-rpc generates and compiles stubs for RPC, which implement the transformed (stateful) WIT, and require Golem host functions for performing the actual RPC. Note that this step is in the process of being simplified, and shortly, the stubs will be dynamically added on the server side, in the worker-executor, using a custom linker, and this model of dynamic stub generation must be used for both gRPC and OpenAPI support.
  5. The user writes component A, invoking the transformed WIT, and linking against the stubs, to eliminate the requirement on the transformed WIT. This step is disappearing as worker-to-worker transitions to the server-side, dynamically generated stubs; it will not be necessary for gRPC or OpenAPI support.
  6. The worker-executor provides the Golem host functions necessary for RPC.

The process for supporting gRPC and OpenAPI will proceed in a similar fashion:

  1. The user is writing a component A, which depends on gRPC defined by Protobuf B and OpenAPI API defined by schema C. The user adds a reference to B as one dependency of type grpc, and a reference to C as another dependency of type openapi, by editing the Golem application manifest file.
  2. golem-cli, using an extended and enhanced wasm-rpc , generates a WIT file for B, corresponding to an idiomatic encoding of the gRPC services into WIT; and another WIT file for C, corresponding to an idiomatic encoding of the OpenAPI API into WIT.
  3. The user writes component A, invoking the generated WIT for B and C as necessary to access the functionality of API B and API C.
  4. The worker-executor, when executing instances of component A, dynamically stubs the WIT interfaces and links them to the instances, allowing instances to interact with the gRPC service and the OpenAPI API.

In supporting both gRPC and OpenAPI, there are several major challenges:

  1. For an arbitrary gRPC or OpenAPI spec, programmatically generating an equivalent WIT that is "WIT idiomatic", and which does not look or feel like it was programmatically generated. This is much more challenging for OpenAPI schemas than it is for Protobuf.
  2. Dynamically adding stubs in the worker-executor corresponding to the generated WIT. These stubs will be implemented in Rust and programmatically execute gRPC and OpenAPI invocations using appropriate metadata.
  3. Ensuring durable execution of the API calls at the level of the individual calls, using the same mechanisms that the worker-executor is already using to make WASI calls durable (essentially, branching off record/play back mode and reading from / writing to the oplog).

Beyond these major challenges, there are several other essential features of the implementation to consider:

  • Capturing the protobuf and OpenAPI schemas during component creation and update. These will be captured by golem-cli, the command-line interface for Golem.
  • Modifying the Golem application manifest file (YAML) parser and schema to accept new types of dependencies, including the protobuf files and OpenAPI files. Currently, the only type of dependency supported by the parser and schema is wasm-rpc, for describing worker-to-worker communication.
  • Modifying the component creation and update REST APIs to accept information on the new types of dependencies.
  • Storing the protobufs and OpenAPI files during component creation and update; or more precisely, storing a structured representation that has enough information for the worker-executor to generate and add the dynamic stubs.
  • Accessing the protobuf and OpenAPI structured information from inside the worker-executor, so when a worker is launched, they have what they need to dynamically add the gRPC / HTTP stubs.

Input Formats

  • Protocol Buffers v3 (proto3) with gRPC service definitions
  • OpenAPI 3.0.x YAML/JSON specifications

Common Requirements

Package Translation

  • Must generate WIT package name and version
  • For gRPC: Use proto package name
  • For OpenAPI: Use sanitized info.title
  • Version must come from:
    • gRPC: Configuration input
    • OpenAPI: info.version

Type Mappings

Protobuf and OpenAPI types must be mapped into their most precise WIT types. A sketch of a few possible mappings for Protobuf is shown below:

oneof         -> variant
message       -> record
string        -> string 
int32         -> s32 
int64         -> s64 
uint32        -> u32 
uint64        -> u64 
float         -> float32 
double        -> float64 
bool/boolean  -> bool 
repeated T    -> list<T> 
optional T    -> option<T>

Note that OpenAPI schemas may embed JSON schemas, which can contain patterns. To the extent possible and reasonable, patterns should be converted into validations that occur in the stubs, to ensure that where possible, local and highly descriptive errors are produced--rather than relying on a remote service to produce a useful error in response to some kind of schema validation failure.

Error Handling

The following sketch of an error type could inform design of a generalized error type. The actual error type used in the implementation must be at least as capable and as expressive as the provided one.

variant error {  
  unauthorized { message: string }, 
  not-found { resource: string, id: string }, 
  validation-error { fields: list<string> }, 
  rate-limited { retry-after: u32 }, 
  server-error { message: string }, 
}

Authentication Types

The following sketches of authentication types could inform design of generalized authentication types. The actual authentication types used in the implementation must be at least as capable and as expressive as these sketches:

record bearer-auth {
  token: string, scheme: string, 
}
record basic-auth {
  username: string, password: string, 
}
record api-key-auth {
  key: string, 
}

gRPC-Specific Requirements

Message Translation

  • Each message type must become a WIT record
  • Names must be converted to kebab-case
  • Must preserve all fields and their relationships
  • Nested messages must become separate records

Service Translation

  • Each service must become a WIT interface
  • Each RPC method must become a WIT function
  • Must return result<response-type, error> for WIT-idiomatic error handling

Example:

message  GetUserRequest  {   string user_id =  1; }

Must become:

record get-user-request {  user-id: string, }

The numerical order of fields in the protobuf MUST correspond to the linear order of fields in WIT.

OpenAPI-Specific Requirements

Schema Translation

  • Each components.schema must become a WIT record
  • Required fields must be non-optional
  • Must preserve all relationships between types

Inline Type Translation

  • All anonymous/inline schema definitions must be converted to named WIT types
  • Names could be synthesized systematically using the following rules (or similar):
    1. For request bodies: {path}-{method}-request-body
    2. For response bodies: {path}-{method}-response-body
    3. For array items: {parent-type}-item
    4. For nested objects: {parent-type}-{field-name}
    5. For parameters: {path}-{method}-params

Example:

paths:
  /users: 
    post: 
      requestBody: 
        content: 
          application/json: 
            schema: 
              type: object
              properties: 
                name: 
                  type: string 
                addresses: 
                  type: array 
                items: 
                  type: object
                  properties: 
                    street: string 
                    city: string

Could generate something like:

record users-post-request-body {
  name: string,
  addresses: list<users-post-request-body-addresses-item>, 
}
record users-post-request-body-addresses-item {
  street: string, 
  city: string,
}

The exact naming scheme may vary, but must be:

  • Deterministic
  • Generate valid WIT identifiers
  • Maintain clear relationship to source structure
  • Avoid name collisions
  • Create traceable mappings for worker-executor's use

Path Translation

  • Must support paths to any number of levels deep
  • Each base resource path must generate interface(s)
  • Must group related operations logically
  • Must handle path/query parameters correctly

Header Handling

Must specially handle these headers:

Authorization     -> auth field in request 
ETag              -> version field in response 
If-Match          -> expected-version in request 
Last-Modified     -> last-updated in response

All other headers must be mechanically translated to kebab-case option fields.

Resource Function Generation Rules

The following sketches of resource interfaces could inform design of generalized resource handling for REST APIs. The actual interfaces used in the implementation must be at least as capable and as expressive as these sketches:

interface resource-collection {
  list: func(params: list-params) -> result<list-response, error>; 
  create: func(params: create-params) -> result<item, error>; 
}
interface resource-item {
  get: func(id: string) -> result<item, error>; 
  update: func(id: string, params: update-params) -> result<item, error>; 
  delete: func(id: string) -> result<unit, error>; 
}

Naming Conventions

Conflict Resolution

Potential algorithm for resolving name conflicts:

  1. Appending type suffix (_record, _params, _result, etc.)
  2. If still conflicting, error out requiring manual resolution

Generated Names

  • Must be valid WIT identifiers
  • Must use kebab-case
  • Should not exceed 64 characters
  • Must prefix reserved words with %

Dynamic Stub Requirements

Dynamic stubs must be added to the worker-executor for all of the gRPC and OpenAPI dependencies. These stubs must, of course, match the type signatures of the generated WITs.

Durability must be ensured using the same mechanism that is used to provide durability for WASI inside the worker-executor (namely, direct interaction with the mode, whether record or playback, and the worker oplog).

Testing Requirements

Automated tests are required at the following levels:

  • Unit. Unit testing should ensure correctness of each part of the system developed.
  • Integration. Integration should ensure that when the components are composed (e.g. golem-cli with wasm-rpc), they function together as specified.
  • System. End-to-end system tests should verify the correctness of the entire system, all the way from golem-cli to the worker-executor.

In particular, there must exist automated tests against REAL gRPC and OpenAPI APIs, which verify both the transformed WIT that is generated, and also which actually invoke the APIs from within a test worker, to verify that the dynamically added stubs in the worker-executor are working correctly. Durability tests must be added to verify durability of any gRPC or HTTP interacts that happen from within the generated stubs.

The solution will be tested against the following resources:

OpenAPI:

gRPC:

We have several additional OpenAPI and gRPC resources we will be "surprise testing" against to ensure sufficient scope and quality.

Acceptance

We will accept the first solution which, in our sole opinion and estimation:

  1. Demonstrates deep understanding of the purpose of this feature, even, if necessary, extending or changing parts of the specification in order to achieve the end goal in a way which is compatible with how worker-to-worker communication currently works, which is highly friendly to developers, and which does not sacrifice type safety, performance, or other characteristics important for production adoption of the feature.
  2. Demonstrates high-quality, best practices in the Rust implementation and associated tests. Code should be modular, well-organized, strongly typed, free of duplication, and demonstrate the best of what Rust has to offer; with consistent conventions as utilized by the best parts of the existing code base.
  3. Demonstrates a systematic and well-thought out approach to testing, having ample unit tests, integration tests, and system tests, which collectively verify end-to-end usage scenarios, including interacting with real-world gRPC and OpenAPI APIs from within Golem workers, in a type-safe way.
  4. Has been updated with latest changes coming from our head branch.
  5. Is repeatably passing the CI build.
  6. Is sufficiently well-documented to enable other developers to build off the solution, and end-developers to use the solution in their own Golem applications.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions