-
Notifications
You must be signed in to change notification settings - Fork 124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Client power user API #201
Client power user API #201
Conversation
No usage samples yet, but the "normal" calls are now expressed through the new RequestBuilder version of the method, and interop-tests passing. |
|
||
def addMetadata(key: String, value: String): RequestBuilder[Req, Res] | ||
def withDeadline(deadline: FiniteDuration): RequestBuilder[Req, Res] | ||
def execute(req: Req): Res |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about invoke
instead of execute
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No strong opinion either way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Me neither. ;-) Lagom uses invoke
hence my slight preference.
Btw, that API style means that we always have a lifted API representation instead of a switch giving access to the lifted/advanced one.
I thought we wanted to provide a non-lift, straight, variation for the most common cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Not sure that it is always available rather than hidden behind a switch is a problem though. The old non-lifted API is still there (and should be the default)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aligning with Lagom seems like sensible motivation for invoke
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, now I see it. Because in gRPC there is always one param, we can overload the method with a paramless variation that gives access to the lifted version. Neat! Much better than a switch.
Right! Moving the parameter to the end of the chain makes a lot of sense: this way a library user can even reuse the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice so far!!
} | ||
|
||
/** | ||
* For access to method metadata use the parameter less version of ${method.name} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
('parameterless')
} | ||
|
||
def withDeadline(deadline: FiniteDuration): RequestBuilderImpl[Req, Res, Ret] = { | ||
copy(options = options.withDeadline(Deadline.after(deadline.length, deadline.unit))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, we don't support this at the server side yet (#188) but since this is 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, just picked this randomly to have some functionality to provide through the request builder.
delegatedExecute: (Req, CallOptions) => Ret) extends RequestBuilder[Req, Ret]() { | ||
|
||
def addMetadata(key: String, value: String): RequestBuilderImpl[Req, Res, Ret] = { | ||
// FIXME support values other than string values? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do 'options' correspond to 'headers' or is there more indirection there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is a bit more indirection, there is also some marshalling infra in the grpc library around this. Not sure if we care about that though, maybe strings are good enough for now, and also avoids tying us down to something client specific.
It's annotated as an experimental API, but so is most of these CallOptions.
def @{method.name}(): RequestBuilder[@method.parameterType, @method.returnType] = { | ||
val descriptor = @{method.name}Descriptor | ||
@if(method.methodType == akka.grpc.gen.Unary) { | ||
RequestBuilderImpl.unary(descriptor, channel, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to organize the template so that the indentation of the generated code makes sense? No real preference here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I thought I did follow that, since it is how it was, but maybe having the templates readable is more important for maintainability than not having too many indentation levels in the generated sources?
One thing I couldn't figure out was a reasonable way to deal with response metadata, for single response calls it may be relatively straight forward API wise ( Any thoughts around that? |
I had proposed a wrapper type for that I believe; I would avoid forcing people to return |
Thinking about this some more, maybe the primary use case will be to set metadata on the request, not deal with response metadata, so perhaps there should be the current "simple" |
I think a better way of doing that is to have a method that changes the return type. That's also how it was done in Lagom. For instance:
This requires two impl of |
I don't see why a stateful/extra type is better than an additional |
Actually, the Lagom implementation has two methods: def handleResponseHeader[T](handler: (ResponseHeader, Response) => T): ServiceCall[Request, T]
def withResponseHeader: ServiceCall[Request, (ResponseHeader, Response)] =
handleResponseHeader((_, _)) The The Moreover, Lagom's Correction: I previously said we needed two |
Yeah, I think it is pretty useful to be able to write decorators that can manipulate either/both the request and response metadata and payloads. I don't see a way with this API to be able to write a decorating function that can intercept the response without using internal APIs. |
Hmm, I still don't get it, why would a special type with transformations be better than just having a |
@johanandren, I’m not sure of all the motivations of it in Lagom, but from what I can infer from the API, it allows you to change the return type using some information available on the metadata. What I think is most important is that the client should only receive the headers if required. The Lagom API allows you to send headers, but to not necessary receive the response headers. This kind of flexibility is important because we let the user augment the API surface when needed. The opposite is also true. In Lagom is possible to make a call and expose the response headers without having to send request headers. I think that the case in Lagom is simply because one is built in terms of the other. First you provide a method to expose the response headers and modify the return type. def handleResponseHeader[T](handler: (ResponseHeader, Response) => T): ServiceCall[Request, T] then you build another one using def withResponseHeader: ServiceCall[Request, (ResponseHeader, Response)] =
handleResponseHeader((_, _)) Now you have the choice to use one or another according to your needs / preferences. I think that's just that. |
I think all those cases you describe should be covered by having two methods on the builder: the one that currently exists and then one that returns a |
Sure, but does that mean that in case of streaming we will get a This sounds good and it's worth experiment. I has some implications though. If the general API returns It also means that there are effectively two calls, one to get the And then we may have a another problem, for the moment mostly theoretical. This two calls may create a situation where the metadata differs between them. |
For completeness and clarity. The implications I mentioned above (two calls) also holds for the |
Just a crazy thought. The only way to associate headers to a stream and make sure that they relate (make part of the same call) is So, maybe... sealed trait Headers
case object EmptyHeaders extends Headers
case class NonEmptyHeaders extends Headers Then is up to the implementers to decide when to send the headers. On the first element, on each element, etc. Didn't thought thoroughly on the implications of this idea. Maybe stupid, maybe crazy, maybe good. I let this go through your brains first. :-) |
I'm not entirely convinced we want to keep the semantics of Source and be able to re-run it (make the call again) whenever we want: I don't think we do that for
|
You are totally right on that. Not a good idea. |
Internally the headers will arrive when the flow is materialized, and then there is a |
That sounds really good. Need to check later how this is being achieved. |
If a Source[T, (Future[Headers], Future[Trailers])] |
Yeah, likely. But I think a specific type capturing it would be better than a tuple. The alternative would be to side effect from I think the matVal feels most natural |
3feee45
to
40e678e
Compare
I find counterintuitive that materializing the IIRC Lagom's client produces a Maybe renaming akka-grpc's |
Both of the source variations could be materialized many times. If that is an issue we can protect against that and fail. I don't have a gut feeling about it being surprising or not yet, I'll see when the actual user API is in place (which is what I am currently working on). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking very good :)
No strong opinion on the source running semantics yet... best to play around a bit and gather feedback on that one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
"127.0.0.1", | ||
8080, | ||
overrideAuthority = Some("foo.test.google.fr"), | ||
None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps name this parameter as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ups, keep missing out on the plugin-testers as they are not part of the aggregated build
@@ -56,7 +66,7 @@ object GreeterClient { | |||
val responseStream = client.itKeepsReplying(HelloRequest("Alice")) | |||
val done: Future[Done] = | |||
responseStream.runForeach(reply => println(s"got streaming reply: ${reply.message}")) | |||
Await.ready(done, 1.minute) | |||
Await.ready(done, 1.minute) // just to keep sample simple - dont do Await.ready in actual code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we should avoid it here as well then - but let's keep that for another PR
|
||
import scala.concurrent.Future | ||
|
||
// FIXME should we provide our own immutable/thread safe response metadata abstraction? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed the upstream Metadata
is not too nice, might be good to at least wrap it in some immutable interface?.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
|
||
def addMetadata(key: String, value: String): T = { | ||
// FIXME Key is instance equal to allow for replacing of the same key but still also allowing multiple | ||
// values with the same name-key, not sure if it is important we support that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep an eye out if we ever see that (multiple role
s?).
If so another way to do it might be to have this method overwrite keys, and introduce a def addMetadata(key: String, value: Seq[String])
for multi-valued metadata
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turned out that what I thought was the same thing, the CallOptions.options and the metadata headers, was in fact not at all the same thing, so this is gone now.
def options: CallOptions | ||
def updated(options: CallOptions): T | ||
|
||
def addMetadata(key: String, value: String): T = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since later the GrpcResponseMetadata
contains headers, perhaps addHeader
would make more sense here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeap, I renamed when changing it into metadata. I realize I renamed it wrong though, changed it to withHeader
as I thought it replaces, but I realize now that it should be additive and not replace, so add
would be better.
* | ||
* INTERNAL API | ||
*/ | ||
// needs to be a separate class because of CompletionStage error handling not bubling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bubbling ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Less doubling in my bubbling!
/** | ||
* Invoke the gRPC method with the additional metadata added and provide access to response metadata | ||
*/ | ||
def invokeWithMetadata(request: Req): Source[Res, Future[GrpcResponseMetadata]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encoding the metadata in the materialized value looks good.
It seems a 'clever' solution which sometimes backfires, but in this case it looks like it works out elegantly.
I agree since the metadata is in the materialized value the need for a separate invokeWithMetadata
kind of disappears... unless perhaps there's opportunity for more optimization in invoke
later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that may be a good argument for keeping the two separate methods
* @param streamingResponse Do we expect a stream of responses or does more than 1 response mean a faulty server? | ||
*/ | ||
@InternalApi | ||
private final class AkkaNettyGrpcClientGraphStage[I, O]( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool
…sts. Bonus feature - does backpressure - akka#83
…ders enabled and passing
ef64ad3
to
1f0c415
Compare
Should be ready for merge now! :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice with the integration tests, hadn't seen those in detail yet and indeed look reasonable
Ah, crap, I missed that it maybe failed validation, github refuses to show me what failed right now though. |
No worries, I think I fixed it on master |
Fixes #191
Went for
instead of