Background
When porting a couple of tools from python, it quickly got apparent that the interface of gRPCClient requires a lot more code for simple tasks. Making a synchronous request takes several operations and intermediate variables. I almost immediately found myself using metaprogramming to generate simpler function wrappers to reduce the amount of code on call site. The current API looks designed to maximize control and performance and gives good insight into the underlying mechanics, while the sort of tasks I'm mostly interested in just requires calling a dozen RPC's in sequence with as little code as possible. It would be nice if the API could be extended to enable simpler syntax and IDE suggestions.
Even if I could make my own wrapper, a solid simple API would probably require support at code generation stage, so it would make sense to make it part of this package. I've spent some thought on possible approaches for a while and I hope this ticket can capture the core ideas.
Desirable properties
The following properties would be desirable for simplifying usage:
- One-liner for unary RPCs (one for opening and one optional for closing/cancelling streaming RPCs).
- IDE suggestions for message types of unary requests.
- Port, host and grpc-handle (and defaults for connection options?) should be packaged into a single object (sort of like a grpc channel/stub in python).
- For streaming RPCs, return just one object that handles two-way communication. For familiarity, this object should have overloads for
Base methods such as put! (operates on request channel), take! (operates on response channel), close (close channels and grpc_async_await) and kill (grpc_cancel). For higher granularity, it should of course still be possible to also access the underlying request and channels explicitly.
- Use a julia
module for the service name, e.g. MyService.MyRPC and avoid the _Client suffix. Also allow using MyService to directly import MyRPC as a function.
- Bonus: some traits for the RPC types could be useful for generating more application-specific wrapper code. For example:
response_type(::typeof(MyService.MyRPC)) = TResponse.
Example
Given the requirements above, an implementation supporting the above could look something like:
include("myprotopackage.jl")
using MyProtoPackage # exports MyService
# Common setup for all calls
chan = gRPCChannel(port, host, grpc)
# Unary RPC
resp::TResponse = MyService.MyRPC(chan, req::TRequest) # equivalent of stub.rpc(msg) in OOP. This syntax should work well with IDE suggestions.
# alt syntax
handle::gRPCStreamHandle = MyService.MyRPC(chan, req::TRequest, async = true)
resp = close(handle) # grpc_async_await
# Streaming RPC
handle::gRPCStreamHandle = MyService.MyRPC(chan)
# handle wraps the gRPCRequest and input/output channels
put!(handle, req::TRequest)
resp = take!(handle)
kill(handle) # grpc_cancel
Implementation
I do not think this should be particularly challenging to implement, but there are at least some important decisions to be made. This of course have to live as a secondary API next to the current one, for backwards compatibility. Not sure if the best way is to always generate code for both or to let the user decide. Some of the names and overloads suggested above should deserve some more thought, to ensure that there is no risk for confusion. I'm also aware that there are some ongoing activities on gRPC server implementations, so maybe there could be a reason to align? Performance-wise, I see no reason to worry at this point. Most, if not all, operations are just zero-overhead abstractions around the current API.
Would something like this make sense to implement as a secondary API? In that case, I'd be happy to make a draft and test it in practice.
Background
When porting a couple of tools from python, it quickly got apparent that the interface of gRPCClient requires a lot more code for simple tasks. Making a synchronous request takes several operations and intermediate variables. I almost immediately found myself using metaprogramming to generate simpler function wrappers to reduce the amount of code on call site. The current API looks designed to maximize control and performance and gives good insight into the underlying mechanics, while the sort of tasks I'm mostly interested in just requires calling a dozen RPC's in sequence with as little code as possible. It would be nice if the API could be extended to enable simpler syntax and IDE suggestions.
Even if I could make my own wrapper, a solid simple API would probably require support at code generation stage, so it would make sense to make it part of this package. I've spent some thought on possible approaches for a while and I hope this ticket can capture the core ideas.
Desirable properties
The following properties would be desirable for simplifying usage:
Basemethods such asput!(operates on request channel),take!(operates on response channel),close(close channels and grpc_async_await) andkill(grpc_cancel). For higher granularity, it should of course still be possible to also access the underlying request and channels explicitly.modulefor the service name, e.g.MyService.MyRPCand avoid the_Clientsuffix. Also allowusing MyServiceto directly importMyRPCas a function.response_type(::typeof(MyService.MyRPC)) = TResponse.Example
Given the requirements above, an implementation supporting the above could look something like:
Implementation
I do not think this should be particularly challenging to implement, but there are at least some important decisions to be made. This of course have to live as a secondary API next to the current one, for backwards compatibility. Not sure if the best way is to always generate code for both or to let the user decide. Some of the names and overloads suggested above should deserve some more thought, to ensure that there is no risk for confusion. I'm also aware that there are some ongoing activities on gRPC server implementations, so maybe there could be a reason to align? Performance-wise, I see no reason to worry at this point. Most, if not all, operations are just zero-overhead abstractions around the current API.
Would something like this make sense to implement as a secondary API? In that case, I'd be happy to make a draft and test it in practice.