com.avsystem.commons.redis.RedisMasterSlaveClient.scala Maven / Gradle / Ivy
package com.avsystem.commons
package redis
import akka.actor.{ActorSystem, Props}
import akka.util.Timeout
import com.avsystem.commons.concurrent.RetryStrategy
import com.avsystem.commons.redis.actor.RedisConnectionActor.PacksResult
import com.avsystem.commons.redis.actor.SentinelsMonitoringActor
import com.avsystem.commons.redis.config.{ExecutionConfig, MasterSlaveConfig}
import com.avsystem.commons.redis.exception.{ClientStoppedException, NoMasterException, NodeRemovedException}
import com.avsystem.commons.redis.protocol.{ErrorMsg, RedisReply, TransactionReply}
import com.avsystem.commons.redis.util.DelayedFuture
/**
* Redis client implementation for master-slave installations with Redis Sentinels.
* [[RedisMasterSlaveClient]] is able to execute the same set of commands as [[RedisNodeClient]].
*
* @param masterName name of the master, as configured in the sentinels
* @param seedSentinels sentinel seed addresses - must point to at least one reachable sentinel
*/
final class RedisMasterSlaveClient(
val masterName: String,
val seedSentinels: Seq[NodeAddress] = Seq(NodeAddress.DefaultSentinel),
val config: MasterSlaveConfig = MasterSlaveConfig()
)(implicit system: ActorSystem) extends RedisClient with RedisNodeExecutor {
require(seedSentinels.nonEmpty, "No seed sentinel nodes provided")
// (optimization) allows us to avoid going through `initPromise` after the client has been successfully initialized
@volatile private[this] var initSuccess = false
@volatile private[this] var master: RedisNodeClient = _
@volatile private[this] var masterListener = (_: RedisNodeClient) => ()
// ensures that all operations fail fast after client is closed instead of being sent further
@volatile private[this] var failure = Opt.empty[Throwable]
private val initPromise = Promise[Unit]()
initPromise.future.foreachNow(_ => initSuccess = true)
private def ifReady[T](code: => Future[T]): Future[T] = failure match {
case Opt.Empty if initSuccess => code
case Opt.Empty => initPromise.future.flatMapNow(_ => code)
case Opt(t) => Future.failed(t)
}
private def onNewMaster(newMaster: RedisNodeClient): Unit = {
master = newMaster
masterListener(master)
if (!initSuccess) {
import system.dispatcher
newMaster.initialized.onComplete { result =>
// this check handles situation when master changed _exactly_ during initialization, see comment for
// analogous situation in RedisClusterClient
if (master eq newMaster) {
initPromise.tryComplete(result.map(_ => ()))
}
}
}
}
private val monitoringActor =
system.actorOf(Props(new SentinelsMonitoringActor(masterName, seedSentinels, config, initPromise.failure, onNewMaster)))
def setMasterListener(listener: RedisNodeClient => Unit)(implicit executor: ExecutionContext): Unit =
masterListener = newMaster => executor.execute(jRunnable(listener(newMaster)))
def currentMaster: Opt[RedisNodeClient] = master.opt
def initializedCurrentMaster: Future[RedisNodeClient] =
initPromise.future.mapNow(_ => master)
def initialized: Future[this.type] =
initPromise.future.mapNow(_ => this)
def executionContext: ExecutionContext = system.dispatcher
def executeBatch[A](batch: RedisBatch[A], execConfig: ExecutionConfig): Future[A] = ifReady {
implicit val timeout: Timeout = execConfig.responseTimeout
val packs = batch.rawCommandPacks
val replyFut = handleFailover(master.executeRaw(packs), packs, config.failoverBackoutStrategy)
replyFut.map(batch.decodeReplies(_))(execConfig.decodeOn)
}
private def handleFailover(
resultFut: Future[PacksResult], packs: RawCommandPacks, retryStrategy: RetryStrategy
)(implicit timeout: Timeout): Future[PacksResult] =
resultFut.flatMapNow { pr =>
pr.collectFirstOpt { case ReadonlyReply(err) => err } match {
case Opt.Empty => resultFut
case Opt(err) =>
// Got READONLY error for at least one command, this means failover is in progress and we should soon obtain
// information about new master. Until that happens, back out and retry.
// NOTE: it is safe (and better) to retry _all_ commands in the batch - the ones that didn't fail are read-only.
retryStrategy.nextRetry match {
case Opt.Empty =>
Future.successful(PacksResult.Failure(new NoMasterException(err)))
case Opt((delay, nextStrategy)) =>
DelayedFuture(delay).flatMapNow(_ => handleFailover(master.executeRaw(packs), packs, nextStrategy))
}
}
} recoverWithNow {
case _: NodeRemovedException =>
// this means the master changed before the batch could be sent, retry immediately
handleFailover(master.executeRaw(packs), packs, config.failoverBackoutStrategy)
}
def executeOp[A](op: RedisOp[A], executionConfig: ExecutionConfig): Future[A] =
ifReady(master.executeOp(op, executionConfig))
def close(): Unit = {
failure = new ClientStoppedException(Opt.Empty).opt
system.stop(monitoringActor)
}
}
object ReadonlyReply {
def unapply(reply: RedisReply): Opt[ErrorMsg] = reply match {
case err: ErrorMsg if err.errorCode == "READONLY" => Opt(err)
case TransactionReply(elements) => elements.headOpt.flatMap(unapply)
case _ => Opt.Empty
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy