Add support for Twirp as a Transport#656
Conversation
|
Some quick initial feedback:
|
|
Excited about this 🎉 |
|
yes this will be a great addition to the supported transports in Go-kit :) |
transport/twirp/client.go
Outdated
| ) | ||
|
|
||
| // rpcFn is a simple type to represent an RPC method on the Twirp client. | ||
| type rpcFn func(context.Context, interface{}) (interface{}, error) |
There was a problem hiding this comment.
This type matches the endpoint.Endpoint function. Maybe we should just use that?
It's odd to have the NewClient function require a private type as a param.
| type ClientResponseFunc func(context.Context) (context.Context, error) | ||
|
|
||
| // SetResponseHeader returns a ServerResponseFunc that sets the given header. | ||
| func SetResponseHeader(key, val string) ServerResponseFunc { |
There was a problem hiding this comment.
@basvanbeek what am I missing here in terms of your comment about http.Header?
There was a problem hiding this comment.
So SetResponseHeader is to report back headers to the client. What is missing is the ability to add headers to a client request and for a server to pick them up. Easiest is to think of
http.Header to be similar to gRPC's metadata.MD.
Client side you'd create a fresh empty http.Header which you'd pass to the server before funcs which can then add custom headers to this http.Header map potentially based on information found in client context.
Server side you'd provide the received http headers from the client with a func that provides the server context and returns a potentially updated server context. This allows server before funcs to add stuff to context based on received metadata found in those client headers.
|
@basvanbeek I didn't see any finalizers in the grpc client/server, but tried to mirror the http ones without actually getting into the http requests/responses since twirp should in theory abstract that away a bit. |
|
@austinylin commenting based on the launcher PR that uses the client The first argument is a bit awkward to work with, and a little inconsistent from how |
|
@groob I'm not a Twirp expert by any stretch, but best I can tell Twirp doesn't have something like There may be a smarter way to do this with reflection, but that is at the end of my go skills :). Open to ideas if anyone has them, agree the current situation is sub-par. |
|
@austinylin I think we could implement invoke as a helper using reflect. I'll try to propose what that looks like in a timely fashion. |
|
@austinylin yes gRPC finalizers are not there yet. There is an upcoming PR for this which currently is being worked on. |
Twirp (https://github.com/twitchtv/twirp) is an RPC system that works over HTTP/1.1 using either JSON or Protobuf. It's leverages protobuf for generating servers and clients.
|
@basvanbeek I took a deeper look at the response, request functions per your earlier comment about making it simpler to interact with headers. I was able to update the request functions to make that cleaner, but I wasn't able to do the same for the response functions. Two reasons:
You can see the Twirp side of the code here: https://github.com/twitchtv/twirp/blob/master/context.go. Let me know if you think this is acceptable (with callouts in the docs). |
|
Hi @austinylin I'm really interested in you work on this Twirp transport, but I'm not sure how I feel about the reflection based Client from your last commit. A few days ago I wanted to try using your work on one of my projects but I did not figure how to correctly use the transport and came up with something of my own. You can find an example here: https://gist.github.com/pauldub/0d9c1b26e1d182c816ba19b126a914ef I've found that defining the endpoints was really simple and did integrate well with go-kit, as for the server side it is the raw Twirp implementation. I hope this will be useful to you, and am looking forward to this transport implementation! |
|
Yeah, I haven't dug in to it yet, but I'm generally not on board with package reflect as used here. It is always the case that a given transport/whatever.{Client.Server} wraps explicitly one RPC handler. |
|
|
||
| // EncodeRequestFunc encodes the passed request object into the Twirp request | ||
| // object. It's designed to be used in Twirp clients, for client-side endpoints. | ||
| // One straightforward EncodeRequestFunc could something that encodes the object |
There was a problem hiding this comment.
s/could something/could be something
|
I tried looking at the twirp client. Everything twirp does comes down to calling http.Client.Do(req) with a custom URL path like |
|
I tried making the changes in my fork here groob@4f8e83e I'm not 100% certain I got the reflect strategy correct, although it's just a copy/paste from The way you'd construct the client is the following.
The NewClient method will use the helpers in |
|
@peterbourgon, @groob, @pauldub - the reflection commit is basically an attempt to mirror the interface that transport/gRPC provides but without the benefit of the helpers gRPC procides since Twirp doesn’t have then. In my opinion the biggest con of this approach is it can cause a runtime error, vs a compile time error for the user if an invalid method is passed (since it’s a string param). I believe that is the case today for transport/gRPC. My original approach was just to pass a function to the client constructor which wraps the underlying function in the generated code. Happy to move forward with either approach depending on what folks think is best. (Edit:) I think we should stay away from breaking the abstractions that Twirp provides simply from a maintainability POV. Separately, @basvanbeek could you take a look at the updated response/request funcs? I ran into one challenge with the response funcs based on what Twirp supports. |
|
I discussed this at length with @groob in the #twirp gopher slack. I'd like to throw my own opinion in here, even though I don't know much about kit. Servers seem to obviously work straightforwardly; all the work here seems to be in making clients nice. As I understand it, clients are tools to get @groob mentioned that this doesn't leave room for the encoder-decoder pattern used in other transports, though; my understanding is that these encoders and decoders are used to let users define their own types instead of using generated structs throughout the application code. I think this is easy enough for users to do too, though, in a user-written Endpoint constructor. For example: // svc is the service interface generated by twirp.
func MakeHatEndpoint(svc example.Haberdasher) endpoint.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) {
// Assert that the request object is the right type.
domainReq, ok := req.(*app.Size)
if !ok {
return nil, errors.Errorf("unexpected request type %T", req)
}
// Convert the request object to a proto struct, so Twirp can send it.
protoReq := &example.Size{
Inches: domainReq.Feet / 12,
}
// Actually do the request.
protoResp, err := svc.MakeHat(ctx, protoReq)
if err != nil {
return nil, err
}
// Convert the response to the type that the application wants.
domainResp := &app.Hat{
Color: protoResp.Color,
}
return domainResp, nil
}
}This seems clear, clean, and doesn't need reflection, which is good. It doesn't include any forked code out of Twirp - the actual serialization is handled by the underlying The downside is that users need to write a little code. However, I think they'll be doing this regardless: they certainly need to write the encoder/decoder themselves. |
| reqHeader http.Header | ||
| ok bool | ||
| ) | ||
| reqHeader, ok = twirp.HTTPRequestHeaders(ctx) |
There was a problem hiding this comment.
@austinylin when using this code. It seemed that the context.WithValue(ctx, contextkeys.RequestHeaderKey, copied) which suppose to set the ctx value with 5 and encode with the headers and send as a request to the server. However, on server side, this value 5 is never there. What I'm I missing on the client side?
|
What's the state here? Do we have consensus on a path forward for client? |
|
Think your feedback would be helpful Peter in terms of what you think is
the right approach here.
…On Fri, Feb 23, 2018 at 11:44 Peter Bourgon ***@***.***> wrote:
What's the state here? Do we have consensus on a path forward for client?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#656 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAgWhAexolQ14GYW_5qbiPWtIZDHBoqTks5tXxUggaJpZM4Rvp0D>
.
|
|
@austinylin @peterbourgon the way I see it after discussing in gophers slack with Peter and @spenczar (slack link) there are two alternatives:
I took the approach of creating a shared lib in my fork. This implementation yields a client that is more consistent with the other transport packages since it only wraps a single RPC. Another thing to consider is that Twirp provides support for both proto and JSON encoding, all contained in the client. The implementation in this PR only deals with protobuf. A decision would have to be made about wether the kit implementation should be broken into two transport packages or encapsulated into a single one. As Austin stated above, this PR needs some guidance from @peterbourgon, @basvanbeek and other maintainers about which approach to take. |
|
I think we can agree that the reflect-based option is probably out. But after reviewing the remaining approaches I feel unqualified to make a judgment. I like the feel of @groob's option more, but it seems like it carries more risk of future breakage, since Twirp isn't exposing the relevant hooks. @spenczar has your opinion shifted since our chat in Slack awhile ago? Is Twirp open to integrating with systems like Go kit, that want to hook into its "transport layer" exclusive of the rest of the project? With that said I don't really have anything against @austinylin's approach, it just feels different. My superficial understanding is that it carries less risk related to Twirp changes, would others agree? @basvanbeek do you have any final opinion here? ("No" is a perfectly acceptable answer.) |
|
I'm with you @peterbourgon in liking Groob's option more and I'm personally not overly worried about future breaking change. Ultimately I have no strong enough favorite to feel like I must be tipping the balance. I would like to see if we can have some votes from actual prospective consumers of this transport as these are ultimately more important then I am as a non user of this particular transport. |
|
@peterbourgon I'd like to move this forward and having had this marinate a bit more I'm more strongly in favor of the option presented by @groob The Twirp specification is very simple and clear. Changes in the future should be fairly easy to fix and even having a custom implementation of Twirp removing dependency on Twirp library and protoc plugin is an option. |
|
I'm in favor of this. What needs to happen in this PR? Should it be a separate PR at this point? |
|
@groob @basvanbeek Absolutely no-pressure ping on this one |
| args[0] = reflect.ValueOf(ctx) | ||
| args[1] = reflect.ValueOf(req) | ||
|
|
||
| retVals := make([]reflect.Value, 2) |
There was a problem hiding this comment.
This slice is never used
|
|
||
| client := reflect.ValueOf(&c.client) | ||
| method := client.MethodByName(c.method) | ||
| if !method.IsValid() { |
There was a problem hiding this comment.
Since client and method are set in NewClient we could move this check there and change it to a panic or (IMO even better) just take a func / method value in NewClient instead of an interface + a method.
This way we can also avoid a few allocations here.
|
There is a suggestion to use Just in case you will be using |
|
I see this PR has been open for a while - I'm wondering if there are still plans to pull Twirp into Go Kit? It'd be awesome if there were! |
|
Closing due to age. |
Twirp (https://github.com/twitchtv/twirp) is an RPC system that works
over HTTP/1.1 using either JSON or Protobuf. It's leverages protobuf for
generating servers and clients.
I still need to do a few things, but wanted to get initial feedback.
Todo