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

com.avsystem.commons.redis.RedisNodeClient.scala Maven / Gradle / Ivy

package com.avsystem.commons
package redis

import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.atomic.AtomicLong

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.pattern.ask
import akka.util.Timeout
import com.avsystem.commons.concurrent.RunInQueueEC
import com.avsystem.commons.redis.actor.ConnectionPoolActor.QueuedConn
import com.avsystem.commons.redis.actor.RedisConnectionActor.PacksResult
import com.avsystem.commons.redis.actor.RedisOperationActor.OpResult
import com.avsystem.commons.redis.actor.{ConnectionPoolActor, RedisConnectionActor, RedisOperationActor}
import com.avsystem.commons.redis.config.{ConfigDefaults, ConnectionConfig, ExecutionConfig, NodeConfig}
import com.avsystem.commons.redis.exception.{ClientStoppedException, NodeInitializationFailure, NodeRemovedException, TooManyConnectionsException}

import scala.collection.mutable.ArrayBuffer
import scala.concurrent.duration._

/**
  * Redis client implementation for a single Redis node using a connection pool. Connection pool size is constant
  * and batches and operations are distributed over connections using round-robin scheme. Connections are automatically
  * reconnected upon failure (possibly with an appropriate delay, see [[config.NodeConfig NodeConfig]] for details).
  */
final class RedisNodeClient(
  val address: NodeAddress = NodeAddress.Default,
  val config: NodeConfig = NodeConfig(),
  val managed: Boolean = false // true when used internally by RedisClusterClient or RedisMasterSlaveClient
)(implicit system: ActorSystem) extends RedisClient with RedisNodeExecutor { client =>

  private def newConnection(i: Int): ActorRef = {
    val connConfig: ConnectionConfig = config.connectionConfigs(i)
    // If this node connects to cluster master, initial connection attempt is not required to be successful
    // This is because in in RedisClusterClient only initial connections to seed nodes must be immediately successful
    val props = Props(new RedisConnectionActor(address, connConfig))
      .withDispatcher(ConfigDefaults.Dispatcher)
    val connection = connConfig.actorName.fold(system.actorOf(props))(system.actorOf(props, _))
    connection ! RedisConnectionActor.Open(!managed, connInitPromises(i))
    connection
  }

  private val connInitPromises = ArrayBuffer.fill(config.poolSize)(Promise[Unit]())
  private val connections = (0 until config.poolSize).iterator.map(newConnection).toArray
  private val index = new AtomicLong(0)

  private val blockingConnectionQueue = new ConcurrentLinkedDeque[QueuedConn]
  private val blockingConnectionPool =
    system.actorOf(Props(new ConnectionPoolActor(address, config, blockingConnectionQueue)))

  private def newBlockingConnection(): Future[ActorRef] =
    blockingConnectionPool.ask(ConnectionPoolActor.CreateNewConnection)(Timeout(1.second)).mapNow {
      case ConnectionPoolActor.NewConnection(connection) =>
        connection
      case ConnectionPoolActor.Full =>
        throw new TooManyConnectionsException(config.maxBlockingPoolSize)
    }

  private def releaseBlockingConnection(connection: ActorRef): Unit =
    blockingConnectionQueue.offerFirst(QueuedConn(connection, System.nanoTime()))

  private def onBlockingConnection[T](operation: ActorRef => Future[T]): Future[T] = {
    def operationWithRelease(connection: ActorRef): Future[T] =
      operation(connection).andThenNow { case _ => releaseBlockingConnection(connection) }
    blockingConnectionQueue.pollFirst().opt.map(qc => operationWithRelease(qc.conn))
      .getOrElse(newBlockingConnection().flatMapNow(operationWithRelease))
  }

  @volatile private[this] var initSuccess = false
  @volatile private[this] var failure = Opt.empty[Throwable]

  private val overallInitFuture: Future[Any] = {
    implicit val executionContext: ExecutionContext = new RunInQueueEC
    val initOpFuture = executeOp(connections(0), config.initOp)(config.initTimeout)
      .transform(identity, new NodeInitializationFailure(_))
    val res = Future.traverse(connInitPromises)(_.future).flatMap(_ => initOpFuture)
    res.onComplete {
      case Success(_) =>
        initSuccess = true
      case Failure(cause) =>
        failure = cause.opt
        close()
    }
    res
  }

  private def ifReady[T](code: => Future[T]): Future[T] =
    failure.fold(if (initSuccess) code else overallInitFuture.flatMapNow(_ => code))(Future.failed)

  private def nextConnection() =
    connections((index.getAndIncrement() % config.poolSize).toInt)

  private[redis] def executeRaw(packs: RawCommandPacks)(implicit timeout: Timeout): Future[PacksResult] =
    ifReady {
      val maxBlockMillis = packs.maxBlockingMillis
      if (maxBlockMillis == 0)
        nextConnection().ask(packs)(timeout, Actor.noSender).mapNow { case pr: PacksResult => pr }
      else {
        val adjTimeout = Timeout((maxBlockMillis.millis + 1.second) max timeout.duration)
        onBlockingConnection(_.ask(packs)(adjTimeout, Actor.noSender)).mapNow { case pr: PacksResult => pr }
      }
    }

  /**
    * Notifies the [[RedisNodeClient]] that its node is no longer a master in Redis Cluster and.
    * The client stops itself as a result and fails any pending unsent requests with
    * [[exception.NodeRemovedException]].
    */
  private[redis] def nodeRemoved(): Unit = {
    val cause = new NodeRemovedException(address)
    failure = cause.opt
    connections.foreach(_ ! RedisConnectionActor.Close(cause, stop = true))
    blockingConnectionPool ! ConnectionPoolActor.Close(cause, stop = true)
  }

  def executionContext: ExecutionContext =
    system.dispatcher

  /**
    * Executes a [[RedisBatch]] on this client by sending its commands to the Redis node in a single network
    * message (technically, a single `akka.io.Tcp.Write` message). Therefore it's also naturally guaranteed that
    * all commands in a batch are executed on the same connection.
    *
    * Note that even though connection used by [[RedisNodeClient]] are automatically reconnected, it's still possible
    * that an error is returned for some batches that were executed around the time connection failure happened.
    * To be precise, [[exception.ConnectionClosedException ConnectionClosedException]]
    * is returned for batches that were sent through the connection but the connection failed before
    * a response could be received. There is no way to safely retry such batches because it is unknown whether Redis
    * node actually received and executed them or not.
    *
    * Execution of each command in the batch or the whole batch may fail due to following reasons:
    * 
    *
  • [[exception.ForbiddenCommandException ForbiddenCommandException]] when trying to execute command not * supported by this client type
  • *
  • [[exception.ErrorReplyException ErrorReplyException]] when Redis server replies with an error for some command. * In particular, if this client was obtained from [[RedisClusterClient]] then every keyed command may fail with * cluster redirection. You can pattern-match redirection errors using [[RedirectionException]] extractor object. *
  • *
  • [[exception.UnexpectedReplyException UnexpectedReplyException]] when Redis server replies with something * unexpected by a decoder of some command
  • *
  • [[exception.ConnectionClosedException ConnectionClosedException]] when connection is closed or * reset (the client reconnects automatically after connection failure but commands that were in the middle of * execution may still fail)
  • *
  • [[exception.WriteFailedException WriteFailedException]] when a network write * failed
  • *
  • [[exception.NodeRemovedException NodeRemovedException]] when this client was obtained from * [[RedisClusterClient]] and Redis node that it's connected to is no longer a master
  • *
*/ def executeBatch[A](batch: RedisBatch[A], config: ExecutionConfig): Future[A] = executeRaw(batch.rawCommandPacks.requireLevel(RawCommand.Level.Node, "NodeClient"))(config.responseTimeout) .map(result => batch.decodeReplies(result))(config.decodeOn) /** * Executes a [[RedisOp]] on this client. [[RedisOp]] is a sequence of dependent [[RedisBatch]]es, * that requires an exclusive access to a single Redis connection. Typically, a [[RedisOp]] is a * `WATCH`-`MULTI`-`EXEC` transaction (see [[RedisOp]] for more details). * * Note that the client does not handle optimistic lock failures (which happen when watched key is modified by * other client). An [[exception.OptimisticLockException OptimisticLockException]] is * returned in such cases and you must recover from it manually. * * Execution of a [[RedisOp]] may also fail for the same reasons as specified for [[RedisBatch]] in [[executeBatch]]. * Be especially careful when using node clients obtained from cluster client. */ //TODO: executionConfig.decodeOn is ignored now def executeOp[A](op: RedisOp[A], executionConfig: ExecutionConfig): Future[A] = ifReady(executeOp(nextConnection(), op)(executionConfig.responseTimeout)) private def executeOp[A](connection: ActorRef, op: RedisOp[A])(implicit timeout: Timeout): Future[A] = system.actorOf(Props(new RedisOperationActor(connection))).ask(op) .mapNow({ case or: OpResult[A@unchecked] => or.get }) /** * Waits until all Redis connections are initialized and `initOp` is executed. * Note that you can call [[executeBatch]] and [[executeOp]] even if the client is not yet initialized - * requests will be internally queued and executed after initialization is complete. */ def initialized: Future[this.type] = overallInitFuture.mapNow(_ => this) def close(): Unit = { val cause = failure.getOrElse(new ClientStoppedException(address.opt)) failure = cause.opt connections.foreach(_ ! RedisConnectionActor.Close(cause, stop = true)) blockingConnectionPool ! ConnectionPoolActor.Close(cause, stop = true) } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy