From cb7cffda43dbdbfb89f55130b0e50d3e2f90cfcc Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Tue, 23 Dec 2025 12:24:52 +0200 Subject: [PATCH 1/3] Added logging for MultiDBClients --- docs/{multi_database.rst => geographic_failover.rst} | 2 +- redis/asyncio/multidb/client.py | 10 +++++++--- redis/asyncio/multidb/database.py | 3 +++ redis/multidb/client.py | 12 +++++++++++- redis/multidb/database.py | 3 +++ 5 files changed, 25 insertions(+), 5 deletions(-) rename docs/{multi_database.rst => geographic_failover.rst} (99%) diff --git a/docs/multi_database.rst b/docs/geographic_failover.rst similarity index 99% rename from docs/multi_database.rst rename to docs/geographic_failover.rst index 479fab0cd0..39573f4932 100644 --- a/docs/multi_database.rst +++ b/docs/geographic_failover.rst @@ -1,4 +1,4 @@ -Multi-database client (Active-Active) +Client-side geographic failover (Active-Active) ===================================== The multi-database client allows your application to connect to multiple Redis databases, which are typically replicas of each other. diff --git a/redis/asyncio/multidb/client.py b/redis/asyncio/multidb/client.py index f6385e46ea..74fd3050d8 100644 --- a/redis/asyncio/multidb/client.py +++ b/redis/asyncio/multidb/client.py @@ -23,7 +23,7 @@ class MultiDBClient(AsyncRedisModuleCommands, AsyncCoreCommands): """ Client that operates on multiple logical Redis databases. - Should be used in Active-Active database setups. + Should be used in Client-side geographic failover database setups. """ def __init__(self, config: MultiDbConfig): @@ -299,8 +299,8 @@ async def _check_databases_health( unhealthy_db = result.database unhealthy_db.circuit.state = CBState.OPEN - logger.exception( - "Health check failed, due to exception", + logger.debug( + f"Health check failed, due to exception", exc_info=result.original_exception, ) @@ -337,8 +337,12 @@ def _on_circuit_state_change_callback( return if old_state == CBState.CLOSED and new_state == CBState.OPEN: + logger.error(f"Database {circuit.database} is unreachable. Failover has been initiated.") loop.call_later(DEFAULT_GRACE_PERIOD, _half_open_circuit, circuit) + if old_state != CBState.CLOSED and new_state == CBState.CLOSED: + logger.info(f"Database {circuit.database} is reachable again.") + async def aclose(self): if self.command_executor.active_database: await self.command_executor.active_database.client.aclose() diff --git a/redis/asyncio/multidb/database.py b/redis/asyncio/multidb/database.py index ecf7a1b972..a8993976fe 100644 --- a/redis/asyncio/multidb/database.py +++ b/redis/asyncio/multidb/database.py @@ -67,3 +67,6 @@ def circuit(self) -> CircuitBreaker: @circuit.setter def circuit(self, circuit: CircuitBreaker): self._cb = circuit + + def __repr__(self): + return f"Database(client={self.client}, weight={self.weight})" diff --git a/redis/multidb/client.py b/redis/multidb/client.py index d557aaa24b..071b1db5bd 100644 --- a/redis/multidb/client.py +++ b/redis/multidb/client.py @@ -24,7 +24,7 @@ class MultiDBClient(RedisModuleCommands, CoreCommands): """ Client that operates on multiple logical Redis databases. - Should be used in Active-Active database setups. + Should be used in Client-side geographic failover database setups. """ def __init__(self, config: MultiDbConfig): @@ -276,6 +276,11 @@ def _check_databases_health(self, on_error: Callable[[Exception], None] = None): unhealthy_db = e.database unhealthy_db.circuit.state = CBState.OPEN + logger.debug( + f"Health check failed, due to exception", + exc_info=e.original_exception, + ) + if on_error: on_error(e.original_exception) except TimeoutError: @@ -291,10 +296,15 @@ def _on_circuit_state_change_callback( return if old_state == CBState.CLOSED and new_state == CBState.OPEN: + logger.error(f"Database {circuit.database} is unreachable. Failover has been initiated.") + self._bg_scheduler.run_once( DEFAULT_GRACE_PERIOD, _half_open_circuit, circuit ) + if old_state != CBState.CLOSED and new_state == CBState.CLOSED: + logger.info(f"Database {circuit.database} is reachable again.") + def close(self): """ Closes the client and all its resources. diff --git a/redis/multidb/database.py b/redis/multidb/database.py index d46de99e2d..46a29ad35e 100644 --- a/redis/multidb/database.py +++ b/redis/multidb/database.py @@ -128,3 +128,6 @@ def circuit(self) -> CircuitBreaker: @circuit.setter def circuit(self, circuit: CircuitBreaker): self._cb = circuit + + def __repr__(self): + return f"Database(client={self.client}, weight={self.weight})" From 367769b16d58d9939582c802a7e39ccaee8f5ae8 Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Tue, 23 Dec 2025 12:26:39 +0200 Subject: [PATCH 2/3] Codestyle changes --- redis/asyncio/multidb/client.py | 4 +++- redis/multidb/client.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/redis/asyncio/multidb/client.py b/redis/asyncio/multidb/client.py index 74fd3050d8..af92d0a55d 100644 --- a/redis/asyncio/multidb/client.py +++ b/redis/asyncio/multidb/client.py @@ -337,7 +337,9 @@ def _on_circuit_state_change_callback( return if old_state == CBState.CLOSED and new_state == CBState.OPEN: - logger.error(f"Database {circuit.database} is unreachable. Failover has been initiated.") + logger.error( + f"Database {circuit.database} is unreachable. Failover has been initiated." + ) loop.call_later(DEFAULT_GRACE_PERIOD, _half_open_circuit, circuit) if old_state != CBState.CLOSED and new_state == CBState.CLOSED: diff --git a/redis/multidb/client.py b/redis/multidb/client.py index 071b1db5bd..0e39f4fcfc 100644 --- a/redis/multidb/client.py +++ b/redis/multidb/client.py @@ -296,7 +296,9 @@ def _on_circuit_state_change_callback( return if old_state == CBState.CLOSED and new_state == CBState.OPEN: - logger.error(f"Database {circuit.database} is unreachable. Failover has been initiated.") + logger.error( + f"Database {circuit.database} is unreachable. Failover has been initiated." + ) self._bg_scheduler.run_once( DEFAULT_GRACE_PERIOD, _half_open_circuit, circuit From 94932ff444546cabc837d90910c0ead5a4d126f0 Mon Sep 17 00:00:00 2001 From: vladvildanov Date: Tue, 23 Dec 2025 12:28:48 +0200 Subject: [PATCH 3/3] Codestyle changes --- redis/asyncio/multidb/client.py | 2 +- redis/multidb/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/redis/asyncio/multidb/client.py b/redis/asyncio/multidb/client.py index af92d0a55d..c541a33e20 100644 --- a/redis/asyncio/multidb/client.py +++ b/redis/asyncio/multidb/client.py @@ -300,7 +300,7 @@ async def _check_databases_health( unhealthy_db.circuit.state = CBState.OPEN logger.debug( - f"Health check failed, due to exception", + "Health check failed, due to exception", exc_info=result.original_exception, ) diff --git a/redis/multidb/client.py b/redis/multidb/client.py index 0e39f4fcfc..3b91043396 100644 --- a/redis/multidb/client.py +++ b/redis/multidb/client.py @@ -277,7 +277,7 @@ def _check_databases_health(self, on_error: Callable[[Exception], None] = None): unhealthy_db.circuit.state = CBState.OPEN logger.debug( - f"Health check failed, due to exception", + "Health check failed, due to exception", exc_info=e.original_exception, )