Skip to content

Commit 6c254f4

Browse files
mikkolajMikołaj Bulpzareba
authoredDec 20, 2023
Redis Client monitoring (#522)
* redis cluster monitoring * redis master slave monitoring --------- Co-authored-by: Mikołaj Bul <[email protected]> Co-authored-by: pzareba <[email protected]>
1 parent 54c276e commit 6c254f4

9 files changed

+123
-31
lines changed
 

‎redis/src/main/scala/com/avsystem/commons/redis/RedisClusterClient.scala

+14-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.avsystem.commons.redis.actor.RedisConnectionActor.PacksResult
1313
import com.avsystem.commons.redis.commands.{Asking, SlotRange}
1414
import com.avsystem.commons.redis.config.{ClusterConfig, ExecutionConfig}
1515
import com.avsystem.commons.redis.exception._
16+
import com.avsystem.commons.redis.monitoring.ClusterStateObserver
1617
import com.avsystem.commons.redis.protocol._
1718
import com.avsystem.commons.redis.util.DelayedFuture
1819

@@ -41,10 +42,13 @@ import scala.concurrent.duration._
4142
*
4243
* @param seedNodes nodes used to fetch initial cluster state from. You don't need to list all cluster nodes, it is
4344
* only required that at least one of the seed nodes is available during startup.
45+
* @param config client configuration - [[ClusterConfig]]
46+
* @param clusterStateObserver optional observer for monitoring client's state and connections - [[ClusterStateObserver]]
4447
*/
4548
final class RedisClusterClient(
4649
val seedNodes: Seq[NodeAddress] = List(NodeAddress.Default),
47-
val config: ClusterConfig = ClusterConfig()
50+
val config: ClusterConfig = ClusterConfig(),
51+
val clusterStateObserver: OptArg[ClusterStateObserver] = OptArg.Empty,
4852
)(implicit system: ActorSystem) extends RedisClient with RedisKeyedExecutor {
4953

5054
require(seedNodes.nonEmpty, "No seed nodes provided")
@@ -59,7 +63,14 @@ final class RedisClusterClient(
5963
@volatile private[this] var failure = Opt.empty[Throwable]
6064

6165
private val initPromise = Promise[Unit]()
62-
initPromise.future.foreachNow(_ => initSuccess = true)
66+
67+
initPromise.future.onCompleteNow {
68+
case Success(_) =>
69+
clusterStateObserver.foreach(_.onClusterInitialized())
70+
initSuccess = true
71+
case Failure(_) =>
72+
clusterStateObserver.foreach(_.onClusterInitFailure())
73+
}
6374

6475
private def ifReady[T](code: => Future[T]): Future[T] = failure match {
6576
case Opt.Empty if initSuccess => code
@@ -95,7 +106,7 @@ final class RedisClusterClient(
95106
}
96107

97108
private val monitoringActor =
98-
system.actorOf(Props(new ClusterMonitoringActor(seedNodes, config, initPromise.failure, onNewState, onTemporaryClient)))
109+
system.actorOf(Props(new ClusterMonitoringActor(seedNodes, config, initPromise.failure, onNewState, onTemporaryClient, clusterStateObserver)))
99110

100111
private def determineSlot(pack: RawCommandPack): Int = {
101112
var slot = -1

‎redis/src/main/scala/com/avsystem/commons/redis/RedisConnectionClient.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import com.avsystem.commons.redis.exception.ClientStoppedException
2626
*/
2727
final class RedisConnectionClient(
2828
val address: NodeAddress = NodeAddress.Default,
29-
val config: ConnectionConfig = ConnectionConfig()
29+
val config: ConnectionConfig = ConnectionConfig(),
3030
)
3131
(implicit system: ActorSystem) extends RedisClient with RedisConnectionExecutor { self =>
3232

‎redis/src/main/scala/com/avsystem/commons/redis/RedisMasterSlaveClient.scala

+9-4
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@ import com.avsystem.commons.redis.actor.RedisConnectionActor.PacksResult
88
import com.avsystem.commons.redis.actor.SentinelsMonitoringActor
99
import com.avsystem.commons.redis.config.{ExecutionConfig, MasterSlaveConfig}
1010
import com.avsystem.commons.redis.exception.{ClientStoppedException, NoMasterException, NodeRemovedException}
11+
import com.avsystem.commons.redis.monitoring.SentinelStateObserver
1112
import com.avsystem.commons.redis.protocol.{ErrorMsg, RedisReply, TransactionReply}
1213
import com.avsystem.commons.redis.util.DelayedFuture
1314

1415
/**
1516
* Redis client implementation for master-slave installations with Redis Sentinels.
1617
* [[RedisMasterSlaveClient]] is able to execute the same set of commands as [[RedisNodeClient]].
1718
*
18-
* @param masterName name of the master, as configured in the sentinels
19-
* @param seedSentinels sentinel seed addresses - must point to at least one reachable sentinel
19+
* @param masterName name of the master, as configured in the sentinels
20+
* @param seedSentinels sentinel seed addresses - must point to at least one reachable sentinel
21+
* @param config client configuration - [[MasterSlaveConfig]]
22+
* @param sentinelStateObserver optional observer of client's state and connections - [[SentinelStateObserver]]
2023
*/
2124
final class RedisMasterSlaveClient(
2225
val masterName: String,
2326
val seedSentinels: Seq[NodeAddress] = Seq(NodeAddress.DefaultSentinel),
24-
val config: MasterSlaveConfig = MasterSlaveConfig()
27+
val config: MasterSlaveConfig = MasterSlaveConfig(),
28+
val sentinelStateObserver: OptArg[SentinelStateObserver] = OptArg.Empty,
2529
)(implicit system: ActorSystem) extends RedisClient with RedisNodeExecutor {
2630

2731
require(seedSentinels.nonEmpty, "No seed sentinel nodes provided")
@@ -45,6 +49,7 @@ final class RedisMasterSlaveClient(
4549
private def onNewMaster(newMaster: RedisNodeClient): Unit = {
4650
master = newMaster
4751
masterListener(master)
52+
sentinelStateObserver.foreach(_.onMasterChange(newMaster.address))
4853
if (!initSuccess) {
4954
import system.dispatcher
5055
newMaster.initialized.onComplete { result =>
@@ -58,7 +63,7 @@ final class RedisMasterSlaveClient(
5863
}
5964

6065
private val monitoringActor =
61-
system.actorOf(Props(new SentinelsMonitoringActor(masterName, seedSentinels, config, initPromise.failure, onNewMaster)))
66+
system.actorOf(Props(new SentinelsMonitoringActor(masterName, seedSentinels, config, initPromise.failure, onNewMaster, sentinelStateObserver)))
6267

6368
def setMasterListener(listener: RedisNodeClient => Unit)(implicit executor: ExecutionContext): Unit =
6469
masterListener = newMaster => executor.execute(jRunnable(listener(newMaster)))

‎redis/src/main/scala/com/avsystem/commons/redis/actor/ClusterMonitoringActor.scala

+11-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.avsystem.commons.redis.actor.RedisConnectionActor.PacksResult
77
import com.avsystem.commons.redis.commands.{NodeInfo, SlotRange, SlotRangeMapping}
88
import com.avsystem.commons.redis.config.ClusterConfig
99
import com.avsystem.commons.redis.exception.{ClusterInitializationException, ErrorReplyException}
10+
import com.avsystem.commons.redis.monitoring.ClusterStateObserver
1011
import com.avsystem.commons.redis.util.{ActorLazyLogging, SingletonSeq}
1112

1213
import scala.collection.mutable
@@ -19,26 +20,23 @@ final class ClusterMonitoringActor(
1920
config: ClusterConfig,
2021
onClusterInitFailure: Throwable => Any,
2122
onNewClusterState: ClusterState => Any,
22-
onTemporaryClient: RedisNodeClient => Any
23+
onTemporaryClient: RedisNodeClient => Any,
24+
clusterStateObserver: OptArg[ClusterStateObserver] = OptArg.Empty,
2325
) extends Actor with ActorLazyLogging {
2426

2527
import ClusterMonitoringActor._
2628
import context._
2729

2830
private def createConnection(addr: NodeAddress): ActorRef =
29-
actorOf(Props(new RedisConnectionActor(addr, config.monitoringConnectionConfigs(addr))))
31+
actorOf(Props(new RedisConnectionActor(addr, config.monitoringConnectionConfigs(addr), clusterStateObserver)))
3032

3133
private def getConnection(addr: NodeAddress, seed: Boolean): ActorRef =
32-
connections.getOrElse(addr, {
33-
openConnection(addr, seed)
34-
getConnection(addr, seed)
35-
})
34+
connections.getOrElse(addr, openConnection(addr, seed))
3635

37-
private def openConnection(addr: NodeAddress, seed: Boolean): Future[Unit] = {
38-
val initPromise = Promise[Unit]()
36+
private def openConnection(addr: NodeAddress, seed: Boolean): ActorRef = {
3937
val connection = connections.getOrElseUpdate(addr, createConnection(addr))
40-
connection ! RedisConnectionActor.Open(seed, initPromise)
41-
initPromise.future
38+
connection ! RedisConnectionActor.Open(seed, Promise[Unit]())
39+
connection
4240
}
4341

4442
private def createClient(addr: NodeAddress, clusterNode: Boolean = true) =
@@ -93,6 +91,7 @@ final class ClusterMonitoringActor(
9391
case pr: PacksResult => Try(StateRefresh.decodeReplies(pr)) match {
9492
case Success((slotRangeMapping, NodeInfosWithMyself(nodeInfos, thisNodeInfo)))
9593
if thisNodeInfo.configEpoch >= lastEpoch =>
94+
clusterStateObserver.foreach(_.onClusterRefresh())
9695

9796
lastEpoch = thisNodeInfo.configEpoch
9897

@@ -137,7 +136,7 @@ final class ClusterMonitoringActor(
137136
}
138137

139138
case Success(_) =>
140-
// obsolete cluster state, ignore
139+
// obsolete cluster state, ignore
141140

142141
case Failure(err: ErrorReplyException)
143142
if state.isEmpty && seedNodes.size == 1 && config.fallbackToSingleNode &&
@@ -157,6 +156,7 @@ final class ClusterMonitoringActor(
157156

158157
case Failure(cause) =>
159158
log.error("Failed to refresh cluster state", cause)
159+
clusterStateObserver.foreach(_.onClusterRefreshFailure())
160160
if (state.isEmpty) {
161161
seedFailures += cause
162162
if (seedFailures.size == seedNodes.size) {

‎redis/src/main/scala/com/avsystem/commons/redis/actor/RedisConnectionActor.scala

+37-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.avsystem.commons.redis._
1010
import com.avsystem.commons.redis.commands.{PubSubCommand, PubSubEvent, ReplyDecoders}
1111
import com.avsystem.commons.redis.config.ConnectionConfig
1212
import com.avsystem.commons.redis.exception._
13+
import com.avsystem.commons.redis.monitoring.{ConnectionState, ConnectionStateObserver}
1314
import com.avsystem.commons.redis.protocol.{RedisMsg, RedisReply, ValidRedisMsg}
1415
import com.avsystem.commons.redis.util.ActorLazyLogging
1516

@@ -20,8 +21,11 @@ import scala.collection.mutable
2021
import scala.concurrent.duration.Duration
2122
import scala.util.Random
2223

23-
final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
24-
extends Actor with ActorLazyLogging { actor =>
24+
final class RedisConnectionActor(
25+
address: NodeAddress,
26+
config: ConnectionConfig,
27+
connectionStateObserver: OptArg[ConnectionStateObserver] = OptArg.Empty,
28+
) extends Actor with ActorLazyLogging { actor =>
2529

2630
import RedisConnectionActor._
2731
import context._
@@ -56,7 +60,7 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
5660
handlePacks(packs)
5761
case open: Open =>
5862
onOpen(open)
59-
become(connecting(config.reconnectionStrategy, Opt.Empty))
63+
become(watchedConnecting(config.reconnectionStrategy, Opt.Empty))
6064
self ! Connect
6165
}
6266

@@ -95,6 +99,11 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
9599
}
96100
}
97101

102+
private def watchedConnecting(retryStrategy: RetryStrategy, readInitSender: Opt[ActorRef]): Receive = {
103+
connectionStateObserver.foreach(_.onConnectionStateChange(address, ConnectionState.Connecting))
104+
connecting(retryStrategy, readInitSender)
105+
}
106+
98107
private def connecting(retryStrategy: RetryStrategy, readInitSender: Opt[ActorRef]): Receive = {
99108
case open: Open =>
100109
onOpen(open)
@@ -128,7 +137,7 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
128137
case ReadInit =>
129138
// not sure if it's possible to receive ReadInit before Connected but just to be safe
130139
// delay replying with ReadAck until Connected is received
131-
become(connecting(retryStrategy, Opt(sender())))
140+
become(watchedConnecting(retryStrategy, Opt(sender())))
132141
case _: TcpEvent => //ignore, this is from previous connection
133142
}
134143

@@ -197,7 +206,7 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
197206
if (delay > Duration.Zero) {
198207
log.info(s"Next reconnection attempt to $address in $delay")
199208
}
200-
become(connecting(nextStrategy, Opt.Empty))
209+
become(watchedConnecting(nextStrategy, Opt.Empty))
201210
system.scheduler.scheduleOnce(delay, self, Connect)
202211
case Opt.Empty =>
203212
close(failureCause, stopSelf = false)
@@ -282,7 +291,7 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
282291
config.initCommands.decodeReplies(packsResult)
283292
log.debug(s"Successfully initialized Redis connection $localAddr->$remoteAddr")
284293
initPromise.trySuccess(())
285-
become(ready)
294+
become(watchedReady)
286295
writeIfPossible()
287296
} catch {
288297
// https://github.com/antirez/redis/issues/4624
@@ -301,6 +310,11 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
301310
close(new ConnectionInitializationFailure(cause), stopSelf = false)
302311
}
303312

313+
def watchedReady: Receive = {
314+
connectionStateObserver.foreach(_.onConnectionStateChange(address, ConnectionState.Connected))
315+
ready
316+
}
317+
304318
def ready: Receive = {
305319
case open: Open =>
306320
onOpen(open)
@@ -338,7 +352,7 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
338352
case open: Open =>
339353
onOpen(open)
340354
initPromise.success(())
341-
become(ready)
355+
become(watchedReady)
342356
case IncomingPacks(packs) =>
343357
packs.reply(PacksResult.Failure(cause))
344358
case Release => //ignore
@@ -512,7 +526,7 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
512526
if (stopSelf) {
513527
stop(self)
514528
} else {
515-
become(closed(cause, tcpConnecting))
529+
become(watchedClosed(cause, tcpConnecting))
516530
}
517531
}
518532

@@ -522,11 +536,16 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
522536
drain(queuedToReserve)(_.reply(failure))
523537
}
524538

539+
private def watchedClosed(cause: Throwable, tcpConnecting: Boolean): Receive = {
540+
connectionStateObserver.foreach(_.onConnectionStateChange(address, ConnectionState.Closed))
541+
closed(cause, tcpConnecting)
542+
}
543+
525544
private def closed(cause: Throwable, tcpConnecting: Boolean): Receive = {
526545
case open: Open =>
527546
onOpen(open)
528547
incarnation = 0
529-
become(connecting(config.reconnectionStrategy, Opt.Empty))
548+
become(watchedConnecting(config.reconnectionStrategy, Opt.Empty))
530549
if (!tcpConnecting) {
531550
self ! Connect
532551
}
@@ -536,11 +555,19 @@ final class RedisConnectionActor(address: NodeAddress, config: ConnectionConfig)
536555
case Connected(connection, _, _, _) if tcpConnecting =>
537556
// failure may have happened while connecting, simply close the connection
538557
connection ! CloseConnection(immediate = true)
539-
become(closed(cause, tcpConnecting = false))
558+
become(watchedClosed(cause, tcpConnecting = false))
540559
case _: TcpEvent => // ignore
541560
case Close(_, true) =>
542561
stop(self)
543562
}
563+
564+
override def preStart(): Unit = {
565+
connectionStateObserver.foreach(_.onConnectionStateChange(address, ConnectionState.Created))
566+
}
567+
568+
override def postStop(): Unit = {
569+
connectionStateObserver.foreach(_.onConnectionStateChange(address, ConnectionState.Removed))
570+
}
544571
}
545572

546573
object RedisConnectionActor {

‎redis/src/main/scala/com/avsystem/commons/redis/actor/SentinelsMonitoringActor.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.avsystem.commons.redis.actor.RedisConnectionActor.PacksResult
66
import com.avsystem.commons.redis.commands.{PubSubEvent, Subscribe}
77
import com.avsystem.commons.redis.config.MasterSlaveConfig
88
import com.avsystem.commons.redis.exception.MasterSlaveInitializationException
9+
import com.avsystem.commons.redis.monitoring.SentinelStateObserver
910
import com.avsystem.commons.redis.protocol.BulkStringMsg
1011
import com.avsystem.commons.redis.util.ActorLazyLogging
1112
import com.avsystem.commons.redis.{NodeAddress, RedisApi, RedisBatch, RedisNodeClient}
@@ -15,7 +16,8 @@ final class SentinelsMonitoringActor(
1516
seedSentinels: Seq[NodeAddress],
1617
config: MasterSlaveConfig,
1718
onInitFailure: Throwable => Unit,
18-
onMasterChange: RedisNodeClient => Unit
19+
onMasterChange: RedisNodeClient => Unit,
20+
stateObserver: OptArg[SentinelStateObserver] = OptArg.Empty,
1921
) extends Actor with ActorLazyLogging {
2022

2123
import RedisApi.Batches.StringTyped._
@@ -40,7 +42,7 @@ final class SentinelsMonitoringActor(
4042

4143
private def openConnection(addr: NodeAddress, seed: Boolean): ActorRef = {
4244
log.debug(s"Opening monitoring connection to sentinel $addr")
43-
val conn = actorOf(Props(new RedisConnectionActor(addr, config.sentinelConnectionConfigs(addr))))
45+
val conn = actorOf(Props(new RedisConnectionActor(addr, config.sentinelConnectionConfigs(addr), stateObserver)))
4446
sentinels(addr) = conn
4547
conn ! RedisConnectionActor.Open(seed, Promise[Unit]())
4648
onReconnection(conn)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.avsystem.commons
2+
package redis.monitoring
3+
4+
import com.avsystem.commons.redis.RedisClusterClient
5+
6+
/**
7+
* Intended for monitoring [[RedisClusterClient]]'s connections.
8+
* Should be non-blocking and handle internal exceptions by itself.
9+
*/
10+
trait ClusterStateObserver extends ConnectionStateObserver {
11+
def onClusterRefresh(): Unit
12+
def onClusterRefreshFailure(): Unit
13+
def onClusterInitialized(): Unit
14+
def onClusterInitFailure(): Unit
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.avsystem.commons
2+
package redis.monitoring
3+
4+
import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx}
5+
import com.avsystem.commons.redis.NodeAddress
6+
7+
/**
8+
* Intended for monitoring the state of a single Redis connection.
9+
* Should be non-blocking and handle internal exceptions by itself.
10+
*/
11+
trait ConnectionStateObserver {
12+
def onConnectionStateChange(addr: NodeAddress, state: ConnectionState): Unit
13+
}
14+
15+
final class ConnectionState(implicit enumCtx: EnumCtx) extends AbstractValueEnum
16+
17+
object ConnectionState extends AbstractValueEnumCompanion[ConnectionState] {
18+
final val Created, Connecting, Connected, Closed, Removed: ConnectionState = new ConnectionState
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.avsystem.commons
2+
package redis.monitoring
3+
4+
import com.avsystem.commons.redis.NodeAddress
5+
import com.avsystem.commons.redis.RedisMasterSlaveClient
6+
7+
/**
8+
* Intended for monitoring [[RedisMasterSlaveClient]]'s state and connections.
9+
* Should be non-blocking and handle internal exceptions by itself.
10+
*/
11+
trait SentinelStateObserver extends ConnectionStateObserver {
12+
def onMasterChange(master: NodeAddress): Unit
13+
}

0 commit comments

Comments
 (0)
Please sign in to comment.