-
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
Add support for service client connection reuse. #1589
Conversation
911bd42
to
10a5ac4
Compare
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.
This seems like a great idea, and the PR looks very good already! I think it needs some care to make sure binary compatibility is ensured, though - particularly between the generated code and the runtime library.
} | ||
|
||
@@AkkaGrpcGenerated | ||
protected final static class Default@{service.name}Client extends @{service.name}Client { | ||
|
||
private final ClientState clientState; |
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 don't think we can remove the ClientState
type entirely: to achieve the binary compatibly promise we do in https://doc.akka.io/docs/akka-grpc/current/binary-compatibility.html , code generated with previous Akka gRPC versions should still work when a newer version of the runtime library is on the classpath. So I think we need to keep at least a deprecated 'wrapper' ClientState
around, that will make sure 'older' generated code keeps working.
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.
Thank you for the feedback!
As you suggested, I recreated ClientState as a wrapper around a GrpcChannel. I think it properly supports the constructor and methods that are used by older generated clients.
6966c54
to
11e85a1
Compare
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 the remaining MiMa errors about ClientState can be safely ignored, since they're about methods that were not used by the generated code.
I'm less certain about the change to the DefaultServerReflectionClient constructor. The documentation would steer users toward using ServerReflectionClient.apply
over new DefaultServerReflectionClient
, but the constructor was public. Should we add a deprecated constructor that takes GrpcClientSettings to the generated default client?
*/ | ||
@deprecated("Kept for binary compatibility between generated code and runtime", "akka-grpc 2.1.4") |
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.
Is a deprecation annotation appropriate here?
Should version number be 2.1.4 (next patch release) or 2.2.0 (next minor release)?
Should version number include library name? I see both styles being used elsewhere in the project.
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.
Is a deprecation annotation appropriate here?
Yes, I think so 👍
Should version number be 2.1.4 (next patch release) or 2.2.0 (next minor release)?
I think the next patch release would be best
Should version number include library name? I see both styles being used elsewhere in the project.
I see https://www.scala-lang.org/api/current/scala/deprecated.html promotes adding the library name, but TBH I don't think it's necessary - I'm OK with either
After thinking about MiMa warnings a bit more, I added access modifiers to GrpcChannel and ChannelUtils to limit unintended exposure of constructors and methods. I think we could make the entire ChannelUtils object private to the akka package, but I wasn't sure if that is within scope for this PR. I also noticed that ChannelUtils.closeCS was no longer used (and hasn't been since 31bb602), so I removed it. |
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.
Shaping up nicely! Can you add the mima exclusions for changed internal API's that you've verified are not used from old generated code?
import akka.grpc.internal.{ ChannelUtils, InternalChannel } | ||
import akka.grpc.scaladsl.Grpc | ||
|
||
final class GrpcChannel private (val settings: GrpcClientSettings, @InternalApi val internalChannel: InternalChannel)( |
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.
settings
is used by the generated code, so could you mark it @InternalStableApi
?
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.
Good catch! When I wrote that, I was thinking that it could be convenient for library users if they could access the settings associated with a channel. Looking through the code, however, it looks like settings are generally only consumed and not exposed.
As you suggested, I added the InternalStableApi annotation to settings, and updated internalChannel to use it as well (instead of InternalApi).
@@ -89,16 +91,18 @@ final class Default@{service.name}Client(settings: GrpcClientSettings)(implicit | |||
@{method.nameSafe}().invoke(in) | |||
} | |||
|
|||
override def close(): scala.concurrent.Future[akka.Done] = clientState.close() | |||
override def closed: scala.concurrent.Future[akka.Done] = clientState.closed() | |||
override def close(): scala.concurrent.Future[akka.Done] = channel.close() |
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.
Hmm, this seems rather tricky - so now closing one client will close the channel, potentially also making other clients unusable. Is that what we want? If so, perhaps we should document it prominently?
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 agree. This is tricky and potentially surprising. Unfortunately, I think the trickiness is an unavoidable consequence of simultaneously supporting both client-owned and externally-owned channels within the same generated service client interface.
At least initially, I think reusing channels will be a power-user feature, like the client PowerApi. As such, I hope that callers who opt-in to shared channels will also understand the channel itself should be closed, rather than the clients that share it, but you're right that this should be more clearly called out in the documentation. I can add a warning to the API docs for the close methods, as well as a section about reusing channels in client/details.md.
We could also add a flag to track whether the service client created the channel, and throw an exception if close is called with an externally-owned channel. This is still a potentially surprising run-time error, but the exception would make the error immediately obvious. Do you think this would be worthwhile?
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 can add a warning to the API docs for the close methods, as well as a section about reusing channels in client/details.md.
Sounds great!
We could also add a flag to track whether the service client created the channel, and throw an exception if close is called with an externally-owned channel. This is still a potentially surprising run-time error, but the exception would make the error immediately obvious. Do you think this would be worthwhile?
I think that would be valuable, but for now just having clear docs should be sufficient as well
0e1b6f5
to
404b4a2
Compare
def apply(channel: GrpcChannel)(implicit sys: ClassicActorSystemProvider): @{service.name}Client = | ||
new Default@{service.name}Client(channel, isChannelOwned = false) | ||
|
||
private class Default@{service.name}Client(channel: GrpcChannel, isChannelOwned: Boolean)(implicit sys: ClassicActorSystemProvider) extends @{service.name}Client { |
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.
Making the default service client private to the service object feels like an improvement to me. The default client was already marked final to prevent subclassing, and user should be using the apply() method on the service client object rather than creating default clients directly. Making the class private to the service client just enforces these rules more strongly, and brings the generated scala client into alignment with the generated java client.
That said, I'm still calibrating my application of the Boy Scout Rule. Let me know if this goes too far.
I'm sorry to leave this hanging, I hope to return to it later. In the mean time, it seems like the CI jobs weren't correctly triggered - could you update/push a commit to get those going again? |
a3e4441
to
7986ed9
Compare
I added some documentation to API docs and client details page. Ideally, I'd also like to add a code snippet in the details that shows how to create a GrpcChannel and use it to create multiple generated clients, but I wasn't sure about the best way to accomplish this. I initially created a file similar to runtime/src/test/scala/docs/akka/grpc/client/GrpcClientSettingsCompileOnly, but I ran into a snag when I tried to define some example protobuf services for it. I created a file to define the service (runtime/src/test/protobuf/example.proto), but the ReflectiveCodeGen plug-in didn't seem to generate source for it. I also considered annotating the relevant section of the GrpcChannelSpec test and referencing that from the documentation. This is less appealing, since the test uses two instances of the same service, which might be confusing to someone reading the documentation, and I'd rather not muddy the test by defining and implementing a second service. Any suggestions here? |
7b44ee7
to
e8a15e0
Compare
Refactored ClientState into GrpcChannel. Modified generated service clients to use GrpcChannel instead of GrpcClientSettings, with an apply() shim to create a GrpcChannel from GrpcClientSettings. Added GrpChannelSpec to confirm that connection is being reused.
Recreated ClientState as wrapper around GrpcChannel to preserve compatibility with generated clients from previous versions.
Added final modifier to GrpcChannel Made GrpcChannel constructor private to enforce usage of apply. Made ChannelUtils.create private to akka package.
Moved default service client into service object and made it private. Removed companion object for default service client.
e8a15e0
to
d4617f2
Compare
Looks great!
I agree this would be nice, and that it would be best to show the feature with two different services. Not sure why I think the feature is already very attractive, though, so I intend to merge this PR as-is - we can add a code example in a later PR. Thanks a lot! |
Refactored ClientState into GrpcChannel.
Modified generated service clients to use GrpcChannel instead of GrpcClientSettings, with an apply() shim to create a GrpcChannel from GrpcClientSettings.
Added GrpChannelSpec to confirm that connection is being reused.
References #1588