|
1 | 1 | package spark.jobserver
|
2 | 2 |
|
| 3 | +import java.util.NoSuchElementException |
3 | 4 | import java.util.concurrent.TimeUnit
|
| 5 | +import javax.net.ssl.SSLContext |
4 | 6 |
|
5 |
| -import akka.actor.{ ActorSystem, ActorRef } |
| 7 | +import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props} |
6 | 8 | import akka.pattern.ask
|
7 | 9 | import akka.util.Timeout
|
8 |
| -import com.typesafe.config.{ Config, ConfigFactory, ConfigException, ConfigRenderOptions } |
9 |
| -import java.util.NoSuchElementException |
10 |
| -import javax.net.ssl.SSLContext |
11 |
| -import ooyala.common.akka.web.{ WebService, CommonRoutes } |
| 10 | +import com.typesafe.config.{Config, ConfigException, ConfigFactory, ConfigRenderOptions} |
| 11 | +import ooyala.common.akka.web.JsonUtils.AnyJsonFormat |
| 12 | +import ooyala.common.akka.web.{CommonRoutes, WebService} |
| 13 | +import org.apache.shiro.SecurityUtils |
| 14 | +import org.apache.shiro.config.IniSecurityManagerFactory |
12 | 15 | import org.joda.time.DateTime
|
13 | 16 | import org.slf4j.LoggerFactory
|
14 |
| -import spark.jobserver.util.SparkJobUtils |
15 |
| -import spark.jobserver.util.SSLContextFactory |
16 |
| -import spark.jobserver.routes.DataRoutes |
17 |
| -import scala.concurrent.{Await, ExecutionContext, Future} |
18 |
| -import scala.util.Try |
19 |
| -import spark.jobserver.io.JobInfo |
20 | 17 | import spark.jobserver.auth._
|
21 |
| -import spray.http.HttpResponse |
22 |
| -import spray.http.MediaTypes |
23 |
| -import spray.http.StatusCodes |
| 18 | +import spark.jobserver.io.JobInfo |
| 19 | +import spark.jobserver.routes.DataRoutes |
| 20 | +import spark.jobserver.util.{SSLContextFactory, SparkJobUtils} |
| 21 | +import spray.can.Http |
| 22 | +import spray.http._ |
24 | 23 | import spray.httpx.SprayJsonSupport.sprayJsonMarshaller
|
| 24 | +import spray.io.ServerSSLEngineProvider |
25 | 25 | import spray.json.DefaultJsonProtocol._
|
26 |
| -import spray.routing.{ HttpService, Route, RequestContext } |
27 | 26 | import spray.routing.directives.AuthMagnet
|
28 |
| -import spray.io.ServerSSLEngineProvider |
29 |
| -import org.apache.shiro.config.IniSecurityManagerFactory |
30 |
| -import org.apache.shiro.mgt.SecurityManager |
31 |
| -import org.apache.shiro.SecurityUtils |
| 27 | +import spray.routing.{HttpService, RequestContext, Route} |
| 28 | + |
| 29 | +import scala.concurrent.{Await, ExecutionContext, Future} |
| 30 | +import scala.util.Try |
| 31 | + |
32 | 32 |
|
33 | 33 | object WebApi {
|
34 | 34 | val StatusKey = "status"
|
35 | 35 | val ResultKey = "result"
|
| 36 | + val ResultKeyStartBytes = "{\n".getBytes |
| 37 | + val ResultKeyEndBytes = "}".getBytes |
| 38 | + val ResultKeyBytes = ("\"" + ResultKey + "\":").getBytes |
36 | 39 |
|
37 | 40 | def badRequest(ctx: RequestContext, msg: String) {
|
38 | 41 | ctx.complete(StatusCodes.BadRequest, errMap(msg))
|
@@ -61,6 +64,14 @@ object WebApi {
|
61 | 64 | Map(ResultKey -> result)
|
62 | 65 | }
|
63 | 66 |
|
| 67 | + def resultToByteIterator(jobReport: Map[String, Any], result: Iterator[_]): Iterator[_] = { |
| 68 | + ResultKeyStartBytes.toIterator ++ |
| 69 | + (jobReport.map(t => Seq(AnyJsonFormat.write(t._1).toString(), |
| 70 | + AnyJsonFormat.write(t._2).toString()).mkString(":") ).mkString(",") ++ |
| 71 | + (if(jobReport.nonEmpty) "," else "")).getBytes().toIterator ++ |
| 72 | + ResultKeyBytes.toIterator ++ result ++ ResultKeyEndBytes.toIterator |
| 73 | + } |
| 74 | + |
64 | 75 | def formatException(t: Throwable): Any =
|
65 | 76 | if (t.getCause != null) {
|
66 | 77 | Map("message" -> t.getMessage,
|
@@ -112,6 +123,12 @@ class WebApi(system: ActorSystem,
|
112 | 123 | val DefaultJobLimit = 50
|
113 | 124 | val StatusKey = "status"
|
114 | 125 | val ResultKey = "result"
|
| 126 | + val ResultChunkSize = if (config.hasPath("spark.jobserver.result-chunk-size")) { |
| 127 | + config.getBytes("spark.jobserver.result-chunk-size").toInt |
| 128 | + } |
| 129 | + else { |
| 130 | + 100 * 1024 |
| 131 | + } |
115 | 132 |
|
116 | 133 | val contextTimeout = SparkJobUtils.getContextTimeout(config)
|
117 | 134 | val bindAddress = config.getString("spark.jobserver.bind-address")
|
@@ -228,6 +245,7 @@ class WebApi(system: ActorSystem,
|
228 | 245 | */
|
229 | 246 | def contextRoutes: Route = pathPrefix("contexts") {
|
230 | 247 | import ContextSupervisor._
|
| 248 | + |
231 | 249 | import collection.JavaConverters._
|
232 | 250 | // user authentication
|
233 | 251 | authenticate(authenticator) { authInfo =>
|
@@ -384,7 +402,12 @@ class WebApi(system: ActorSystem,
|
384 | 402 | val resultFuture = jobInfo ? GetJobResult(jobId)
|
385 | 403 | resultFuture.map {
|
386 | 404 | case JobResult(_, result) =>
|
387 |
| - ctx.complete(jobReport ++ resultToTable(result)) |
| 405 | + result match { |
| 406 | + case s: Stream[_] => |
| 407 | + sendStreamingResponse(ctx, ResultChunkSize, |
| 408 | + resultToByteIterator(jobReport, s.toIterator)) |
| 409 | + case _ => ctx.complete(jobReport ++ resultToTable(result)) |
| 410 | + } |
388 | 411 | case _ =>
|
389 | 412 | ctx.complete(jobReport)
|
390 | 413 | }
|
@@ -466,7 +489,13 @@ class WebApi(system: ActorSystem,
|
466 | 489 | JobManagerActor.StartJob(appName, classPath, jobConfig, events))(timeout)
|
467 | 490 | respondWithMediaType(MediaTypes.`application/json`) { ctx =>
|
468 | 491 | future.map {
|
469 |
| - case JobResult(_, res) => ctx.complete(resultToTable(res)) |
| 492 | + case JobResult(_, res) => { |
| 493 | + res match { |
| 494 | + case s: Stream[_] => sendStreamingResponse(ctx, ResultChunkSize, |
| 495 | + resultToByteIterator(Map.empty, s.toIterator)) |
| 496 | + case _ => ctx.complete(resultToTable(res)) |
| 497 | + } |
| 498 | + } |
470 | 499 | case JobErroredOut(_, _, ex) => ctx.complete(errMap(ex, "ERROR"))
|
471 | 500 | case JobStarted(jobId, context, _) =>
|
472 | 501 | jobInfo ! StoreJobConfig(jobId, postedJobConfig)
|
@@ -504,6 +533,43 @@ class WebApi(system: ActorSystem,
|
504 | 533 | }
|
505 | 534 | }
|
506 | 535 |
|
| 536 | + private def sendStreamingResponse(ctx: RequestContext, |
| 537 | + chunkSize: Int, |
| 538 | + byteIterator: Iterator[_]): Unit = { |
| 539 | + // simple case class whose instances we use as send confirmation message for streaming chunks |
| 540 | + case class Ok(remaining: Iterator[_]) |
| 541 | + actorRefFactory.actorOf { |
| 542 | + Props { |
| 543 | + new Actor with ActorLogging { |
| 544 | + // we use the successful sending of a chunk as trigger for sending the next chunk |
| 545 | + ctx.responder ! ChunkedResponseStart( |
| 546 | + HttpResponse(entity = HttpEntity(MediaTypes.`application/json`, |
| 547 | + byteIterator.take(chunkSize).map { |
| 548 | + case c: Byte => c |
| 549 | + }.toArray))).withAck(Ok(byteIterator)) |
| 550 | + |
| 551 | + def receive: Receive = { |
| 552 | + case Ok(remaining) => |
| 553 | + val arr = remaining.take(chunkSize).map { |
| 554 | + case c: Byte => c |
| 555 | + }.toArray |
| 556 | + if (arr.nonEmpty) { |
| 557 | + ctx.responder ! MessageChunk(arr).withAck(Ok(remaining)) |
| 558 | + } |
| 559 | + else { |
| 560 | + ctx.responder ! ChunkedMessageEnd |
| 561 | + context.stop(self) |
| 562 | + } |
| 563 | + case ev: Http.ConnectionClosed => { |
| 564 | + log.warning("Stopping response streaming due to {}", ev) |
| 565 | + context.stop(self) |
| 566 | + } |
| 567 | + } |
| 568 | + } |
| 569 | + } |
| 570 | + } |
| 571 | + } |
| 572 | + |
507 | 573 | override def timeoutRoute: Route =
|
508 | 574 | complete(500, errMap("Request timed out. Try using the /jobs/<jobID>, /jobs APIs to get status/results"))
|
509 | 575 |
|
|
0 commit comments