3

I have data being pulled from Reactive Mongo that I need to push through a Spray Rest API. I had hoped to do this with Chunked Responses. However I have discovered that the Enumerator that comes back from Reactive Mongo is capable of pushing through Spray faster than the network connection can handle. The result is that the connection is terminated.

I was able to resolve this problem using the Spray Ack feature in an intermediate actor. This along with a Blocking Await allowed me to create backpressure on the Enumerator. However I don't really want the Await. I would like to figure out a way to stream the data through Spray in a non-blocking fashion.

Is this possible? I have few ideas that might work if I can fill in the missing pieces.

1) Create back pressure on the Enumerator in a non-blocking fashion (no idea how to do this. Suggestions?)

2) Break the enumerator into smaller enumerators. Start consuming each enumerator only once the previous one has completed. I can do this using an actor. What I lack here is a way to break the larger enumerator into smaller enumerators.

3) Use something like the "Enumeratee.take" method. Where I would take some number of records from the Enumerator, then when I am ready, take some more. This is really just the same solution as 2) but from a slightly different perspective. This would require the enumerator to maintain state however. Is there a way to use the Enumeratee.take multiple times against the same enumerator without restarting from the beginning each time?

Can anyone offer any alternate suggestions that might work? Or if it is not possible please let me know.

I am using Play Enumerators 2.3.5

Wade
  • 346
  • 2
  • 12

2 Answers2

1

I think the idea is you implement an Iteratee whose fold method only calls the supplied callback after receiving the Spray Ack. Something like:

def handleData(input: Input[String]) = new Iteratee[String] {
  def fold[B](folder: Step[Error, String] => Future[B]): Future[B] = {
    (sprayActor ? input).flatMap {
      case success => folder(Cont(handleData))
      case error => folder(Error(...))
      case done => ...
    }
  }
}

val initialIteratee = new Iteratee[String] {
  def fold[B](folder: Step[Error, String] => Future[B]) = folder(Cont(handleData))
}

enumerator.run(initialIteratee)

This should be nonblocking but ensures the next chunk is only sent after the previous chunk has succeeded.

lmm
  • 17,386
  • 3
  • 26
  • 37
  • I agree, this seems promising and I have pursued this exact idea before reading your post. However I could never figure out the syntax. Where does the "handleData" in your example get called? Where do we get that initial "Input" value? I am not sure in Play 2.3.5 how to implement this. – Wade Oct 28 '14 at 14:21
  • I think maybe you need a separate initial iteratee; I've added one to the code. – lmm Oct 28 '14 at 14:50
  • Yes, I believe I have it solved now. Your post definitely helped. I had to add an initial iteratee and alter a few other things to get it to work but I think I have it now. Thanks for your assistance. – Wade Oct 28 '14 at 14:54
  • Will there be an issue with having the "handleData" method be recursive? I.E. if there was a lot of data would this eventually cause a stack overflow? Or is there some optimization under the hood to prevent this? – Wade Oct 28 '14 at 23:14
  • I would expect `Iteratee` to do something clever with the `Future[B]`s, but I don't know; I can only suggest testing it. – lmm Oct 29 '14 at 00:00
0

After a fair amount of experimentation (and help from stackoverflow) I was able to figure out a solution that seems to work. It uses Spray Chunked Responses and builds an iteratee around that.

The relevant code snippets are included here:

ChunkedResponder.scala

package chunkedresponses

import akka.actor.{Actor, ActorRef}
import spray.http.HttpHeaders.RawHeader
import spray.http._

object ChunkedResponder {
  case class Chunk(data: HttpData)
  case object Shutdown
  case object Ack
}

class ChunkedResponder(contentType: ContentType, responder: ActorRef) extends Actor {
  import ChunkedResponder._
  def receive:Receive = {
    case chunk: Chunk =>
      responder.forward(ChunkedResponseStart(HttpResponse(entity = HttpEntity(contentType, chunk.data))).withAck(Ack))
      context.become(chunking)
    case Shutdown =>
      responder.forward(HttpResponse(headers = List(RawHeader("Content-Type", contentType.value))).withAck(Ack))
      context.stop(self)
  }

  def chunking:Receive = {
    case chunk: Chunk =>
      responder.forward(MessageChunk(chunk.data).withAck(Ack))
    case Shutdown =>
      responder.forward(ChunkedMessageEnd().withAck(Ack))
      context.stop(self)
  }
}

ChunkIteratee.scala

package chunkedresponses

import akka.actor.ActorRef
import akka.util.Timeout
import akka.pattern.ask
import play.api.libs.iteratee.{Done, Step, Input, Iteratee}
import spray.http.HttpData
import scala.concurrent.duration._

import scala.concurrent.{ExecutionContext, Future}

class ChunkIteratee(chunkedResponder: ActorRef) extends Iteratee[HttpData, Unit] {
  import ChunkedResponder._
  private implicit val timeout = Timeout(30.seconds)

  def fold[B](folder: (Step[HttpData, Unit]) => Future[B])(implicit ec: ExecutionContext): Future[B] = {
    def waitForAck(future: Future[Any]):Iteratee[HttpData, Unit] = Iteratee.flatten(future.map(_ => this))

    def step(input: Input[HttpData]):Iteratee[HttpData, Unit] = input match {
      case Input.El(e) => waitForAck(chunkedResponder ? Chunk(e))
      case Input.Empty => waitForAck(Future.successful(Unit))
      case Input.EOF =>
        chunkedResponder ! Shutdown
        Done(Unit, Input.EOF)
    }

    folder(Step.Cont(step))
  }
}

package.scala

import akka.actor.{ActorContext, ActorRefFactory, Props}
import play.api.libs.iteratee.Enumerator
import spray.http.{HttpData, ContentType}
import spray.routing.RequestContext

import scala.concurrent.ExecutionContext

package object chunkedresponses {
  implicit class ChunkedRequestContext(requestContext: RequestContext) {
    def completeChunked(contentType: ContentType, enumerator: Enumerator[HttpData])
                       (implicit executionContext: ExecutionContext, actorRefFactory: ActorRefFactory) {
      val chunkedResponder = actorRefFactory.actorOf(Props(new ChunkedResponder(contentType, requestContext.responder)))
      val iteratee = new ChunkIteratee(chunkedResponder)
      enumerator.run(iteratee)
    }
  }
}
Wade
  • 346
  • 2
  • 12
  • 1
    please put the important portion of your answer in SO rather than linking to your blog (which no longer exists) – alvi Nov 28 '16 at 16:23
  • Updated to include the code snippets from the original blog post that is now dead. – Wade Feb 21 '17 at 15:32