4

Using akka stream and akka HTTP, I have created a stream which polls an API every 3 seconds, Unmarshalls the result to a JsValue object and sends this result to an actor. As can be seen in the following code:

// Source which performs an http request every 3 seconds.
val source = Source.tick(0.seconds,
                         3.seconds,
                         HttpRequest(uri = Uri(path = Path("/posts/1"))))

// Processes the result of the http request
val flow = Http().outgoingConnectionHttps("jsonplaceholder.typicode.com").mapAsync(1) {
  
  // Able to reach the API.
  case HttpResponse(StatusCodes.OK, _, entity, _) =>
    // Unmarshal the json response.
    Unmarshal(entity).to[JsValue]

  // Failed to reach the API.
  case HttpResponse(code, _, entity, _) =>
    entity.discardBytes()
    Future.successful(code.toString())


}

// Run stream
source.via(flow).runWith(Sink.actorRef[Any](processJsonActor,akka.actor.Status.Success(("Completed stream"))))

This works, however the stream closes after 100 HttpRequests (ticks).

What is the cause of this behaviour?

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
DP Greveling
  • 409
  • 5
  • 12

2 Answers2

2

Definitely something to do with outgoingConnectionHttps. This is a low level DSL and there could be some misconfigured setting somewhere which is causing this (although I couldn't figure out which one).

Usage of this DSL is actually discouraged by the docs.

Try using a higher level DSL like cached connection pool

  val flow = Http().cachedHostConnectionPoolHttps[NotUsed]("akka.io").mapAsync(1) {

    // Able to reach the API.
    case (Success(HttpResponse(StatusCodes.OK, _, entity, _)), _) =>
      // Unmarshal the json response.
      Unmarshal(entity).to[String]

    // Failed to reach the API.
    case (Success(HttpResponse(code, _, entity, _)), _) =>
      entity.discardBytes()
      Future.successful(code.toString())

    case (Failure(e), _) ⇒
      throw e
  }

  // Run stream
  source.map(_ → NotUsed).via(flow).runWith(...)
Stefano Bonetti
  • 8,973
  • 1
  • 25
  • 44
  • This solved the problem. As for the `outgoingConnectionHttps`, could it be that the entity is not fully consumed resulting in backpressure and that why it stops? – DP Greveling Sep 28 '17 at 19:02
  • The flow definitely backpressures, so that was my first thought as well. However, the problem persists even with the simplest of flows (`Http().outgoingConnection("jsonplaceholder.typicode.com").map(_.discardEntityBytes())`) which should effectively discard all entities. I'm more inclined to think this is caused by some setting buried somewhere, or maybe even a bug.. – Stefano Bonetti Sep 28 '17 at 19:07
2

A potential issue is that there is no backpressure signal with Sink.actorRef, so the actor's mailbox could be getting full. If the actor, whenever it receives a JsValue object, is doing something that could take a long time, use Sink.actorRefWithAck instead. For example:

val initMessage = "start"
val completeMessage = "done"
val ackMessage = "ack"

source
  .via(flow)
  .runWith(Sink.actorRefWithAck[Any](
    processJsonActor, initMessage, ackMessage, completeMessage))

You would need to change the actor to handle an initMessage and reply to the stream for every stream element with an ackMessage (with sender ! ackMessage). More information on Sink.actorRefWithAck is found here.

Jeffrey Chung
  • 19,319
  • 8
  • 34
  • 54