Skip to content
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

Merged
merged 45 commits into from
Feb 25, 2019
Merged

Conversation

ctoomey
Copy link
Contributor

@ctoomey ctoomey commented Oct 7, 2018

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, and UsePlayActions to generate a handler and router that implement Play's RequestHeader => EssentialAction instead of HttpRequest => 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.).

@akka-ci
Copy link

akka-ci commented Oct 7, 2018

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!

@johanandren
Copy link
Member

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. )

@raboof
Copy link
Member

raboof commented Oct 9, 2018

OK TO TEST

Copy link
Member

@raboof raboof left a 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) {
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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")
Copy link
Member

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 Actions 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?

Copy link
Contributor Author

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]]
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Member

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} {
Copy link
Member

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.

@ctoomey ctoomey changed the title Server-side power API #179, More idiomatic Play integration #420 Server-side power API #179 Oct 29, 2018
@ctoomey
Copy link
Contributor Author

ctoomey commented Oct 29, 2018

I've updated AkkaGrpcPlugin to include the additional server-side code generation options into akkaGrpcCodeGeneratorSettings as requested.

@ctoomey
Copy link
Contributor Author

ctoomey commented Nov 6, 2018

@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.

Copy link
Member

@johanandren johanandren left a 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.

@for(method <- service.methods) {
case "@method.grpcName" =>
val responseCodec = Codecs.negotiate(request)
@{if(powerApis) { "val metadataMap = MetadataMap(request.headers)" } else { "" }}
Copy link
Member

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.

Copy link
Contributor Author

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]]
Copy link
Member

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?

Copy link
Member

@johanandren johanandren left a 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)
Copy link
Member

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

Copy link
Contributor Author

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]]
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

entries += (key -> (entry :: Nil))
}
entries
}
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@ctoomey
Copy link
Contributor Author

ctoomey commented Jan 22, 2019

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.

@johanandren
Copy link
Member

I haven't done a thorough review (traveling) but just wanted to say that this is shaping up nicely!

@ctoomey
Copy link
Contributor Author

ctoomey commented Feb 5, 2019

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 .

Copy link
Member

@johanandren johanandren left a 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.

Copy link
Member

@johanandren johanandren left a 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!

@johanandren
Copy link
Member

We'll want a final LGTM from @raboof as well but he is on vacation right now.

@ctoomey
Copy link
Contributor Author

ctoomey commented Feb 7, 2019

Thanks @johanandren, I hope @raboof comes back from vacation in a merging mood :-).

I'm working on getting #447 finished up now.

ctoomey added a commit to livongo/akka-grpc that referenced this pull request Feb 12, 2019
Copy link
Member

@raboof raboof left a 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!

@raboof raboof merged commit 4449442 into akka:master Feb 25, 2019
@ctoomey
Copy link
Contributor Author

ctoomey commented Feb 25, 2019

Yay, thanks @raboof! Happy to be able to contribute back our enhancements to this important project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants