All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.squbs.unicomplex.streaming.ServiceRegistry.scala Maven / Gradle / Ivy

There is a newer version: 0.14.0
Show newest version
/*
 *  Copyright 2015 PayPal
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.squbs.unicomplex.streaming

import akka.actor.Actor._
import akka.actor.SupervisorStrategy.Escalate
import akka.actor._
import akka.agent.Agent
import akka.event.LoggingAdapter
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.Uri.Path
import akka.http.scaladsl.server.directives.PathDirectives
import akka.http.scaladsl.server._
import akka.http.scaladsl.{ConnectionContext, Http}
import akka.http.scaladsl.model.HttpRequest
import akka.stream.{BindFailedException, ActorMaterializer}
import akka.stream.TLSClientAuth.{Want, Need}
import akka.stream.scaladsl.Sink
import com.typesafe.config.Config
import org.squbs.pipeline.streaming.{PipelineExtension, PipelineSetting}
import org.squbs.unicomplex._
import org.squbs.unicomplex.streaming.StatsSupport.StatsHolder

import scala.util.{Try, Failure, Success}
import akka.pattern.pipe
import akka.actor.Status.{Failure => ActorFailure}

/**
  * Akka HTTP based [[ServiceRegistryBase]] implementation.
  */
class ServiceRegistry(val log: LoggingAdapter) extends ServiceRegistryBase[Path] {

  case class ServerBindingInfo(serverBinding: Option[ServerBinding], exception: Option[Throwable] = None)

  private var serverBindings = Map.empty[String, ServerBindingInfo] // Service actor and HttpListener actor

  var listenerRoutesVar = Map.empty[String, Agent[Seq[(Path, ActorWrapper, PipelineSetting)]]]

  override protected def listenerRoutes: Map[String, Agent[Seq[(Path, ActorWrapper, PipelineSetting)]]] = listenerRoutesVar

  override protected def listenerRoutes_=[B](newListenerRoutes: Map[String, Agent[Seq[(B, ActorWrapper, PipelineSetting)]]]): Unit =
    listenerRoutesVar = newListenerRoutes.asInstanceOf[Map[String, Agent[Seq[(Path, ActorWrapper, PipelineSetting)]]]]

  override private[unicomplex] def startListener(name: String, config: Config, notifySender: ActorRef)
                                                (implicit context: ActorContext): Receive = {

    val BindConfig(interface, port, localPort, sslContext, needClientAuth) = Try { bindConfig(config) } match {
      case Success(bc) => bc
      case Failure(ex) =>
        serverBindings = serverBindings + (name -> ServerBindingInfo(None, Some(ex)))
        notifySender ! Failure(ex)
        throw ex
    }

    implicit val am = ActorMaterializer()
    import context.system
    import context.dispatcher

    val handler = try { Handler(listenerRoutes(name), localPort) } catch { case e: Throwable =>
      serverBindings = serverBindings + (name -> ServerBindingInfo(None, Some(e)))
      log.error(s"Failed to build streaming flow handler.  System may not function properly.")
      notifySender ! Failure(e)
      throw e
    }

    val uniSelf = context.self
    val serverFlow = sslContext match {
      case Some(sslCtx) =>
        val httpsCtx = ConnectionContext.https(sslCtx, clientAuth = Some { if(needClientAuth) Need else Want })
        Http().bind(interface, port, connectionContext = httpsCtx )

      case None => Http().bind(interface, port)
    }

    val statsHolder = new StatsHolder
    serverFlow.to(Sink.foreach { conn =>

      conn.flow.transform(() => statsHolder.watchRequests())
        .join(handler.flow.transform(() => statsHolder.watchResponses()))
        .run()

    }).run() pipeTo uniSelf

    {
      case sb: ServerBinding =>
        import org.squbs.unicomplex.JMX._
        JMX.register(new ServerStats(name, statsHolder), prefix + serverStats + name)
        serverBindings = serverBindings + (name -> ServerBindingInfo(Some(sb)))
        notifySender ! Ack
        uniSelf ! HttpBindSuccess
      case ActorFailure(ex) if ex.isInstanceOf[BindFailedException] =>
        serverBindings = serverBindings + (name -> ServerBindingInfo(None, Some(ex)))
        log.error(s"Failed to bind listener $name. Cleaning up. System may not function properly.")
        notifySender ! ex
        uniSelf ! HttpBindFailed
    }
  }

  override private[unicomplex] def registerContext(listeners: Iterable[String], webContext: String, servant: ActorWrapper,
                                                   ps: PipelineSetting)(implicit context: ActorContext) {

    // Calling this here just to see if it would throw an exception.
    // We do not want it to be thrown at materialization time, instead face it during startup.
    PipelineExtension(context.system).getFlow(ps)

    listeners foreach { listener =>
      val agent = listenerRoutes(listener)
      agent.send {
        currentSeq =>
          merge(currentSeq, webContext, servant, ps, {
            log.warning(s"Web context $webContext already registered on $listener. Override existing registration.")
          })
      }
    }
  }

  override private[unicomplex] def isListenersBound = serverBindings.size == listenerRoutes.size

  case class Unbound(sb: ServerBinding)

  override private[unicomplex] def shutdownState: Receive = {
    case Unbound(sb) =>
      serverBindings = serverBindings.filterNot {
        case (_, ServerBindingInfo(Some(`sb`), None)) => true
        case _ => false
      }
  }

  override private[unicomplex] def listenerTerminated(listenerActor: ActorRef): Unit =
    log.warning(s"Unexpected serviceRegistry.listenerTerminated(${listenerActor.toString()}) in streaming use case.")

  override private[unicomplex] def stopAll()(implicit context: ActorContext): Unit = {

    import context.dispatcher
    import context.system
    val uniSelf = context.self

//    Http().shutdownAllConnectionPools() andThen { case _ =>
      serverBindings foreach {
        case (name, ServerBindingInfo(Some(sb), None)) =>
          listenerRoutes(name)() foreach {case (_, aw, _) => aw.actor ! PoisonPill}
          listenerRoutes = listenerRoutes - name
          sb.unbind() andThen { case _ => uniSelf ! Unbound(sb) }
          if (listenerRoutes.isEmpty) {
            Http().shutdownAllConnectionPools()
            // TODO Unregister "Listeners" JMX Bean.
          }
        // TODO Unregister "ServerStats,Listener=name" JMX Bean
        case _ =>
      }
//    }
  }

  override private[unicomplex] def isAnyFailedToInitialize: Boolean = serverBindings.values exists (_.exception.nonEmpty)

  override private[unicomplex] def isShutdownComplete: Boolean = serverBindings.isEmpty

  override protected def pathCompanion(s: String): Path = Path(s)

  override protected def pathLength(p: Path) = p.length

  override protected def listenerStateMXBean(): ListenerStateMXBean = {
    new ListenerStateMXBean {
      import scala.collection.JavaConversions._
      override def getListenerStates: java.util.List[ListenerState] = {
        serverBindings map { case (name, ServerBindingInfo(sb, exception)) =>
          ListenerState(name, sb.map(_ => "Success").getOrElse("Failed"), exception.getOrElse("").toString)
        } toSeq
      }
    }
  }
}

private[unicomplex] class RouteActor(webContext: String, clazz: Class[RouteDefinition])
  extends Actor with ActorLogging {

  import scala.concurrent.duration._

  override val supervisorStrategy =
    OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
      case e: Exception =>
        log.error(s"Received ${e.getClass.getName} with message ${e.getMessage} from ${sender().path}")
        Escalate
    }

  def actorRefFactory = context

  val routeDef =
    try {
      val d = RouteDefinition.startRoutes {
        WebContext.createWithContext[RouteDefinition](webContext) {
          clazz.newInstance
        }
      }
      context.parent ! Initialized(Success(None))
      d
    } catch {
      case e: Exception =>
        log.error(e, s"Error instantiating route from {}: {}", clazz.getName, e)
        context.parent ! Initialized(Failure(e))
        context.stop(self)
        RouteDefinition.startRoutes(new RejectRoute)
    }

  // TODO Hold on..  Why do we directly need a materializer here..  Should be passed down..
  implicit val am = ActorMaterializer()
  implicit val rejectionHandler:RejectionHandler = routeDef.rejectionHandler.getOrElse(RejectionHandler.default)
  implicit val exceptionHandler:ExceptionHandler = routeDef.exceptionHandler.getOrElse(PartialFunction.empty[Throwable, Route])

  lazy val route = if (webContext.nonEmpty) {
    PathDirectives.pathPrefix(PathMatchers.separateOnSlashes(webContext)) {routeDef.route}
  } else {
    // don't append pathPrefix if webContext is empty, won't be null due to the top check
    routeDef.route
  }

  import akka.pattern.pipe
  import context.dispatcher

  def receive: Receive = {
    case request: HttpRequest =>
      val origSender = sender()
      Route.asyncHandler(route).apply(request) pipeTo origSender
  }
}

object RouteDefinition {

  private[unicomplex] val localContext = new ThreadLocal[Option[ActorContext]] {
    override def initialValue(): Option[ActorContext] = None
  }

  def startRoutes[T](fn: => T)(implicit context: ActorContext): T = {
    localContext.set(Some(context))
    val r = fn
    localContext.set(None)
    r
  }
}

trait RouteDefinition extends Directives {
  protected implicit final val context: ActorContext = RouteDefinition.localContext.get.get
  implicit final lazy val self = context.self

  def route: Route

  def rejectionHandler: Option[RejectionHandler] = None

  def exceptionHandler: Option[ExceptionHandler] = None
}

class RejectRoute extends RouteDefinition {

  val route: Route = reject
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy