Load Balancing akka-http - scala

I am using akka-http, my build.sbt configuration is:
scalaVersion := "2.11.7"
libraryDependencies += "com.typesafe.akka" % "akka-actor_2.11" % "2.4.2"
libraryDependencies += "com.typesafe.akka" % "akka-http-experimental_2.11" % "2.4.2"
libraryDependencies += "com.typesafe.akka" % "akka-http-spray-json-experimental_2.11" % "2.4.2"
libraryDependencies += "com.typesafe.akka" % "akka-slf4j_2.11" % "2.4.2"
I am exposing a simple REST api with only one GET url
foo is a function that returns a Future
implicit val actorSystem = ActorSystem("system", config)
implicit val actorMaterializer = ActorMaterializer()
val route: Route = {
get {
path("foo") {
complete { foo }
}
}
}
The web-service is expected to have a lot of calls, and I want to make the service redundant in case of failure, so I want to have two instances running simultaneously to handle all the requests.
1) what is the best way to have two intances of the web-service handling the requests simultaneously?, with an external load balancer or with some magic (wich I don't know) inside akka/akka-http ?
2) What are the principal parameters I have to tune up to improve perfomance?

The answer to this question demonstrates how to make an Actor call from within a Route.
If you combine that technique with the clustering capabilities within Akka you should be able to get the job done.
Have your Route send a message to a Router that will dispatch the message to 1 of N remotely deployed Actors (from your question it sounds like a round robin router is what you want).
class HttpResponseActor extends Actor {
def foo : HttpResponse = ??? // Not specified in question
override def receive = {
case _ : HttpRequest => foo
}
}
import akka.actor.{ Address, AddressFromURIString }
import akka.remote.routing.RemoteRouterConfig
val addresses = Seq(Address("akka.tcp", "remotesys", "otherhost", 1234),
AddressFromURIString("akka.tcp://othersys#anotherhost:1234"))
val routerRemote =
system.actorOf(RemoteRouterConfig(RoundRobinPool(5), addresses).props(Props[HttpResponseActor]))
The remote Actor responds with the HttpResponse. This Response can go through the Router or directly back to the Route.
The Route sticks the answer in a complete Directive to return back to the client.
val route =
get {
path("foo") {
onComplete((routerRemote ? request).mapTo[HttpResponse]) {
case Success(response) => complete(response)
case Failure(ex) => complete((InternalServerError, s"Actor not playing nice: ${ex.getMessage}"))
}
}
}

Related

Trace4Cats example for Tapir endpoints

I am learning about different Scala libraries and I got to tracing. Trace4Cats claims integration with Tapir endpoints and I want to include it inside my example Play SIRD router that uses Tapir with OpenAPI documentation.
So far I've included these dependencies for tracing:
// Tracing
libraryDependencies += "io.janstenpickle" %% "trace4cats-core" % trace4CatsVersion
libraryDependencies += "io.janstenpickle" %% "trace4cats-inject" % trace4CatsVersion
libraryDependencies += "io.janstenpickle" %% "trace4cats-avro-exporter" % trace4CatsVersion
libraryDependencies += "io.janstenpickle" %% "trace4cats-sttp-tapir" % trace4CatsVersion
libraryDependencies += "io.janstenpickle" %% "trace4cats-datadog-http-exporter" % trace4CatsVersion
I have a working Tapir example with Play Framework's SIRD router, as suggested by Tapir docs. Here is the ApiRouter:
#Singleton
class ApiRouter #Inject() (implicit mat: Materializer) extends SimpleRouter {
// Interpreter
private val interpreter = PlayServerInterpreter()
// Controller logic
def countCharacters(s: String): Future[Either[Unit, Int]] =
Future(Right[Unit, Int](s.length))
// Endpoint
val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] =
endpoint
.tag("Example API")
.in("count")
.in(query[String]("string"))
.out(plainBody[Int])
.errorOut(
statusCode(StatusCode.NotFound)
)
// Route
val countCharactersRoutes: Routes =
interpreter.toRoutes(countCharactersEndpoint.serverLogic(countCharacters))
// OpenAPI
private val openApiDocs: OpenAPI = OpenAPIDocsInterpreter().toOpenAPI(
List(countCharactersEndpoint),
"Tapir Play Sample",
"1.0.0"
)
// Doc will be on /docs
private val openApiRoute: Routes = interpreter.toRoutes(SwaggerUI[Future](openApiDocs.toYaml))
// Router
override def routes: Routes =
openApiRoute
.orElse(countCharactersRoutes)
}
I have tried to search Trace4Cats docs on how to integrate it with Tapir, but all I have found is other examples, including STTP, but I'm not sure of the syntax for Tapir. I need help from someone that has experience with Trace4Cats (or Natchez or any other solution that can work here). Help is greatly appreciated.
Your question is too broad to give a precise answer but I would recommend you to look at the tests and examples on the GitHub repository of trace4cats: https://github.com/trace4cats/trace4cats-sttp/tree/master/modules/sttp-tapir/src/test/scala/io/janstenpickle/trace4cats/sttp/tapir

Akkatype with classic actors giving me an error: Unsupported access to ActorContext from outside of Actor

I am getting the following runtime error in my akka application (using both akka typed and classic)
java.lang.UnsupportedOperationException: Unsupported access to
ActorContext from the outside of
Actor[akka://my-classic-actor-system/user/ChatServer#1583147696]. No
message is currently processed by the actor, but ActorContext was
called from
Thread[my-classic-actor-system-akka.actor.default-dispatcher-5,5,run-main-group-0].
def main(args: Array[String]): Unit = {
val logger = LoggerFactory.getLogger(getClass)
logger.debug("Server starting yo...")
implicit val system = akka.actor.ActorSystem("my-classic-actor-system")
implicit val typedGuardian: ActorRef[Any] =
system.spawn(HttpWebsocketServer(), "ChatServer")
val client = system.actorOf(Client.props(remote), "clientActor")
//..
}
The source of the error is inside of HttpWebsocketServer which is taken from (
https://github.com/johanandren/chat-with-akka-http-websockets/blob/akka-2.6/src/main/scala/chat/Server.scala#L101):
object HttpWebsocketServer {
def apply(): Behavior[Any] = {
Behaviors.setup[Any] { context =>
implicit val ec: ExecutionContext = context.executionContext
val system = context.system
val chatRoom = context.spawn(ChatRoom(), "chat")
val userParent = context.spawn(SpawnProtocol(), "users")
val maker =
context.spawn(websock.Maker(), "mmaker")
val route =
path("chat") {
get {
handleWebSocketMessages(
newUserFlow(system, chatRoom, userParent)
)
}
}
// needed until Akka HTTP has a 2.6 only release
implicit val materializer: Materializer =
SystemMaterializer(context.system).materializer
implicit val classicSystem: akka.actor.ActorSystem =
context.system.toClassic
Http()
.bindAndHandle(route, "0.0.0.0", 9000)
// future callback, be careful not to touch actor state from in here
.onComplete {
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
}
Behaviors.empty
}
}
}
build.sbt
val AkkaVersion = "2.6.18"
val AkkaHttpVersion = "10.2.7"
libraryDependencies ++= Seq(
"io.netty" % "netty-all" % "4.1.68.Final",
"com.typesafe.akka" %% "akka-actor" % AkkaVersion,
"com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-stream" % AkkaVersion,
"com.typesafe.akka" %% "akka-stream-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion,
"com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion,
"ch.qos.logback" % "logback-classic" % "1.2.10",
scalaTest % Test
)
If I comment out below in my code I don't get the runtime error, so it looks like that is the source of the issue:
implicit val materializer: Materializer =
SystemMaterializer(context.system).materializer
implicit val classicSystem: akka.actor.ActorSystem =
context.system.toClassic
Http()
.bindAndHandle(route, "0.0.0.0", 9000)
// future callback, be careful not to touch actor state from in here
.onComplete {
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
}
I'm not sure what I can do to solve this issue, I am creating actors from either the system or child actors.
You are not allowed to use context outside of an actor. And you do it in callback of your future.
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
The problem is that actor context is treated as internal state of an actor and must not be accessed outside of the scope. Even your comment says that
// future callback, be careful not to touch actor state from in here
The solution is to pre-reference your log and system (you have this already) in a val outside of the callback. The callback can look like
case Success(binding) =>
log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
log.error("Server failed to start, terminating")
system.terminate()

How to use a standalone WSClient in a Scala application

I would like to use ws in a standalone application. Trying this code, copied from https://gist.github.com/cdimascio/46b2b7d2986636c1189c :
import com.ning.http.client.AsyncHttpClientConfig
import play.api.libs.ws.ning._
import play.api.libs.ws._
// provide an execution context
import scala.concurrent.ExecutionContext.Implicits.global
object WSStandaloneTest {
def main(args: Array[String]) {
// set up the client
val config = new NingAsyncHttpClientConfigBuilder(DefaultWSClientConfig()).build
val builder = new AsyncHttpClientConfig.Builder(config)
val client = new NingWSClient(builder.build)
// execute a GET request
val response = client.url("http://www.example.com").get
// print the response body
response.foreach(r => {
println(r.body)
// not the best place to close the client,
// but it ensures we dont close the threads before the response arrives
// Good enough for the gist :-D
client.close()
})
}
}
Results in the following error:
[error] object ning is not a member of package play.api.libs.ws
[error] import play.api.libs.ws.ning._
In my build.sbt I have this:
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.1"
libraryDependencies += "com.typesafe.play" %% "play-ws" % "2.6.1"
What am I doing wrong?
NingWSClient is deprecated in Play! 2.5.x.
In 2.6.x
The ning package has been replaced by the ahc package, and the Ning* classes replaced by AHC*.
There is a migration guide available in the official doc.
So you can choose to downgrade to 2.5.x and use ning or update the code.

Obtaining the client IP in Akka-http

I am trying to write an Akka HTTP microservice (akka version 2.4.11, Scala version 2.11.8, both latest versions at time of writing) which is aware of the client service's IP (i.e., remote address), and I cannot get this to work.
I can create and run a service which says 'Hello!' using a route like this:
val routeHello: Route = path("SayHello") {
get {
entity(as[String]) {
body => complete {
HttpResponse(entity = HttpEntity("Hello!"))
}
}
}
}
I have constructed a similar route to the one above, which is extended so that it is aware of the client's IP address.
I noted that I need to edit the application.conf file and set 'remote-address-header = on' to enable the addition of a Remote-Address header holding the clients (remote) IP address. I have done this in case it is required.
Here is the route:
val routeHelloIp: Route = path("SayHelloIp") {
get {
// extractClientIp appears to be working as a filter
// instead of an extractor - why?
extractClientIp {
clientIp => {
entity(as[String]) {
body => complete {
HttpResponse(entity = HttpEntity("Hello!"))
}
}
}
}
}
}
However when I run this route, I get a message 'The requested resource could not be found.'.
It looks like I have got the Akka-http DSL syntactic sugar wrong in the example above. I would be grateful if you could put me on the right path!
EDIT:
I have tried the following program in response to Ramon's helpful answer. Unfortunately it does not compile and I cannot see what I need to do to make it compile.
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.IncomingConnection
import java.net.InetSocketAddress
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.Sink
import akka.http.scaladsl.server.Directives._
import java.net.InetSocketAddress
object TestHttp {
def main(args: Array[String]) {
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// allow connections from any IP
val interface = "0.0.0.0"
//from the question
def createRoute(address: InetSocketAddress) = path("SayHelloIp") {
get {
extractRequestEntity { entity =>
entity(as[String]) { body =>
complete(entity = s"Hello ${address.getAddress().getHostAddress()}")
}
}
}
}
Http().bind(interface).runWith(Sink foreach { conn =>
val address = conn.remoteAddress
conn.handleWithAsyncHandler(createRoute(address))
})
}
}
I have the following build.sbt to ensure that the latest version of Scala and akka-http are used:
import sbt.Keys._
name := "Find my IP"
version := "1.0"
scalaVersion := "2.11.8"
resolvers ++= Seq(
"Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
)
libraryDependencies ++= {
Seq(
"com.typesafe.akka" %% "akka-actor" % "2.4.11",
"com.typesafe.akka" %% "akka-stream" % "2.4.11",
"com.typesafe.akka" %% "akka-http-experimental" % "2.4.11",
"com.typesafe.akka" %% "akka-http-core" % "2.4.11"
)
}
I get the following compile-time errors:
[error] /Users/tilopa/temp/akka-test/src/main/scala/Test.scala:24: akka.http.scaladsl.model.RequestEntity does not take parameters
[error] entity(as[String]) { body =>
[error] ^
[error] /Users/tilopa/temp/akka-test/src/main/scala/Test.scala:25: reassignment to val
[error] complete(entity = s"Hello ${address.getAddress().getHostAddress()}")
[error] ^
[error] two errors found
[error] (compile:compileIncremental) Compilation failed
Using extractClientIp
extractClientIp is not working for you because the sender has not specified one of the required header fields. From the documentation:
Provides the value of X-Forwarded-For, Remote-Address, or X-Real-IP
headers as an instance of RemoteAddress.
You just have to turn on the right setting in your sender:
The akka-http server engine adds the Remote-Address header to every
request automatically if the respective setting
akka.http.server.remote-address-header is set to on. Per default it is
set to off.
generic solution
If you want this to work for any HttpRequest, not just the ones with the correct header settings, then you have to use the bind method on an HttpExt instead of bindAndHandle:
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.IncomingConnection
import java.net.InetSocketAddress
implicit val actorSystem : ActorSystem = ???
implicit val actorMat = ActorMaterializer()
//alow connections from any IP
val interface = "0.0.0.0"
//from the question
def createRoute(address : InetSocketAddress) = path("SayHelloIp") {
get {
extractRequestEntity { entity =>
entity(as[String]) { body =>
complete(entity = s"Hello ${address.getAddress().getHostAddress()}")
}
}
}
}
Http().bind(interface).runWith(Sink foreach { conn =>
val address = conn.remoteAddress
conn.handleWithAsyncHandler(createRoute(address))
})
Edit
As noted in the comments: since akka 10.0.13 use conn.handleWith.
HttpExt.bind() has been depreciated. Here's a workaround with connectionSource():
val https: HttpsConnectionContext = /*...*/
Http(actorSystem).
newServerAt(interface,httpsPort).
enableHttps(https).
connectionSource().
to(Sink.foreach { connection =>
println("New Connection from remote address :: ${connection.remoteAddress}")
// handle connection here
}).run().onComplete {
case Success(binding)=>
binding.addToCoordinatedShutdown(hardTerminationDeadline = 10.seconds)
log.info("{} HTTPS Server running {}",errorContext,binding)
case Failure(ex)=>
log.error("{} HTTPS Server failed {}",errorContext,ex.toString)
sys.exit(10)
}

Get content from Akka ResponseEntity in Scala

I do a GET HTTP call to a rest service which returns a json. I would like to parse the json to a scala object but here I got stuck. I am using the Akka api and I can't manage to retrieve the content from the Akka's ResponseEntity
Here is my sbt file:
name := "ScalaHttp"
version := "1.0"
scalaVersion := "2.11.8"
libraryDependencies ++={
val akkaV = "2.4.5"
Seq(
"com.typesafe.akka" %% "akka-http-core" % akkaV,
"com.typesafe.play" %% "play-json" % "2.4.0-M3"
)
}
And here is the app
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpRequest
import akka.stream.ActorMaterializer
import scala.concurrent.ExecutionContext.Implicits.global
object Sender {
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
Http().singleRequest(HttpRequest(uri = "http://declinators.com/declinator?noun=komunikacja")) foreach {
y => println(y.entity)
println(y.entity.getContentType())
println(y.entity.contentType)
}
}
}
This prints:
HttpEntity.Strict(application/json,{"nominative":"komunikacja","genitive":"komunikacji","dative":"komunikacji","accusative":"komunikacjÄ™","instrumental":"komunikacjÄ…","locative":"komunikacji","vocative":"komunikacjo"})
application/json
application/json
Here come the questions:
1. Why ResponseEntity supplies getContentType() and contentType()? They return the same thing.
2. Getting the contentyType is easy, you have two ways for doing it, but how can I get the content itself, so I can play (i.e. parse it using play) with the json!
You can use entity.data.toString for Binary content type, or the following code piece
data.decodeString(nb.charset.value)
Please follow the HttpEntity.Strict.toString implementation here for detail:
https://github.com/akka/akka/blob/master/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala#L316