-
Notifications
You must be signed in to change notification settings - Fork 177
Description
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:
- The user is writing a component
A, which depends on a componentB. The user adds componentBas a dependency of componentA, by editing the Golem application manifest file (a YAML file read bygolem-cli). - The public interface of component
Bis described by WIT (WASM Interface Types). golem-cli, using thewasm-rpcproject, can generate a transformed WIT file that is the stateful version of the original WIT exported by componentB. 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.wasm-rpcgenerates 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 theworker-executor, using a custom linker, and this model of dynamic stub generation must be used for both gRPC and OpenAPI support.- 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. - The
worker-executorprovides the Golem host functions necessary for RPC.
The process for supporting gRPC and OpenAPI will proceed in a similar fashion:
- The user is writing a component
A, which depends on gRPC defined by ProtobufBand OpenAPI API defined by schemaC. The user adds a reference toBas one dependency of typegrpc, and a reference toCas another dependency of typeopenapi, by editing the Golem application manifest file. golem-cli, using an extended and enhancedwasm-rpc, generates a WIT file forB, corresponding to an idiomatic encoding of the gRPC services into WIT; and another WIT file forC, corresponding to an idiomatic encoding of the OpenAPI API into WIT.- The user writes component
A, invoking the generated WIT forBandCas necessary to access the functionality of APIBand APIC. - The
worker-executor, when executing instances of componentA, 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:
- 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.
- 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.
- Ensuring durable execution of the API calls at the level of the individual calls, using the same mechanisms that the
worker-executoris 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-executorto 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):
- For request bodies:
{path}-{method}-request-body - For response bodies:
{path}-{method}-response-body - For array items:
{parent-type}-item - For nested objects:
{parent-type}-{field-name} - For parameters:
{path}-{method}-params
- For request bodies:
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: stringCould 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 responseAll 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:
- Appending type suffix (_record, _params, _result, etc.)
- 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-cliwithwasm-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-clito theworker-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:
- https://github.com/cloudflare/api-schemas
- https://github.com/openai/openai-openapi
- https://github.com/github/rest-api-description
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:
- 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.
- 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.
- 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.
- Has been updated with latest changes coming from our head branch.
- Is repeatably passing the CI build.
- 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.