在Spray中完成请求处理后是否可以安装回调?

时间:2014-04-03 02:58:01

标签: scala akka future spray

我正在尝试从Spray提供大型临时文件。 HTTP请求完成后,我需要删除这些文件。到目前为止,我找不到办法做到这一点......

我正在使用与this或类似的代码:

          respondWithMediaType(`text/csv`) {
            path("somepath" / CsvObjectIdSegment) {
              id =>
                CsvExporter.export(id) { // loan pattern to provide temp file for this request
                  file =>
                    encodeResponse(Gzip) {
                      getFromFile(file)
                    }
                }
            }
          }

所以基本上调用getFromFile来完成Future中的路由。问题是,即使Future已完成,Web请求仍未完成。我尝试编写类似于getFromFile的函数,我会在file.delete()的{​​{1}}中调用onComplete,但它有同样的问题 - Future在完成之前完成如果文件足够大,客户端就完成了下载文件。

以下是来自Spray的Future以供参考:

getFromFile

我无法使用/** * Completes GET requests with the content of the given file. The actual I/O operation is * running detached in a `Future`, so it doesn't block the current thread (but potentially * some other thread !). If the file cannot be found or read the request is rejected. */ def getFromFile(file: File)(implicit settings: RoutingSettings, resolver: ContentTypeResolver, refFactory: ActorRefFactory): Route = getFromFile(file, resolver(file.getName)) ,因为JVM可能暂时不会重新启动,而临时文件会因浪费磁盘空间而停留。

另一方面,这是一个更普遍的问题 - 有没有办法在Spray中安装回调,以便在请求处理完成时可以释放资源或者可以更新统计/日志等。

1 个答案:

答案 0 :(得分:3)

感谢@VladimirPetrosyan指针。以下是我实施它的方法:

路线有:

trait MyService extends HttpService ... with CustomMarshallers {

   override def routeSettings = implicitly[RoutingSettings]

   ...

        get {
          respondWithMediaType(`text/csv`) {
            path("somepath" / CsvObjectIdSegment) {
              filterInstanceId => // just an ObjectId
                val tempResultsFile = CsvExporter.saveCsvResultsToTempFile(filterInstanceId)
                respondWithLastModifiedHeader(tempResultsFile.lastModified) {
                  encodeResponse(Gzip) {
                    complete(tempResultsFile)
                  }
                }
            }
          }

和我混合的特性使得解组产生了分块响应:

import akka.actor._
import spray.httpx.marshalling.{MarshallingContext, Marshaller}
import spray.http.{MessageChunk, ChunkedMessageEnd, HttpEntity, ContentType}
import spray.can.Http
import spray.http.MediaTypes._
import scala.Some
import java.io.{RandomAccessFile, File}
import spray.routing.directives.FileAndResourceDirectives
import spray.routing.RoutingSettings
import math._

trait CustomMarshallers extends FileAndResourceDirectives {

  implicit def actorRefFactory: ActorRefFactory
  implicit def routeSettings: RoutingSettings

  implicit val CsvMarshaller =
    Marshaller.of[File](`text/csv`) {
      (file: File, contentType: ContentType, ctx: MarshallingContext) =>

        actorRefFactory.actorOf {
          Props {
            new Actor with ActorLogging {
              val defaultChunkSize = min(routeSettings.fileChunkingChunkSize, routeSettings.fileChunkingThresholdSize).toInt

              private def getNumberOfChunks(file: File): Int = {
                val randomAccessFile = new RandomAccessFile(file, "r")
                try {
                  ceil(randomAccessFile.length.toDouble / defaultChunkSize).toInt
                } finally {
                  randomAccessFile.close
                }
              }

              private def readChunk(file: File, chunkIndex: Int): String = {
                val randomAccessFile = new RandomAccessFile(file, "r")
                val byteBuffer = new Array[Byte](defaultChunkSize)
                try {
                  val seek = chunkIndex * defaultChunkSize
                  randomAccessFile.seek(seek)
                  val nread = randomAccessFile.read(byteBuffer)
                  if(nread == -1) ""
                  else if(nread < byteBuffer.size) new String(byteBuffer.take(nread))
                  else new String(byteBuffer)
                } finally {
                  randomAccessFile.close
                }
              }

              val chunkNum = getNumberOfChunks(file)

              val responder: ActorRef = ctx.startChunkedMessage(HttpEntity(contentType, ""), Some(Ok(0)))(self)

              sealed case class Ok(seq: Int)

              def stop() = {
                log.debug("Stopped CSV download handler actor.")
                responder ! ChunkedMessageEnd
                file.delete()
                context.stop(self)
              }

              def sendCSV(seq: Int) =
                if (seq < chunkNum)
                  responder ! MessageChunk(readChunk(file, seq)).withAck(Ok(seq + 1))
                else
                  stop()

              def receive = {
                case Ok(seq) =>
                  sendCSV(seq)
                case ev: Http.ConnectionClosed =>
                  log.debug("Stopping response streaming due to {}", ev)
              }
            }
          }
        }
    }
}

创建临时文件,然后actor开始流式传输。每当收到客户端的响应时,它都会发送一个块。每当客户端断开连接时临时文件被删除并且actor被关闭。

这需要您在 spray-can 中运行您的应用程序,如果您在容器中运行它,我认为这将无效。

一些有用的链接: example1example2docs