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

WORK IN PROGRESS: Akka-HTTP backend by @2m #205

Closed
wants to merge 36 commits into from

Conversation

ktoso
Copy link
Member

@ktoso ktoso commented Dec 14, 2017

Opening a PR of @2m's work just so it's easier to comment and review :)

THIS IS NOT YET INTENDED TO BE MERGED.

"throw an exception on invalid url" in {
withClient() { client =>
{ client.url("localhost") } must throwAn[IllegalArgumentException]
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I see, consistent with existing WS I guess?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure play-ws has a consistent behavior between Java and Scala APIs though.

Copy link
Member

Choose a reason for hiding this comment

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

So, it is consistent that both versions throw an Exception:

In Scala:

java.lang.IllegalArgumentException: Invalid URL localhost
  at play.api.libs.ws.ahc.StandaloneAhcWSClient.validate(StandaloneAhcWSClient.scala:84)
  at play.api.libs.ws.ahc.StandaloneAhcWSClient.url(StandaloneAhcWSClient.scala:42)
  ... 39 elided
Caused by: java.lang.IllegalArgumentException: localhost could not be parsed into a proper Uri, missing scheme
  at play.shaded.ahc.org.asynchttpclient.uri.Uri.create(Uri.java:40)
  at play.shaded.ahc.org.asynchttpclient.uri.Uri.create(Uri.java:32)
  at play.api.libs.ws.ahc.StandaloneAhcWSClient.validate(StandaloneAhcWSClient.scala:81)
  ... 40 more

And in Java:

java.lang.RuntimeException: java.net.MalformedURLException: no protocol: localhost
  at play.libs.ws.ahc.StandaloneAhcWSRequest.<init>(StandaloneAhcWSRequest.java:89)
  at play.libs.ws.ahc.StandaloneAhcWSClient.url(StandaloneAhcWSClient.java:60)
  ... 39 elided
Caused by: java.net.MalformedURLException: no protocol: localhost
  at java.net.URL.<init>(URL.java:593)
  at java.net.URL.<init>(URL.java:490)
  at java.net.URL.<init>(URL.java:439)
  at play.libs.ws.ahc.StandaloneAhcWSRequest.<init>(StandaloneAhcWSRequest.java:75)
  ... 40 more

Copy link
Member

Choose a reason for hiding this comment

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

The URL parsing happens immediately, not in the Future, but the exact exception wasn't specified in code (no @throws in the scala trait, at least).

Copy link

Choose a reason for hiding this comment

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

The tests are only shared between the different backends but are separate for Scala and Java APIs. So the functionality is encoded per API basis and I have modified akka-http backend implementation to follow the behavior of AHC backend.

@@ -0,0 +1,16 @@
package play.api.libs.ws.akkahttp
Copy link
Member Author

Choose a reason for hiding this comment

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

copyright

Copy link

Choose a reason for hiding this comment

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

Ahh, right. Added manually. Would be best to use sbt-header plugin in the future.

Copy link

Choose a reason for hiding this comment

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

client.close()
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

So you did pull off the sharing tests :)

Copy link

Choose a reason for hiding this comment

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

Yea. Tests are shared between the backends, but are different between the Scala and Java APIs.

It might be possible to share the tests between the Scala and Java APIs as well, but it would need another abstraction layer. Maybe implemented using FreeMonad. :)

withClient() { client =>
val callList = scala.collection.mutable.ArrayBuffer[Int]()
client.url(s"http://localhost:$testServerPort")
.withRequestFilter(new CallbackRequestFilter(callList, 1))
Copy link
Member Author

Choose a reason for hiding this comment

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

are we sure that's safe? (concurrency wise)

Copy link
Member

Choose a reason for hiding this comment

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

oh heck no, that's a implementation for the spec.

Copy link

Choose a reason for hiding this comment

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

Yea. For the test its ok, since every tests has its own immutable request filter.


class AkkaHttpWSRequestFilterSpec(implicit val executionEnv: ExecutionEnv) extends WSRequestFilterSpec {
def withClient()(block: StandaloneWSClient => Result): Result = {
val client = StandaloneAkkaHttpWSClient()
Copy link
Member Author

Choose a reason for hiding this comment

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

I see, so our goal is I guess to just have people change their impl class here and it should be source compatible.
I thought we could do it as just a configuration flag which backend is to be used hm

Copy link

Choose a reason for hiding this comment

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

When used in Play framework, the client is injected using the API interface (implementation non-specific) like so:

class Application @Inject() (ws: WSClient) extends Controller {}

So to use the akka-http backend client one will have to change the dependency in the project. Something like

<<< libraryDependencies += ws
>>> libraryDependencies += wsAkkaHttp

or similar.

*/
@Override
public Object getUnderlying() {
return null;
Copy link
Member Author

Choose a reason for hiding this comment

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

should be Http.get(system)?

Copy link

Choose a reason for hiding this comment

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

Yes, added.

@Override
public void close() throws IOException {

}
Copy link
Member Author

Choose a reason for hiding this comment

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

Could be httpshutdownAllConnectionPools()?

Copy link

Choose a reason for hiding this comment

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

Yes, added.


private final ActorSystem sys;
private final Materializer mat;

Copy link
Member Author

Choose a reason for hiding this comment

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

Store the val http = Http() created from those and pass it into the StandaloneAkkaHttpWSRequest.
This way if someone calls the close here the right things would be closed

Copy link

Choose a reason for hiding this comment

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

As Http is an extension, Http.get(sys).shutdownAllConnectionPools() in close is going to be the same as using the stored reference, right?

*/
@Override
public Map<String, List<String>> getHeaders() {
return null;
Copy link
Member Author

Choose a reason for hiding this comment

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

FIXME?

Copy link

@2m 2m Dec 18, 2017

Choose a reason for hiding this comment

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

Yea. :) I treat return null and ??? as fixme's currently. WIll not merge until any of these are left.

import java.util.Map;
import java.util.Optional;

public final class StandaloneAkkaHttpWSResponse implements StandaloneWSResponse {
Copy link
Member Author

Choose a reason for hiding this comment

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

Seems a bit inversed? Thought this would wrap the Akka HTTP HttpResponse?

Copy link

Choose a reason for hiding this comment

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

Yes. Wrapping Scala API did not fly. Now it wraps akka.http.javadsl.model.HttpResponse

/**
* Get the underlying response object.
*/
override def underlying[T]: T = ???
Copy link
Member Author

Choose a reason for hiding this comment

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

return the response?


final class StandaloneAkkaHttpWSResponse private (val response: HttpResponse)(implicit val mat: Materializer) extends StandaloneWSResponse {

final val UnmarshalTimeout = 1.second
Copy link
Member Author

Choose a reason for hiding this comment

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

could it be configurable somehow?

Copy link

Choose a reason for hiding this comment

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

Configuration still needs to be tackled. Noted in the mega ticket.

/**
* The response status message.
*/
override def statusText: String = ???
Copy link
Member Author

Choose a reason for hiding this comment

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

That's the reason field for us : scala> akka.http.scaladsl.model.StatusCodes.OK.reason

*/
override def body: String = {
import akka.http.scaladsl.unmarshalling.PredefinedFromEntityUnmarshallers.stringUnmarshaller
Await.result(Unmarshal(response).to[String], UnmarshalTimeout)
Copy link
Member Author

Choose a reason for hiding this comment

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

I would rather suggest that we do a toStrict on the response, and store the strictified entity.
The current impl would blow up if we do r.body; r.body I think?

Choose a reason for hiding this comment

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

Yes, I agree, that will only work once. We first need to clarify the semantics whether these methods can be called several times and then figure out how to be able to make it work.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think play people value convenience a lot.in this API... perhaps we can do some cheating with a CAS ing state machine here? like, here spin a future and await hehe other to drain? a hack, but it would get us to ship this WS impl. and once we do the smarter things in Akka http we could remove the hack here?

Choose a reason for hiding this comment

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

It could probably just be a lazy val that collects all the stuff. The question is in which way, so that the below methods would also work. That's what I meant with "clarify the semantics", i.e. whether it is allowed to call one or more of those body interpreting/accessing methods and whether it is allowed to call them only once or several times.

Copy link

@2m 2m Dec 18, 2017

Choose a reason for hiding this comment

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

responded only seeing the first comment

Yea, current impl will blow up with such a testcase and needs to be fixed.

I am not sure about calling toStrict every time. If the user is using the client for streamed responses and is materializing stream from bodyAsSource only once then we should not waste resources with by calling toStrict internally. Take a look at 8d4a2e8 where I store strict response only when getBody or getBodyBytes is called.

/**
* @return the response as a source of bytes
*/
override def bodyAsSource: Source[ByteString, _] = response.entity.dataBytes
Copy link
Member Author

Choose a reason for hiding this comment

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

naisu :-)

/**
* Execute this request
*/
override def execute(): Future[Response] = {
Copy link
Member Author

Choose a reason for hiding this comment

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

Good to ask play team about semantics here.
Does execute mean strict and stream "streaming" as our default mode is?
If so, then we should rather implement the stream() and the execute() should be implemented in terms of it, by applying a toStrict()?

Copy link
Member

Choose a reason for hiding this comment

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

Does execute mean strict and stream "streaming" as our default mode is?

Yes. We should make this API clearer later.

Copy link

Choose a reason for hiding this comment

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

Yes. That makes sense.

* @return a future with the response for the PATCH request
*/
override def patch[T: BodyWritable](body: T): Future[Response] =
withBody(body).execute("PATCH")
Copy link
Member Author

Choose a reason for hiding this comment

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

naisu ^_^

/**
* Sets the proxy server to use in this request
*/
override def withProxyServer(proxyServer: WSProxyServer): Self = ???
Copy link
Member Author

Choose a reason for hiding this comment

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

missing feature in our impl right? AFAIR we still only do https proxy but not any http?

Copy link
Member Author

Choose a reason for hiding this comment

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

Add fixme and link to ticket in akka-http

/**
* Sets whether redirects (301, 302) should be followed automatically
*/
override def withFollowRedirects(follow: Boolean): Self = ???
Copy link
Member Author

Choose a reason for hiding this comment

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

Missing Akka HTTP feature, please link to the ticket from here :)

Copy link
Member Author

Choose a reason for hiding this comment

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

And let's mark the ticket as "play integration" (we have such tag)

@@ -47,15 +47,20 @@ object Dependencies {

val akkaVersion = "2.5.3"
val akkaStreams = Seq("com.typesafe.akka" %% "akka-stream" % akkaVersion)
val akkaHttp = Seq("com.typesafe.akka" %% "akka-http" % "10.0.8")
val akkaHttp = Seq("com.typesafe.akka" %% "akka-http" % "10.0.11")
val akkaHttpCore = Seq("com.typesafe.akka" %% "akka-http" % "10.0.11")
Copy link
Member

Choose a reason for hiding this comment

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

Is this duplication intentional? (akkaHttp is used, at least once, in build.sbt but not in Dependencies)

If the duplication is intentional, what is the difference between akkaHttp and akkaHttpCore?

Copy link

Choose a reason for hiding this comment

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

Sorry, messed this up. My idea was to make the new akka-http backend depend only on akka-http-core. But later I needed more (marshalling) from akka-http. Fixed in 108ee30

*/
@Override
public CompletionStage<? extends StandaloneWSResponse> get() {
return execute("GET");

Choose a reason for hiding this comment

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

Could we use the constant values here where we know it statically like HttpMethods.GET?

Copy link

Choose a reason for hiding this comment

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

Done in f00d8b9

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public final class StandaloneAkkaHttpWSRequest implements StandaloneWSRequest {

Choose a reason for hiding this comment

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

I think we talked about that already, but is there any good reason to implement these classes in Java? Keeping the interfaces written in Java might make sense but the implementation could still be done in Scala, right? If that's not possible I would still prefer an approach where the Java classes just delegate to the real implementation which could then be written in Scala.

Copy link
Contributor

@schmitch schmitch Dec 18, 2017

Choose a reason for hiding this comment

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

This is not possible that easily:

This was an expirment i did in the past: https://github.com/schmitch/play-ws-akka
The biggest probem is that if something returns java.lang.Boolean instead of boolean or everything that returns a Future/CompletionStage I created a WrappedFuture to support that: https://github.com/schmitch/play-ws-akka/blob/master/play-ws-akka-standalone/src/main/scala/play/api/libs/ws/akkaws/WrappedFuture.scala Basically all overlapping types, might be problematic.

The delegate approach might be more feasible.

Copy link

Choose a reason for hiding this comment

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

At least doing it this way uncovered some missing Java API methods in Akka Http.

Copy link
Member

Choose a reason for hiding this comment

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

is there any good reason to implement these classes in Java

Similar to what @2m said, the main reason is idiom -- if you're changing or adding to the API in Java but writing the tests in Scala, you often don't feel the pain of refactoring because you can leverage the Scala Collections API. It also helps for discovery, because Java users don't have to decipher Scala tests to see how the code should work, and writing documentation, because the tests and examples match up.

Choose a reason for hiding this comment

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

I was only talking about implementation. Interfaces and tests could and probably should be done in Java, I agree.

@2m
Copy link

2m commented Dec 18, 2017

Addressed review feedback. Will continue implementing missing features and adding tests further.

@2m
Copy link

2m commented Dec 20, 2017

This PR now contains everything, except the features listed in #207

I have also published a JAR from this PR for anyone that would like to try it out:

resolvers += Resolver.bintrayRepo("2m", "maven")
libraryDependencies += "com.typesafe.play" %% "play-akka-http-ws-standalone" % "1.0.0+62-7728dc21"

This PR does not touch the existing implementation, so after a review it can be safely merged and the work can continue in separate PRs.

@@ -47,7 +47,7 @@ object Dependencies {

val akkaVersion = "2.5.3"

Choose a reason for hiding this comment

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

2.5.8?

Copy link

Choose a reason for hiding this comment

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

Yes, we could update it. But should probably be done in a separate PR.

@2m
Copy link

2m commented Dec 22, 2017

Added one more commit which adds a HTTPS test and also allows to use a custom HTTPS connection context.

This is ready to be reviewed and merged so the following work for implementing #207 can be done as separate PRs.

@ktoso
Copy link
Member Author

ktoso commented Dec 22, 2017

Closing, Martynas will submit the same branch as not-WIP PR :)

@ktoso ktoso closed this Dec 22, 2017
@2m
Copy link

2m commented Dec 22, 2017

Follow up PR: #208

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.

8 participants