Skip to content

gRPC extension traits #1072

@Mirko-von-Leipzig

Description

@Mirko-von-Leipzig

We sometimes extend our gRPC client functionality to include things like automatically retrying on connection failures. It would be convenient if we could centralize these implementations as an extension method which is callable on any gRPC client function call.

This isn't trivially possible right now because gRPC methods don't all implement a specific trait or type so one can't target them. Part one of this issue would therefore involve prototyping a few approaches and deciding whether its worth it or not.

I'll list a couple of ideas I've had bouncing around here, that doesn't necessarily mean they're good ones.

Standardize the return result

If we standardize gRPC methods to return GrpcResult<T> then we could implement the extension trait on that. At minimum we need the error type to return whether it was a connection issue, or not, so we know when to retry.

This would be similar to tokio's timeout extension method, which wraps the future in a timeout.

trait GrpcMethod

This would be more involved, but would let us standardize and add nicer telemetry to operations. In general, our gRPC methods all broadly follow the process of deserializing request, do some work, serialize the response.

Using a trait isn't required, but it would let us standardize the error types and process. However, there are still unknowns to me, like how would one inject the actual protobuf client, and which method to call on it. This should be possible, but I haven't fully explored this yet.

⚠️ Reminder that this may still result in bad ergonomics in the end..

enum GrpcDecodingError {..}

enum GrpcError {
    Decoding(GrpcDecodingError),
    ...
}

trait GrpcDecode<Raw, Domain> {
    fn decode(proto: Raw) -> Result<Self, GrpcDecodingError> {...}
}

trait GrpcEncode<Raw, Domain> {
    fn encode(domain: Domain) -> Raw {...}
}

trait GrpcMethod {
    // Raw protobuf inputs and outputs
    type Request;
    type Response;

    // Actual domain inputs and outputs.
    //
    // Must satisfy encoding/decoding from the protobufs.
    type Input: GrpcDecoding<Self::Request, Self::Input>;
    type Output: GrpcEncoding<Self::Response, Self::Output>;
    
    async fn call(&self, client: T, input: Self::Input) -> Result<Self::Output, GrpcError> {
        // We can add a tracing span for each step here, so we know
        // how long encoding, work, and decoding take.
        let request = input.encode();
        let response = client.call(request).await?; // Client bit is unclear.
        Self::Output::decode(response)
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions