-
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
Server-side power API #179 #429
Conversation
Thank you for your pull request! After a quick sanity check one of the team will reply with 'OK TO TEST' to kick off our automated validation on Jenkins. This compiles the project, runs the tests, and checks for things like binary compatibility and source code formatting. When two team members have also manually reviewed and (perhaps after asking for some amendments) accepted your contribution, it should be good to be merged. For more details about our contributing process, check out CONTRIBUTING.md - and feel free to ask! |
I think it would be easier to review and understand what is going on if the PRs were split up and there was one with the power-user API and one with the Play-nativeness changes, you think you could do like that instead @ctoomey ? (Would be fine to base one PR branch off the other if you have changes that both need. ) |
OK TO TEST |
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.
Hi @ctoomey, thanks for sharing your progress! This is definitely interesting, much appreciated.
I agree with @johanandren that doing both the 'provide power api' and the 'support play api' in one PR might make this a rather large one, that thus will take more time/effort/cycles to review and get to a point where it can be merged. On the other hand I can see how those 2 are rather intertwined in the current PR.
My main concern with the approach in this PR is that you have basically duplicated the implementation of the Router and Handler with another model. That can be fine, but in akka-grpc we're already also duplicating the generated code across Scala and Java implementations/models - so this would eventually lead to 4 variations of each component.
I wonder if we could achieve the same by 'wrapping' the Akka HTTP-based infrastructure with a Play API 'conversion': this has the advantage that this 'wrapping' logic could eventually be moved over to Play itself and be generally used to serve Akka HTTP routes in Play applications. This might also untangle things a bit, so for example the 'provide power api' feature could indeed be more independent from the 'support play api' feature, and presented as independent PR's.
What do you think?
import @{service.name}.Serializers._ | ||
|
||
def handle(request: RequestHeader, method: String): EssentialAction = method match { | ||
@for(method <- service.methods) { |
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 to sort of duplicate the logic in the @{serviceName}Handler
. Would it be possible to implement this as a 'wrapper around' @{serviceName}Handler
instead?
As discussed in #420, I'm fine with adding some code to akka-grpc to handle this 'for the time being', but for the longer term I'd like this 'wrapping' to happen in Play rather than in Akka gRPC (on 2.x by adding the 'wrapping' there, and hopefully this can 'evaporate' for Play 3).
Implementing this 'by wrapping' in akka-grpc for now will make it easier to transition that code to Play eventually.
Perhaps if we do the 'wrapping' in the Router
we don't need a separate Hander
implementation anymore?
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'll investigate this, but given the Akka gRPC handler returns completed HttpResponses whereas the Play one returns EssentialActions that need further processing including from filters before they generate a response, not sure wrapping is gonna work.
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.
Right, so the wrapping would take the HttpResponse
generated by the generic handler, and convert it to an EssentialAction
to be passed through further filters. Could that work?
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.
Yep it looks like something along these lines is doable.
private val generateRouterUsingActions: (Logger, Service) => CodeGeneratorResponse.File = (logger, service) => { | ||
val b = CodeGeneratorResponse.File.newBuilder() | ||
b.setContent(RouterUsingActions(service, powerApis).body) | ||
b.setName(s"${service.packageDir}/Abstract${service.name}RouterUsingActions.scala") |
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 we're already generating either the router implemented with Action
s or the router implemented with Akka HTTP models, wouldn't it make sense to generate it with the same name? That way for the end user they should be able to switch between 'backends' without further code changes, right?
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.
Sure, it'd be less work, but as of now the Action router takes an extra constructor parameter.
import scala.collection.immutable | ||
|
||
package object scaladsl { | ||
type MetadataMap = immutable.Map[String, Seq[MetadataEntry]] |
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, so you're promoting the previously-internal MetadataEntry
types to the DSL. Why? Wouldn't it be OK to keep the existing Metadata
facade?
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.
The existing Metadata
facade is a super limited interface that is inadequate for "real" use by service implementers. It only provides getText()
and getBinary()
methods returning optional values if given keys exist. It doesn't provide a means for getting all the keys nor for getting multi-valued keys. So some kind of richer interface is needed, and per discussion on #179 I kept the new-code surface small by using a Map
and reusing MetadataEntry
. If you want the latter to be kept internal, how would you suggest providing this richer API?
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.
Instead of exposing MetadataEntry
we could also consider adding additional methods to the Metadata
API. I don't have a strong preference at this point, though.
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.
Keeping a simple facade gives us more freedom to change implementation though and optimize for performance etc. I think having the facade makes sense, having an explicit Metadata.toSeq
to get the collection capabilities for example would perhaps allow for the more advanced use cases?
|
||
import io.grpc.Status | ||
|
||
trait @{service.name}PowerApi extends @{service.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.
In the future we'd want to allow finer-grained configuration to determine which methods should have the power API and which methods have the simple API. In that case perhaps PowerApi
is not a great name - though I'm not sure about alternatives. This is OK for now.
I've updated |
@raboof @johanandren: I've made the requested changes so am ready to look at supporting java and the other build tools. Please confirm you're good with this moving forward. |
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 general approach is good, but added some comments on details.
codegen/src/main/scala/akka/grpc/gen/scaladsl/ScalaServerCodeGenerator.scala
Outdated
Show resolved
Hide resolved
runtime/src/main/scala/akka/grpc/internal/PlayRouterUsingActions.scala
Outdated
Show resolved
Hide resolved
@for(method <- service.methods) { | ||
case "@method.grpcName" => | ||
val responseCodec = Codecs.negotiate(request) | ||
@{if(powerApis) { "val metadataMap = MetadataMap(request.headers)" } else { "" }} |
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.
Now this is eager for all power APIs even if they don't actually read the metadata. An actual type inbetween could lessen that to just one object allocation, and then parse the headers into our datatypes only if they are accessed.
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.
Reverted to using an enhanced version of the Metadata
trait, see runtime/src/main/scala/akka/grpc/scaladsl/Metadata.scala
.
import scala.collection.immutable | ||
|
||
package object scaladsl { | ||
type MetadataMap = immutable.Map[String, Seq[MetadataEntry]] |
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.
Keeping a simple facade gives us more freedom to change implementation though and optimize for performance etc. I think having the facade makes sense, having an explicit Metadata.toSeq
to get the collection capabilities for example would perhaps allow for the more advanced use 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.
Good, added some detail feedback.
entries | ||
} | ||
|
||
override def getText(key: String): Option[String] = headers.find(_.name == key).map(_.value) |
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 guess these may be from the original implementation, however collectFirst { case header if header.name == key => header.value }
would avoid an extra option and function allocation 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.
Fixed
entries | ||
} | ||
} | ||
type MetadataMap = Map[String, List[MetadataEntry]] |
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 should have a type alias as public API, and the signature isn't that messy, so let's just use that instead.
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.
Fixed
codegen/src/main/scala/akka/grpc/gen/scaladsl/ScalaServerCodeGenerator.scala
Outdated
Show resolved
Hide resolved
entries += (key -> (entry :: Nil)) | ||
} | ||
entries | ||
} |
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 wish we could do this with the mutable Map.newBuilder
instead of creating a new map for each header entry. Not sure that is doable though.
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.
Ya, not with having to group by header 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.
I think we could create the map by folding over the headers, but I guess that's an implementation detail, OK for 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.
That wouldn't help because we'd still pass a map along and update, so it would just be a different way of doing the same.
} | ||
|
||
class MetadataImpl(headers: immutable.Seq[HttpHeader] = immutable.Seq.empty) extends Metadata { | ||
lazy private val b64Decoder = Base64.getDecoder |
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 one is just returning a static final field in java.util.Encoder
no need to introduce a lazy val for 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.
Fixed
Note: I've finished the Java implementation of the power APIs and the maven plugin update, so AFAIK the only things left are the gradle plugin and updating the doc. |
I haven't done a thorough review (traveling) but just wanted to say that this is shaping up nicely! |
Hi all, I've added documentation for the server power APIs and that completes everything that was left to do AFAIK. Please review so we can finally get it merged and I can move on to similarly finishing #447 . |
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.
Some minor comments added, looks good in general.
codegen/src/main/scala/akka/grpc/gen/scaladsl/ScalaTraitCodeGenerator.scala
Show resolved
Hide resolved
codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaServerCodeGenerator.scala
Show resolved
Hide resolved
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.
LGTM, thanks for all the work on this!
We'll want a final LGTM from @raboof as well but he is on vacation right now. |
Thanks @johanandren, I hope @raboof comes back from vacation in a merging mood :-). I'm working on getting #447 finished up 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.
Very cool, sorry to have been a bottleneck here ;).
I think this looks really good! Seems complete in terms of API parity, maven/gradle/sbt support, docs and examples!
@ctoomey many thanks for your perseverance adding this feature to the project!
Yay, thanks @raboof! Happy to be able to contribute back our enhancements to this important project. |
Here's a draft, scala-only implementation of both #179 and #420.
In AkkaGrpcPlugin.scala I added a new setting
akkaGrpcGeneratorOptions
with 2 options:ServerPowerApis
to generate server power APIs, andUsePlayActions
to generate a handler and router that implement Play'sRequestHeader => EssentialAction
instead ofHttpRequest => Future[HttpResponse]
.The server power APIs are defined by a power API trait that extends the base trait and adds a
MetadataMap
parameter to each method.If this looks good I’ll work on the rest of the changes (Java implementation, etc.).