Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
092e33b
Handling of topology update push notifications for Standalone Redis c…
petyaslavova Jun 27, 2025
41a199e
Adding sequence id to the maintenance push notifications. Adding unit…
petyaslavova Jul 11, 2025
63d0c45
Adding integration-like tests for migrating/migrated events handling
petyaslavova Jul 11, 2025
5c71733
Removed unused imports
petyaslavova Jul 11, 2025
96c6e5d
Revert changing of the default retry object initialization for connec…
petyaslavova Jul 11, 2025
8691475
Complete migrating/migrated integration-like tests
petyaslavova Jul 14, 2025
7b57a22
Adding moving integration-like tests
petyaslavova Jul 15, 2025
bed2e40
Fixed BlockingConnectionPool locking strategy. Removed debug logging.…
petyaslavova Jul 17, 2025
0744ee5
Fixing linters
petyaslavova Jul 17, 2025
4c536f3
Applying Copilot's comments
petyaslavova Jul 17, 2025
6768d5d
Fixed type annotations not compatible with older python versions
petyaslavova Jul 17, 2025
ce31ec7
Add a few more tests and fix pool mock for python 3.9
petyaslavova Jul 17, 2025
d73cd35
Adding maintenance state to connections. Migrating and Migrated are n…
petyaslavova Jul 18, 2025
788cf52
Refactored the tmp host address and timeout storing and the way to ap…
petyaslavova Jul 22, 2025
6d496f0
Apply review comments
petyaslavova Jul 24, 2025
a8ba5ce
Adding handling of FAILING_OVER and FAILED_OVER events/push notificat…
petyaslavova Jul 24, 2025
2d3731f
Applying moving/moved only on connections to the same proxy.
petyaslavova Jul 26, 2025
d2288e9
Adds handshake for enabling server maintenance notifications
elena-kolevska Aug 1, 2025
b294db2
Adds tests
elena-kolevska Aug 7, 2025
a82cbfe
Merge branch 'master' into ps_hitless_upgrade_sync_redis
petyaslavova Aug 8, 2025
2cdfa75
Applying review comments.
petyaslavova Aug 8, 2025
822fccd
Refactor to have less methods in pool classes and made some of the ex…
petyaslavova Aug 11, 2025
2736aaa
Fixing lint errors
petyaslavova Aug 11, 2025
1e2b96d
Fixing tests
petyaslavova Aug 11, 2025
fb487c0
Adds handshake
elena-kolevska Aug 7, 2025
67bbee9
Fixes mock messages to follow the latest standard
elena-kolevska Aug 12, 2025
70688cc
Linters
elena-kolevska Aug 12, 2025
f9eec35
Hitless Upgrades enabled by default
elena-kolevska Aug 12, 2025
5fd2ddb
Fixing unit tests
petyaslavova Aug 13, 2025
e8785de
Applying review comments and moving resolving of conn ip in the Abstr…
petyaslavova Aug 13, 2025
c3caf6a
Fixing the docs of some of the new methods in connection pools. Handl…
petyaslavova Aug 14, 2025
8d7cc00
Merge branch 'ps_hitless_upgrade_sync_redis' into ps_add_fail_over_ev…
petyaslavova Aug 15, 2025
76eba1b
Merge branch 'ps_add_fail_over_events_handling' into hitless_handshake
petyaslavova Aug 15, 2025
579d032
Handling of topology update push notifications for Standalone Redis c…
petyaslavova Jun 27, 2025
8d27a86
Adding sequence id to the maintenance push notifications. Adding unit…
petyaslavova Jul 11, 2025
32a16f0
Adding integration-like tests for migrating/migrated events handling
petyaslavova Jul 11, 2025
8bfdf13
Removed unused imports
petyaslavova Jul 11, 2025
33d7295
Revert changing of the default retry object initialization for connec…
petyaslavova Jul 11, 2025
346097f
Complete migrating/migrated integration-like tests
petyaslavova Jul 14, 2025
f3a9a71
Adding moving integration-like tests
petyaslavova Jul 15, 2025
c0438c8
Fixed BlockingConnectionPool locking strategy. Removed debug logging.…
petyaslavova Jul 17, 2025
6ca514f
Fixing linters
petyaslavova Jul 17, 2025
778abdf
Applying Copilot's comments
petyaslavova Jul 17, 2025
667109b
Fixed type annotations not compatible with older python versions
petyaslavova Jul 17, 2025
ef1742a
Add a few more tests and fix pool mock for python 3.9
petyaslavova Jul 17, 2025
7b43890
Adding maintenance state to connections. Migrating and Migrated are n…
petyaslavova Jul 18, 2025
08f1585
Refactored the tmp host address and timeout storing and the way to ap…
petyaslavova Jul 22, 2025
9a31a71
Apply review comments
petyaslavova Jul 24, 2025
602bbe9
Applying moving/moved only on connections to the same proxy.
petyaslavova Jul 26, 2025
953b41a
Applying review comments.
petyaslavova Aug 8, 2025
2210fed
Refactor to have less methods in pool classes and made some of the ex…
petyaslavova Aug 11, 2025
1427d99
Fixing lint errors
petyaslavova Aug 11, 2025
a2744f3
Fixing tests
petyaslavova Aug 11, 2025
260b34e
Fixing the docs of some of the new methods in connection pools. Handl…
petyaslavova Aug 14, 2025
4c6eb44
Applying review comments
petyaslavova Aug 15, 2025
10ded34
Adding handling of FAILING_OVER and FAILED_OVER events/push notificat…
petyaslavova Jul 24, 2025
241be74
Remove unused parse_list_to_dict function from helpers (#3733)
mengxunQAQ Aug 12, 2025
ba7cb87
Typos in vectorset commands.py (#3719)
hunterhogan Aug 12, 2025
c01d331
Adding abstractmethod declaration for cache property setter in Evicti…
mengxunQAQ Aug 12, 2025
7a2c3fc
Fix async clients safety when used as an async context manager (#3512)
abrookins Aug 15, 2025
5024b5f
Applying review comments
petyaslavova Aug 15, 2025
5fd93c5
Adding handling of FAILING_OVER and FAILED_OVER events/push notificat…
petyaslavova Jul 24, 2025
efa9852
Fixes mock messages to follow the latest standard
elena-kolevska Aug 12, 2025
8a6402f
Applying some test fixes after rebase
petyaslavova Aug 15, 2025
07402d0
Merge branch 'feat/hitless-upgrade-sync-standalone' into ps_add_fail_…
petyaslavova Aug 18, 2025
b9afaf0
Fixing tests after merging with feature branch
petyaslavova Aug 18, 2025
058be2c
Fixing lint errors.
petyaslavova Aug 18, 2025
e4a8646
Update tests/test_maintenance_events_handling.py
petyaslavova Aug 18, 2025
51d24ba
Applying review comments
petyaslavova Aug 18, 2025
66c1fe0
Applying review comments
petyaslavova Aug 18, 2025
67aee8c
Merge branch 'ps_add_fail_over_events_handling' into hitless_handshake
petyaslavova Aug 18, 2025
a7dd150
Merge branch 'feat/hitless-upgrade-sync-standalone' into hitless_hand…
petyaslavova Aug 19, 2025
1388cb9
Fixing a check if we should send hitless handshake. Fixing merge issues.
petyaslavova Aug 19, 2025
97db940
Applying review comments
petyaslavova Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 120 additions & 7 deletions redis/_parsers/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import logging
import sys
from abc import ABC
from asyncio import IncompleteReadError, StreamReader, TimeoutError
from typing import Callable, List, Optional, Protocol, Union

from redis.maintenance_events import (
NodeMigratedEvent,
NodeMigratingEvent,
NodeMovingEvent,
)

if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
from asyncio import timeout as async_timeout
else:
Expand Down Expand Up @@ -50,6 +57,8 @@
"Client sent AUTH, but no password is set": AuthenticationError,
}

logger = logging.getLogger(__name__)


class BaseParser(ABC):
EXCEPTION_CLASSES = {
Expand Down Expand Up @@ -158,48 +167,146 @@ async def read_response(
raise NotImplementedError()


_INVALIDATION_MESSAGE = [b"invalidate", "invalidate"]
_INVALIDATION_MESSAGE = (b"invalidate", "invalidate")
_MOVING_MESSAGE = (b"MOVING", "MOVING")
_MIGRATING_MESSAGE = (b"MIGRATING", "MIGRATING")
_MIGRATED_MESSAGE = (b"MIGRATED", "MIGRATED")
_FAILING_OVER_MESSAGE = (b"FAILING_OVER", "FAILING_OVER")
_FAILED_OVER_MESSAGE = (b"FAILED_OVER", "FAILED_OVER")

_MAINTENANCE_MESSAGES = (
*_MIGRATING_MESSAGE,
*_MIGRATED_MESSAGE,
*_FAILING_OVER_MESSAGE,
*_FAILED_OVER_MESSAGE,
)


class PushNotificationsParser(Protocol):
"""Protocol defining RESP3-specific parsing functionality"""

pubsub_push_handler_func: Callable
invalidation_push_handler_func: Optional[Callable] = None
node_moving_push_handler_func: Optional[Callable] = None
maintenance_push_handler_func: Optional[Callable] = None

def handle_pubsub_push_response(self, response):
"""Handle pubsub push responses"""
raise NotImplementedError()

def handle_push_response(self, response, **kwargs):
if response[0] not in _INVALIDATION_MESSAGE:
msg_type = response[0]
if msg_type not in (
*_INVALIDATION_MESSAGE,
*_MAINTENANCE_MESSAGES,
*_MOVING_MESSAGE,
):
return self.pubsub_push_handler_func(response)
if self.invalidation_push_handler_func:
return self.invalidation_push_handler_func(response)

try:
if (
msg_type in _INVALIDATION_MESSAGE
and self.invalidation_push_handler_func
):
return self.invalidation_push_handler_func(response)
if msg_type in _MOVING_MESSAGE and self.node_moving_push_handler_func:
# Expected message format is: MOVING <seq_number> <time> <endpoint>
id = response[1]
ttl = response[2]
host, port = response[3].decode().split(":")
notification = NodeMovingEvent(id, host, port, ttl)
return self.node_moving_push_handler_func(notification)

if msg_type in _MAINTENANCE_MESSAGES and self.maintenance_push_handler_func:
notification = None

if msg_type in _MIGRATING_MESSAGE:
# Expected message format is: MIGRATING <seq_number> <time> <shard_id-s>
id = response[1]
ttl = response[2]
notification = NodeMigratingEvent(id, ttl)
elif msg_type in _MIGRATED_MESSAGE:
id = response[1]
notification = NodeMigratedEvent(id)

if notification is not None:
return self.maintenance_push_handler_func(notification)
except Exception as e:
logger.error(
"Error handling {} message ({}): {}".format(msg_type, response, e)
)

return None

def set_pubsub_push_handler(self, pubsub_push_handler_func):
self.pubsub_push_handler_func = pubsub_push_handler_func

def set_invalidation_push_handler(self, invalidation_push_handler_func):
self.invalidation_push_handler_func = invalidation_push_handler_func

def set_node_moving_push_handler(self, node_moving_push_handler_func):
self.node_moving_push_handler_func = node_moving_push_handler_func

def set_maintenance_push_handler(self, maintenance_push_handler_func):
self.maintenance_push_handler_func = maintenance_push_handler_func


class AsyncPushNotificationsParser(Protocol):
"""Protocol defining async RESP3-specific parsing functionality"""

pubsub_push_handler_func: Callable
invalidation_push_handler_func: Optional[Callable] = None
node_moving_push_handler_func: Optional[Callable] = None
maintenance_push_handler_func: Optional[Callable] = None

async def handle_pubsub_push_response(self, response):
"""Handle pubsub push responses asynchronously"""
raise NotImplementedError()

async def handle_push_response(self, response, **kwargs):
"""Handle push responses asynchronously"""
if response[0] not in _INVALIDATION_MESSAGE:

msg_type = response[0]
if msg_type not in (
*_INVALIDATION_MESSAGE,
*_MAINTENANCE_MESSAGES,
*_MOVING_MESSAGE,
):
return await self.pubsub_push_handler_func(response)
if self.invalidation_push_handler_func:
return await self.invalidation_push_handler_func(response)

try:
if (
msg_type in _INVALIDATION_MESSAGE
and self.invalidation_push_handler_func
):
return await self.invalidation_push_handler_func(response)
if msg_type in _MOVING_MESSAGE and self.node_moving_push_handler_func:
# push notification from enterprise cluster for node moving
id = response[1]
ttl = response[2]
host, port = response[3].split(":")
notification = NodeMovingEvent(id, host, port, ttl)
return await self.node_moving_push_handler_func(notification)

if msg_type in _MAINTENANCE_MESSAGES and self.maintenance_push_handler_func:
notification = None

if msg_type in _MIGRATING_MESSAGE:
id = response[1]
ttl = response[2]
notification = NodeMigratingEvent(id, ttl)
elif msg_type in _MIGRATED_MESSAGE:
id = response[1]
notification = NodeMigratedEvent(id)

if notification is not None:
return self.maintenance_push_handler_func(notification)
except Exception as e:
logger.error(
"Error handling {} message ({}): {}".format(msg_type, response, e)
)

return None

def set_pubsub_push_handler(self, pubsub_push_handler_func):
"""Set the pubsub push handler function"""
Expand All @@ -209,6 +316,12 @@ def set_invalidation_push_handler(self, invalidation_push_handler_func):
"""Set the invalidation push handler function"""
self.invalidation_push_handler_func = invalidation_push_handler_func

def set_node_moving_push_handler_func(self, node_moving_push_handler_func):
self.node_moving_push_handler_func = node_moving_push_handler_func

def set_maintenance_push_handler(self, maintenance_push_handler_func):
self.maintenance_push_handler_func = maintenance_push_handler_func


class _AsyncRESPBase(AsyncBaseParser):
"""Base class for async resp parsing"""
Expand Down
26 changes: 16 additions & 10 deletions redis/_parsers/hiredis.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def __init__(self, socket_read_size):
self.socket_read_size = socket_read_size
self._buffer = bytearray(socket_read_size)
self.pubsub_push_handler_func = self.handle_pubsub_push_response
self.node_moving_push_handler_func = None
self.maintenance_push_handler_func = None
self.invalidation_push_handler_func = None
self._hiredis_PushNotificationType = None

Expand Down Expand Up @@ -141,12 +143,15 @@ def read_response(self, disable_decoding=False, push_request=False):
response, self._hiredis_PushNotificationType
):
response = self.handle_push_response(response)
if not push_request:
return self.read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:

# if this is a push request return the push response
if push_request:
return response

return self.read_response(
disable_decoding=disable_decoding,
push_request=push_request,
)
return response

if disable_decoding:
Expand All @@ -169,12 +174,13 @@ def read_response(self, disable_decoding=False, push_request=False):
response, self._hiredis_PushNotificationType
):
response = self.handle_push_response(response)
if not push_request:
return self.read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:
if push_request:
return response
return self.read_response(
disable_decoding=disable_decoding,
push_request=push_request,
)

elif (
isinstance(response, list)
and response
Expand Down
16 changes: 11 additions & 5 deletions redis/_parsers/resp3.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class _RESP3Parser(_RESPBase, PushNotificationsParser):
def __init__(self, socket_read_size):
super().__init__(socket_read_size)
self.pubsub_push_handler_func = self.handle_pubsub_push_response
self.node_moving_push_handler_func = None
self.maintenance_push_handler_func = None
self.invalidation_push_handler_func = None

def handle_pubsub_push_response(self, response):
Expand Down Expand Up @@ -117,17 +119,21 @@ def _read_response(self, disable_decoding=False, push_request=False):
for _ in range(int(response))
]
response = self.handle_push_response(response)
if not push_request:
return self._read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:

# if this is a push request return the push response
if push_request:
return response

return self._read_response(
disable_decoding=disable_decoding,
push_request=push_request,
)
else:
raise InvalidResponse(f"Protocol Error: {raw!r}")

if isinstance(response, bytes) and disable_decoding is False:
response = self.encoder.decode(response)

return response


Expand Down
2 changes: 2 additions & 0 deletions redis/asyncio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,8 @@ def __init__(
)
self._condition = asyncio.Condition()
self.timeout = timeout
self._in_maintenance = False
self._locked = False

@deprecated_args(
args_to_warn=["*"],
Expand Down
Loading