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

roc.postgresql.ClientDispatcher.scala Maven / Gradle / Ivy

The newest version!
package roc
package postgresql

import cats.data.Xor
import cats.std.all._
import cats.syntax.eq._
import com.twitter.finagle.dispatch.GenSerialClientDispatcher
import com.twitter.finagle.transport.Transport
import com.twitter.finagle.{Service, WriteException}
import com.twitter.util.{Future, Promise, Time}
import roc.postgresql.failures.{PostgresqlServerFailure, PostgresqlStateMachineFailure,
  UnsupportedAuthenticationFailure}
import roc.postgresql.server.{ErrorMessage, PostgresqlMessage, WarningMessage}
import roc.postgresql.transport.{Packet, PacketEncoder}

private[roc] final class ClientDispatcher(trans: Transport[Packet, Packet],
  startup: Startup)
  extends GenSerialClientDispatcher[Request, Result, Packet, Packet](trans) {
  import ClientDispatcher._

  private[this] var backendKeyData: Option[BackendKeyData] = None
  private[this] var mutableParamStatuses: List[ParameterStatus] = Nil

  // this has the potential to be badly constructed if called before
  // the startup phase has been completed
  private[roc] lazy val paramStatuses: Map[String, String] =
    mutableParamStatuses.map(x => (x.parameter, x.value)).toMap

  override def apply(req: Request): Future[Result] = 
    startupPhase.flatMap(_ => super.apply(req))

  /** Performs the Startup phase of a Postgresql Connection.
    *
    * The startup phase is performed once per connection prior to any exchanges
    * between the client and server. Failure to startup renders the service unsuable.
    * The startup phase consists of two separate but sequential  phases 
    * 1. Authentication 2. Server Process setting run time parameters
    * @see [[http://www.postgresql.org/docs/current/static/protocol-flow.html#AEN108589]]
    */
  private[this] val startupPhase: Future[Unit] =
    authenticationPhase.flatMap(_ => serverProcessStartupPhase)

  /** Represents one exchange of FrontendMessage => Future[Message] with the server
    *
    * From the external Client's point of view, Roc maintains the Finagle abstraction of
    * Request => Future[Result]. Internally, there may be many messages passed back and
    * forth between the client and server to build up or complete the Result. This method
    * represents a one-to-one exchange of [[com.github.finagle.roc.postgresql.Messages]]
    * with a Postgresql server.
    */
  private[this] def exchange[A <: FrontendMessage](fm: A)
    (implicit f: PacketEncoder[A]): Future[Message] = trans.write(f(fm)) rescue {
        wrapWriteException
      } before {
        for {
          packet <- trans.read()
          message <- Message.decode(packet) match {
            case Xor.Left(l)  => Future.exception(l)
            case Xor.Right(m) => Future.value(m)
          }
        } yield message
      }

  /**
   * Returns a Future that represents the result of an exchange
   * between the client and server. An exchange does not necessarily entail
   * a single write and read operation. Thus, the result promise
   * is decoupled from the promise that signals a complete exchange.
   * This leaves room for implementing streaming results.
   */
  override protected def dispatch(req: Request, rep: Promise[Result]): Future[Unit] = {
    val query = new Query(req.query)
    for {
      _      <- trans.write(encodePacket(query)).rescue(wrapWriteException)
      signal =  rep.become(readTransport(query, new Promise[Unit]))
    } yield signal 
  }

  private[this] def readTransport(req: Transmission, signal: Promise[Unit]): Future[Result] =
    req match {
      case Query(_) => readQueryTx(signal)
    }

  private[this] def readQueryTx(signal: Promise[Unit]): Future[Result] = {

    type Descriptions          = List[RowDescription]
    type Rows                  = List[DataRow]
    type CommandCompleteString = String
    type Collection            = (Descriptions, Rows, CommandCompleteString)
    def go(xs: Descriptions, ys: Rows, ccStr: CommandCompleteString):
      Future[Collection] = trans.read().flatMap(packet => Message.decode(packet) match {
        case Xor.Right(RowDescription(a,b)) => go(RowDescription(a,b) :: xs, ys, ccStr)
        case Xor.Right(DataRow(a,b))        => go(xs, DataRow(a,b) :: ys, ccStr)
        case Xor.Right(EmptyQueryResponse)  => go(xs, ys, "EmptyQueryResponse")
        case Xor.Right(CommandComplete(x))  => go(xs, ys, x)
        case Xor.Right(ErrorResponse(e))    => 
          Future.exception(new PostgresqlServerFailure(e))
        case Xor.Right(NoticeResponse(_))   => go(xs, ys, ccStr) // throw Notice Responses away
        case Xor.Right(Idle)                => Future.value((xs.reverse, ys.reverse, ccStr))
        case Xor.Right(u) =>
          Future.exception(new PostgresqlStateMachineFailure("Query", u.toString))
        case Xor.Left(l)  => Future.exception(l)
        }
      )

    go(List.empty[RowDescription], List.empty[DataRow], "")
      .map(tuple => {
        val f = signal.setDone()
        new Result(tuple._1, tuple._2, tuple._3)
      })
  }

  /** Closes the connection.
    *
    * We make a best faith effort to inform the Postgresql Server that we are terminating the
    * connection prior to closure.
    * @param deadline the deadline by which the connection must be closed
    */
  override def close(deadline: Time): Future[Unit] =
    trans.write(encodePacket(new Terminate())).ensure {
      super.close(deadline)
      ()
    }

  /** Performs the Authenticaion portion of the Startup Phase
    */
  private[this] def authenticationPhase: Future[Unit] = {
    val sm = StartupMessage(startup.username, startup.database)
    exchange(sm).flatMap(message => message match {
        case AuthenticationOk              => Future.Done
        case AuthenticationClearTxtPasswd  => clearTxtPasswdMachine
        case AuthenticationMD5Passwd(salt) => md5PasswdMachine(salt)
        case AuthenticationKerberosV5      =>
          Future.exception(new UnsupportedAuthenticationFailure("AuthenticationKerberosV5"))
        case AuthenticationSCMCredential   =>
          Future.exception(new UnsupportedAuthenticationFailure("AuthenticationSCMCredential"))
        case AuthenticationSSPI            =>
          Future.exception(new UnsupportedAuthenticationFailure("AuthenticationSSPI"))
        case AuthenticationGSS             =>
          Future.exception(new UnsupportedAuthenticationFailure("AuthenticationGSS"))
        case ErrorResponse(m) => Future.exception(new PostgresqlServerFailure(m))
        case u => Future.exception(new PostgresqlStateMachineFailure("StartupMessage", u.toString))
    })
  }

  private[this] def serverProcessStartupPhase: Future[Unit] = {

    type ParamStatuses = List[ParameterStatus]
    type BKDs = List[BackendKeyData]
    def go(safetyCheck: Int, xs: ParamStatuses, ys: BKDs): Future[(ParamStatuses, BKDs)] = 
      safetyCheck match {
        // TODO - create an Error type for this
        case x if x > 1000 => Future.exception(new Exception())
        case x if x < 1000 => trans.read().flatMap(packet => Message.decode(packet) match {
          case Xor.Left(l) => Future.exception(l)
          case Xor.Right(ParameterStatus(i, j)) => go(safetyCheck + 1, ParameterStatus(i,j) :: xs, ys)
          case Xor.Right(BackendKeyData(i, j)) => go(safetyCheck + 1, xs, BackendKeyData(i, j) :: ys)
          case Xor.Right(Idle) => Future.value((xs, ys))
          case Xor.Right(message) => Future.exception(
            new PostgresqlStateMachineFailure("StartupMessage", message.toString)
          )
        })
      }

    go(0, List.empty[ParameterStatus], List.empty[BackendKeyData]).flatMap(tuple => {
      mutableParamStatuses = tuple._1
      backendKeyData = tuple._2.headOption
      Future.Done
    })
  }

  /** Performs the AuthenticationCleartextPassword startup sequence
    */
  private[this] def clearTxtPasswdMachine: Future[Unit] = {
      val pm = new PasswordMessage(startup.password)
      exchange(pm).flatMap(response => response match {
        case AuthenticationOk => Future.Done
        case ErrorResponse(e) => Future.exception(new PostgresqlServerFailure(e))
        case u => Future.exception(
          new PostgresqlStateMachineFailure("PasswordMessage", u.toString)
        )
      })
    }


  /** Performs the AuthenticationMD5Password startup sequence
    */
  private[this] def md5PasswdMachine(salt: Array[Byte]): Future[Unit] = {
    val encryptedPasswd = PasswordMessage.encryptMD5Passwd(startup.username, startup.password,
      salt)
    val pm = new PasswordMessage(encryptedPasswd)
    exchange(pm).flatMap(response => response match {
      case AuthenticationOk => Future.Done
      case ErrorResponse(e) => Future.exception(new PostgresqlServerFailure(e))
      case u => Future.exception(new PostgresqlStateMachineFailure("PasswordMessage", u.toString))
    })
  }

}
private[roc] object ClientDispatcher {

  private val wrapWriteException: PartialFunction[Throwable, Future[Nothing]] = {
    case exc: Throwable => Future.exception(WriteException(exc))
  }

  def apply(trans: Transport[Packet, Packet], startup: Startup): Service[Request, Result] =
    new ClientDispatcher(trans, startup)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy