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

com.codemettle.akkasolr.client.ClientConnection.scala Maven / Gradle / Ivy

/*
 * ClientConnection.scala
 *
 * Updated: Oct 16, 2014
 *
 * Copyright (c) 2014, CodeMettle
 */
package com.codemettle.akkasolr
package client

import com.codemettle.akkasolr.Solr.SolrOperation
import com.codemettle.akkasolr.client.ClientConnection.{RequestQueue, fsm}
import com.codemettle.akkasolr.solrtypes.SolrQueryResponse
import com.codemettle.akkasolr.util.Util

import akka.actor._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri}
import akka.http.scaladsl.settings.{ClientConnectionSettings, ConnectionPoolSettings, ParserSettings}
import akka.stream.scaladsl.{Keep, Sink, Source}
import akka.stream.{Materializer, OverflowStrategy, QueueOfferResult, StreamTcpException}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success}

/**
 * @author steven
 *
 */
private[akkasolr] object ClientConnection {
    def props(uri: Uri, username: Option[String], password: Option[String])(implicit mat: Materializer) =
        Props(new ClientConnection(uri, username, password))

    object fsm {
        sealed trait State
        case object Disconnected extends State
        case object TestingConnection extends State
        case object Connected extends State

        case class CCData(pingTriesRemaining: Int = 0)
    }

    case class RequestQueue(baseUri: Uri, connPoolSettings: ConnectionPoolSettings)
                           (implicit sys: ActorSystem, mat: Materializer) {
        private val connPool = {
            if (baseUri.isSsl)
                Http().cachedHostConnectionPoolHttps[Promise[HttpResponse]](baseUri.authority.host.address,
                    baseUri.authority.port, settings = connPoolSettings)
            else
                Http().cachedHostConnectionPool[Promise[HttpResponse]](baseUri.authority.host.address,
                    baseUri.authority.port, connPoolSettings)
        }

        private val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](Solr.Client.requestQueueSize, OverflowStrategy.dropNew)
          .via(connPool)
          .toMat(Sink.foreach {
              case ((Success(resp), p)) => p.success(resp)
              case ((Failure(t), p)) => p.failure(t)
          })(Keep.left)
          .run()

        def shutdown(): Unit = queue.complete()

        def queueRequest(req: HttpRequest)(implicit ec: ExecutionContext): Future[HttpResponse] = {
            val p = Promise[HttpResponse]()
            queue.offer(req -> p) flatMap {
                case QueueOfferResult.Enqueued => p.future
                case QueueOfferResult.Failure(t) => Future.failed(t)
                case QueueOfferResult.Dropped => Future.failed(new Exception(s"Request queue for $baseUri is full"))
                case QueueOfferResult.QueueClosed => Future.failed(new Exception(s"Request queue for $baseUri is closed"))
            }
        }
    }
}

private[akkasolr] class ClientConnection(baseUri: Uri, username: Option[String], password: Option[String])
                                        (implicit mat: Materializer)
    extends FSM[fsm.State, fsm.CCData] with ActorLogging {

    startWith(fsm.Disconnected, fsm.CCData())

    implicit val system: ActorSystem = context.system

    private val stasher = context.actorOf(ConnectingStasher.props, "stasher")

    private val actorName = Util actorNamer "request"

    private val requestQueue = RequestQueue(baseUri, connPoolSettings)

    override def postStop(): Unit = {
        super.postStop()

        requestQueue.shutdown()
    }

    private def parserSettings = {
        ParserSettings(context.system).withMaxChunkSize(Solr.Client.maxChunkSize)
          .withMaxContentLength(Solr.Client.maxContentLength)
    }

    private def connSettings = {
        ClientConnectionSettings(context.system).withParserSettings(parserSettings)
    }

    private def connPoolSettings =
        ConnectionPoolSettings(context.system).withConnectionSettings(connSettings)

    private def serviceRequest(request: SolrOperation, requestor: ActorRef, timeout: FiniteDuration) = {
        def props = RequestHandler.props(baseUri, username, password, requestQueue, requestor, request, timeout)
        context.actorOf(props, actorName.next())
    }

    private def pingServer() = {
        val req = Solr.Ping()
        serviceRequest(req, self, req.requestTimeout)
    }

    whenUnhandled {
        case Event(req: SolrOperation, _) =>
            val to = req.requestTimeout
            stasher ! ConnectingStasher.WaitingRequest(sender(), req, to, to)
            stay()

        case Event(ConnectingStasher.StashedRequest(act, req, remainingTimeout, origTimeout), _) =>
            // if we get this message, it means that we successfully connected, asked for stashed connections, and
            // then got disconnected before processing them all
            stasher ! ConnectingStasher.WaitingRequest(act, req, remainingTimeout, origTimeout)
            stay()

        case Event(m, _) =>
            stay() replying Status.Failure(Solr.InvalidRequest(m.toString))
    }

    private def handleConnExc: StateFunction = {
        case Event(Status.Failure(e: StreamTcpException), _) =>
            log.error(e, "Couldn't connect to {}", baseUri)

            stasher ! ConnectingStasher.ErrorOutAllWaiting(e)

            goto(fsm.Disconnected) using fsm.CCData()
    }

    when(fsm.Disconnected) {
        case Event(m: SolrOperation, _) =>
            val to = m.requestTimeout

            stasher ! ConnectingStasher.WaitingRequest(sender(), m, to, to)

            goto(fsm.TestingConnection) using fsm.CCData(pingTriesRemaining = 4)
    }

    onTransition {
        case _ -> fsm.TestingConnection => pingServer()
    }

    when(fsm.TestingConnection) (handleConnExc orElse {
        case Event(Status.Failure(Solr.RequestTimedOut(_)), data) if data.pingTriesRemaining > 0 =>
            log debug "didn't get response from server ping, retrying"
            pingServer()
            stay() using data.copy(pingTriesRemaining = data.pingTriesRemaining - 1)

        case Event(Status.Failure(Solr.RequestTimedOut(_)), _) =>
            log warning "Never got a response from server ping, aborting"
            val exc = Solr.ConnectionException("No response to server pings")
            stasher ! ConnectingStasher.ErrorOutAllWaiting(exc)
            goto(fsm.Disconnected) using fsm.CCData()

        case Event(qr: SolrQueryResponse, _) =>
            log.debug("got response to ping {}, check status etc?", qr)
            goto(fsm.Connected)

        case Event(Status.Failure(t), _) =>
            log.error(t, "Couldn't ping server")
            stasher ! ConnectingStasher.ErrorOutAllWaiting(t)
            goto(fsm.Disconnected) using fsm.CCData()
    })

    onTransition {
        case fsm.TestingConnection -> fsm.Connected => stasher ! ConnectingStasher.FlushWaitingRequests
    }

    when(fsm.Connected) {
        case Event(m: SolrOperation, _) =>
            serviceRequest(m, sender(), m.requestTimeout)
            stay()

        case Event(ConnectingStasher.StashedRequest(act, req: Solr.SolrOperation, remaining, _), _) =>
            serviceRequest(req, act, remaining)
            stay()
    }

    initialize()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy