5

I am very new to akka-http and have troubles to run queries on the same route in parallel.

I have a route that may return the result very quickly (if cached) or not (heavy CPU multithreaded computations). I would like to run these queries in parallel, in case a short one arrives after a long one with heavy computation, I do not want the second call to wait for the first to finish.

However it seems that these queries do not run in parallel if they are on the same route (run in parallel if on different routes)

I can reproduice it in a basic project:

Calling the server 3 time in parallel (with 3 Chrome's tab on http://localhost:8080/test) causes the responses to arrive respectively at 3.0s, 6.0-s and 9.0-s. I suppose queries do not run in parallel.

Running on a 6 cores (with HT) machine on Windows 10 with jdk 8.

build.sbt

name := "akka-http-test"

version := "1.0"

scalaVersion := "2.11.8"

libraryDependencies += "com.typesafe.akka" %% "akka-http-experimental" % "2.4.11"

*AkkaHttpTest.scala**

import java.util.concurrent.Executors

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer

import scala.concurrent.{ExecutionContext, Future}

object AkkaHttpTest extends App {

  implicit val actorSystem = ActorSystem("system") // no application.conf here
  implicit val executionContext = 
                             ExecutionContext.fromExecutor(Executors.newFixedThreadPool(6))
  implicit val actorMaterializer = ActorMaterializer()

  val route = path("test") {
    onComplete(slowFunc()) { slowFuncResult =>
      complete(slowFuncResult)
    }
  }

  def slowFunc()(implicit ec: ExecutionContext): Future[String] = Future {
    Thread.sleep(3000)
    "Waited 3s"
  }

  Http().bindAndHandle(route, "localhost", 8080)

  println("server started")
}

What am I doing wrong here ?

Thanks for your help

EDIT: Thanks to @Ramon J Romero y Vigil, I added Future Wrapping, but the problem still persists

def slowFunc()(implicit ec : ExecutionContext) : Future[String] = Future {
  Thread.sleep(3000)
  "Waited 3.0s"
}

val route = path("test") {
  onComplete(slowFunc()) { slowFuncResult =>
    complete(slowFuncResult)
  }
}

Tries with a the default Thread pool, the one defined above in the config file, and a Fixed Thread Pool (6 Threads).

It seems that the onComplete directive still waits for the future to complete and then block the Route (with same connection).

Same problem with the Flow trick

import akka.stream.scaladsl.Flow

val parallelism = 10

val reqFlow = 
  Flow[HttpRequest].filter(_.getUri().path().equalsIgnoreCase("/test"))
                   .mapAsync(parallelism)(_ => slowFunc())
                   .map(str => HttpResponse(status=StatusCodes.Ok, entity=str))

Http().bindAndHandle(reqFlow, ...)

Thanks for your help

Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
ogen
  • 802
  • 2
  • 7
  • 23

2 Answers2

7

Each IncomingConnection is handled by the same Route, therefore when you "call the server 3 times in parallel" you are likely using the same Connection and therefore the same Route.

The Route is handling all 3 incoming HttpRequest values in an akka-stream fashion, i.e. the Route is composed of multiple stages but each stage can only processes 1 element at any given time. In your example the "complete" stage of the stream will call Thread.sleep for each incoming Request and process each Request one-at-a-time.

To get multiple concurrent requests handled at the same time you should establish a unique connection for each request.

An example of the client side connection pool can be created similar to the documentation examples:

import akka.http.scaladsl.Http

val connPoolFlow = Http().newHostConnectionPool("localhost", 8080)

This can then be integrated into a stream that makes the requests:

import akka.http.scaladsl.model.Uri._
import akka.http.scaladsl.model.HttpRequest

val request = HttpRequest(uri="/test")

import akka.stream.scaladsl.Source

val reqStream = 
  Source.fromIterator(() => Iterator.continually(request).take(3))
        .via(connPoolFlow)
        .via(Flow.mapAsync(3)(identity))
        .to(Sink foreach { resp => println(resp)})
        .run()

Route Modification

If you want each HttpRequest to be processed in parallel then you can use the same Route to do so but you must spawn off Futures inside of the Route and use the onComplete directive:

def slowFunc()(implicit ec : ExecutionContext) : Future[String] = Future {
  Thread.sleep(1500)
  "Waited 1.5s"
}

val route = path("test") {
  onComplete(slowFunc()) { slowFuncResult =>
    complete(slowFuncResult)
  }
}

One thing to be aware of: if you don't specify a different ExecutionContext for your sleeping function then the same thread pool for routes will be used for your sleeping. You may exhaust the available threads this way. You should probably use a seperate ec for your sleeping...

Flow Based

One other way to handle the HttpRequests is with a stream Flow:

import akka.stream.scaladsl.Flow

val parallelism = 10

val reqFlow = 
  Flow[HttpRequest].filter(_.getUri().path().equalsIgnoreCase("/test"))
                   .mapAsync(parallelism)(_ => slowFunc())
                   .map(str => HttpResponse(status=StatusCodes.Ok, entity=str))

Http().bindAndHandle(reqFlow, ...)
Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
  • It is not clear for me, how should I do that ? Thanks for your help. – ogen Nov 10 '16 at 17:18
  • This Rest API will be called in parallel by the same server, And I can't re-establish a connection each time between the server and the Rest API – ogen Nov 10 '16 at 17:30
  • @ogen see the updated connection pool code I added. You can have multiple connections between the same client & host, even on the same port... – Ramón J Romero y Vigil Nov 10 '16 at 17:31
  • Thanks for your help. Is there a way to handle this server-side ? The strange thing is when I call it from different chrome windows, the stages still not run in parallel. Do the Chromes windows share the same connection even if they are different processes ? – ogen Nov 10 '16 at 17:41
  • I am unaware of any way to force each incoming `HttpRequest` value to be processed by a seperate Route. You can wrap the `Thread.sleep` functionality inside of a Future and wrap it all inside of an `onComplete` Directive which will give you the same effect as different Routes... – Ramón J Romero y Vigil Nov 10 '16 at 17:46
  • Could you please write a snippet for the wrapping into the Future ? Thanks a lot – ogen Nov 10 '16 at 17:53
  • This still does not work, the second call still waits for the sleep of the first call to finish. I have tried with the default dispatcher and the one defined in my original post, any suggestion ? Thanks for your hem – ogen Nov 14 '16 at 09:11
  • original post updated. It seems that the onComplete directive still waits for the Future to complete and then still block the Route/Connection. There is no blocking if I call different routes. Am I doing something wrong ? – ogen Nov 14 '16 at 10:56
  • @ogen I'm very surprised by your results. I have seen the opposite effect to what you described. I've added another possible solution using Flows. Try that... – Ramón J Romero y Vigil Nov 14 '16 at 13:49
  • From where whould I import Flow ? My Intellij does not resolve it. The onComplete method you suggested should work, and I would like to understand what it does not on my machine, do you see any misconfiguration I have done ? Thank you for your help – ogen Nov 14 '16 at 15:03
  • @ogen I updated the import to show where Flow is coming from and added a link to documentation. It would be VERY hard figure out your specific problem without seeing the full code stack, build, configuration files, etc... – Ramón J Romero y Vigil Nov 14 '16 at 18:23
  • U Iwas able to run the server with your Flox trick but the queries still run as if they were in a queue. I have rewritten my original post with the build file and the full code. Thanks for your help – ogen Nov 15 '16 at 09:34
  • I have tried to spawn Futures manually in the path directive, and it really seems that only one thread could run in the path at a time. Do you reproduce the problem with the code/build.sbt given in the original post.? Thank you so much for your help – ogen Nov 16 '16 at 09:07
1

In case this is still relevant, or for future readers, the answer is inside Http().bindAndHandle documentation:

 /**
   * Convenience method which starts a new HTTP server...
   * ... 
   * The number of concurrently accepted connections can be configured by overriding
   * the `akka.http.server.max-connections` setting....
   * ...
   */
  def bindAndHandle(...

use akka.http.server.max-connections setting for number of concurrent connections.

shay__
  • 3,815
  • 17
  • 34
  • I think the question asks about handling *requests* in parallel, while this is talking about handling *connections* concurrently (which is done by default, anyway.) – LCC Apr 05 '19 at 21:30