Skip to content

Commit 4518ce8

Browse files
authored
Hitless upgrade: Support initial implementation for synchronous Redis client - no handshake, no failing over notifications support. (#3713)
1 parent 688c2b0 commit 4518ce8

File tree

9 files changed

+3691
-92
lines changed

9 files changed

+3691
-92
lines changed

redis/_parsers/base.py

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import sys
22
from abc import ABC
33
from asyncio import IncompleteReadError, StreamReader, TimeoutError
4-
from typing import Callable, List, Optional, Protocol, Union
4+
from typing import Awaitable, Callable, List, Optional, Protocol, Union
5+
6+
from redis.maintenance_events import (
7+
NodeMigratedEvent,
8+
NodeMigratingEvent,
9+
NodeMovingEvent,
10+
)
511

612
if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
713
from asyncio import timeout as async_timeout
@@ -158,48 +164,122 @@ async def read_response(
158164
raise NotImplementedError()
159165

160166

161-
_INVALIDATION_MESSAGE = [b"invalidate", "invalidate"]
167+
_INVALIDATION_MESSAGE = (b"invalidate", "invalidate")
168+
_MOVING_MESSAGE = (b"MOVING", "MOVING")
169+
_MIGRATING_MESSAGE = (b"MIGRATING", "MIGRATING")
170+
_MIGRATED_MESSAGE = (b"MIGRATED", "MIGRATED")
171+
_FAILING_OVER_MESSAGE = (b"FAILING_OVER", "FAILING_OVER")
172+
_FAILED_OVER_MESSAGE = (b"FAILED_OVER", "FAILED_OVER")
173+
174+
_MAINTENANCE_MESSAGES = (
175+
*_MIGRATING_MESSAGE,
176+
*_MIGRATED_MESSAGE,
177+
*_FAILING_OVER_MESSAGE,
178+
*_FAILED_OVER_MESSAGE,
179+
)
162180

163181

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

167185
pubsub_push_handler_func: Callable
168186
invalidation_push_handler_func: Optional[Callable] = None
187+
node_moving_push_handler_func: Optional[Callable] = None
188+
maintenance_push_handler_func: Optional[Callable] = None
169189

170190
def handle_pubsub_push_response(self, response):
171191
"""Handle pubsub push responses"""
172192
raise NotImplementedError()
173193

174194
def handle_push_response(self, response, **kwargs):
175-
if response[0] not in _INVALIDATION_MESSAGE:
195+
msg_type = response[0]
196+
if msg_type not in (
197+
*_INVALIDATION_MESSAGE,
198+
*_MAINTENANCE_MESSAGES,
199+
*_MOVING_MESSAGE,
200+
):
176201
return self.pubsub_push_handler_func(response)
177-
if self.invalidation_push_handler_func:
202+
if msg_type in _INVALIDATION_MESSAGE and self.invalidation_push_handler_func:
178203
return self.invalidation_push_handler_func(response)
204+
if msg_type in _MOVING_MESSAGE and self.node_moving_push_handler_func:
205+
# TODO: PARSE latest format when available
206+
host, port = response[2].decode().split(":")
207+
ttl = response[1]
208+
id = 1 # Hardcoded value until the notification starts including the id
209+
notification = NodeMovingEvent(id, host, port, ttl)
210+
return self.node_moving_push_handler_func(notification)
211+
if msg_type in _MAINTENANCE_MESSAGES and self.maintenance_push_handler_func:
212+
if msg_type in _MIGRATING_MESSAGE:
213+
# TODO: PARSE latest format when available
214+
ttl = response[1]
215+
id = 2 # Hardcoded value until the notification starts including the id
216+
notification = NodeMigratingEvent(id, ttl)
217+
elif msg_type in _MIGRATED_MESSAGE:
218+
# TODO: PARSE latest format when available
219+
id = 3 # Hardcoded value until the notification starts including the id
220+
notification = NodeMigratedEvent(id)
221+
else:
222+
notification = None
223+
if notification is not None:
224+
return self.maintenance_push_handler_func(notification)
225+
else:
226+
return None
179227

180228
def set_pubsub_push_handler(self, pubsub_push_handler_func):
181229
self.pubsub_push_handler_func = pubsub_push_handler_func
182230

183231
def set_invalidation_push_handler(self, invalidation_push_handler_func):
184232
self.invalidation_push_handler_func = invalidation_push_handler_func
185233

234+
def set_node_moving_push_handler(self, node_moving_push_handler_func):
235+
self.node_moving_push_handler_func = node_moving_push_handler_func
236+
237+
def set_maintenance_push_handler(self, maintenance_push_handler_func):
238+
self.maintenance_push_handler_func = maintenance_push_handler_func
239+
186240

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

190244
pubsub_push_handler_func: Callable
191245
invalidation_push_handler_func: Optional[Callable] = None
246+
node_moving_push_handler_func: Optional[Callable[..., Awaitable[None]]] = None
247+
maintenance_push_handler_func: Optional[Callable[..., Awaitable[None]]] = None
192248

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

197253
async def handle_push_response(self, response, **kwargs):
198254
"""Handle push responses asynchronously"""
199-
if response[0] not in _INVALIDATION_MESSAGE:
255+
msg_type = response[0]
256+
if msg_type not in (
257+
*_INVALIDATION_MESSAGE,
258+
*_MAINTENANCE_MESSAGES,
259+
*_MOVING_MESSAGE,
260+
):
200261
return await self.pubsub_push_handler_func(response)
201-
if self.invalidation_push_handler_func:
262+
if msg_type in _INVALIDATION_MESSAGE and self.invalidation_push_handler_func:
202263
return await self.invalidation_push_handler_func(response)
264+
if msg_type in _MOVING_MESSAGE and self.node_moving_push_handler_func:
265+
# push notification from enterprise cluster for node moving
266+
# TODO: PARSE latest format when available
267+
host, port = response[2].split(":")
268+
ttl = response[1]
269+
id = 1 # Hardcoded value for async parser
270+
notification = NodeMovingEvent(id, host, port, ttl)
271+
return await self.node_moving_push_handler_func(notification)
272+
if msg_type in _MAINTENANCE_MESSAGES and self.maintenance_push_handler_func:
273+
if msg_type in _MIGRATING_MESSAGE:
274+
# TODO: PARSE latest format when available
275+
ttl = response[1]
276+
id = 2 # Hardcoded value for async parser
277+
notification = NodeMigratingEvent(id, ttl)
278+
elif msg_type in _MIGRATED_MESSAGE:
279+
# TODO: PARSE latest format when available
280+
id = 3 # Hardcoded value for async parser
281+
notification = NodeMigratedEvent(id)
282+
return await self.maintenance_push_handler_func(notification)
203283

204284
def set_pubsub_push_handler(self, pubsub_push_handler_func):
205285
"""Set the pubsub push handler function"""
@@ -209,6 +289,12 @@ def set_invalidation_push_handler(self, invalidation_push_handler_func):
209289
"""Set the invalidation push handler function"""
210290
self.invalidation_push_handler_func = invalidation_push_handler_func
211291

292+
def set_node_moving_push_handler(self, node_moving_push_handler_func):
293+
self.node_moving_push_handler_func = node_moving_push_handler_func
294+
295+
def set_maintenance_push_handler(self, maintenance_push_handler_func):
296+
self.maintenance_push_handler_func = maintenance_push_handler_func
297+
212298

213299
class _AsyncRESPBase(AsyncBaseParser):
214300
"""Base class for async resp parsing"""

redis/_parsers/hiredis.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def __init__(self, socket_read_size):
4747
self.socket_read_size = socket_read_size
4848
self._buffer = bytearray(socket_read_size)
4949
self.pubsub_push_handler_func = self.handle_pubsub_push_response
50+
self.node_moving_push_handler_func = None
51+
self.maintenance_push_handler_func = None
5052
self.invalidation_push_handler_func = None
5153
self._hiredis_PushNotificationType = None
5254

@@ -141,12 +143,15 @@ def read_response(self, disable_decoding=False, push_request=False):
141143
response, self._hiredis_PushNotificationType
142144
):
143145
response = self.handle_push_response(response)
144-
if not push_request:
145-
return self.read_response(
146-
disable_decoding=disable_decoding, push_request=push_request
147-
)
148-
else:
146+
147+
# if this is a push request return the push response
148+
if push_request:
149149
return response
150+
151+
return self.read_response(
152+
disable_decoding=disable_decoding,
153+
push_request=push_request,
154+
)
150155
return response
151156

152157
if disable_decoding:
@@ -169,12 +174,13 @@ def read_response(self, disable_decoding=False, push_request=False):
169174
response, self._hiredis_PushNotificationType
170175
):
171176
response = self.handle_push_response(response)
172-
if not push_request:
173-
return self.read_response(
174-
disable_decoding=disable_decoding, push_request=push_request
175-
)
176-
else:
177+
if push_request:
177178
return response
179+
return self.read_response(
180+
disable_decoding=disable_decoding,
181+
push_request=push_request,
182+
)
183+
178184
elif (
179185
isinstance(response, list)
180186
and response

redis/_parsers/resp3.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class _RESP3Parser(_RESPBase, PushNotificationsParser):
1818
def __init__(self, socket_read_size):
1919
super().__init__(socket_read_size)
2020
self.pubsub_push_handler_func = self.handle_pubsub_push_response
21+
self.node_moving_push_handler_func = None
22+
self.maintenance_push_handler_func = None
2123
self.invalidation_push_handler_func = None
2224

2325
def handle_pubsub_push_response(self, response):
@@ -117,17 +119,21 @@ def _read_response(self, disable_decoding=False, push_request=False):
117119
for _ in range(int(response))
118120
]
119121
response = self.handle_push_response(response)
120-
if not push_request:
121-
return self._read_response(
122-
disable_decoding=disable_decoding, push_request=push_request
123-
)
124-
else:
122+
123+
# if this is a push request return the push response
124+
if push_request:
125125
return response
126+
127+
return self._read_response(
128+
disable_decoding=disable_decoding,
129+
push_request=push_request,
130+
)
126131
else:
127132
raise InvalidResponse(f"Protocol Error: {raw!r}")
128133

129134
if isinstance(response, bytes) and disable_decoding is False:
130135
response = self.encoder.decode(response)
136+
131137
return response
132138

133139

0 commit comments

Comments
 (0)