1+ import logging
12import sys
23from abc import ABC
34from asyncio import IncompleteReadError , StreamReader , TimeoutError
4- from typing import Callable , List , Optional , Protocol , Union
5+ from typing import Awaitable , Callable , List , Optional , Protocol , Union
6+
7+ from redis .maintenance_events import (
8+ MaintenanceEvent ,
9+ NodeFailedOverEvent ,
10+ NodeFailingOverEvent ,
11+ NodeMigratedEvent ,
12+ NodeMigratingEvent ,
13+ NodeMovingEvent ,
14+ )
515
616if sys .version_info .major >= 3 and sys .version_info .minor >= 11 :
717 from asyncio import timeout as async_timeout
5060 "Client sent AUTH, but no password is set" : AuthenticationError ,
5161}
5262
63+ logger = logging .getLogger (__name__ )
64+
5365
5466class BaseParser (ABC ):
5567 EXCEPTION_CLASSES = {
@@ -158,48 +170,195 @@ async def read_response(
158170 raise NotImplementedError ()
159171
160172
161- _INVALIDATION_MESSAGE = [b"invalidate" , "invalidate" ]
173+ class MaintenanceNotificationsParser :
174+ """Protocol defining maintenance push notification parsing functionality"""
175+
176+ @staticmethod
177+ def parse_maintenance_start_msg (response , notification_type ):
178+ # Expected message format is: <event_type> <seq_number> <time>
179+ id = response [1 ]
180+ ttl = response [2 ]
181+ return notification_type (id , ttl )
182+
183+ @staticmethod
184+ def parse_maintenance_completed_msg (response , notification_type ):
185+ # Expected message format is: <event_type> <seq_number>
186+ id = response [1 ]
187+ return notification_type (id )
188+
189+ @staticmethod
190+ def parse_moving_msg (response ):
191+ # Expected message format is: MOVING <seq_number> <time> <endpoint>
192+ id = response [1 ]
193+ ttl = response [2 ]
194+ if response [3 ] in [b"null" , "null" ]:
195+ host , port = None , None
196+ else :
197+ value = response [3 ]
198+ if isinstance (value , bytes ):
199+ value = value .decode ()
200+ host , port = value .split (":" )
201+ port = int (port ) if port is not None else None
202+
203+ return NodeMovingEvent (id , host , port , ttl )
204+
205+
206+ _INVALIDATION_MESSAGE = "invalidate"
207+ _MOVING_MESSAGE = "MOVING"
208+ _MIGRATING_MESSAGE = "MIGRATING"
209+ _MIGRATED_MESSAGE = "MIGRATED"
210+ _FAILING_OVER_MESSAGE = "FAILING_OVER"
211+ _FAILED_OVER_MESSAGE = "FAILED_OVER"
212+
213+ _MAINTENANCE_MESSAGES = (
214+ _MIGRATING_MESSAGE ,
215+ _MIGRATED_MESSAGE ,
216+ _FAILING_OVER_MESSAGE ,
217+ _FAILED_OVER_MESSAGE ,
218+ )
219+
220+ MSG_TYPE_TO_EVENT_PARSER_MAPPING : dict [str , tuple [type [MaintenanceEvent ], Callable ]] = {
221+ _MIGRATING_MESSAGE : (
222+ NodeMigratingEvent ,
223+ MaintenanceNotificationsParser .parse_maintenance_start_msg ,
224+ ),
225+ _MIGRATED_MESSAGE : (
226+ NodeMigratedEvent ,
227+ MaintenanceNotificationsParser .parse_maintenance_completed_msg ,
228+ ),
229+ _FAILING_OVER_MESSAGE : (
230+ NodeFailingOverEvent ,
231+ MaintenanceNotificationsParser .parse_maintenance_start_msg ,
232+ ),
233+ _FAILED_OVER_MESSAGE : (
234+ NodeFailedOverEvent ,
235+ MaintenanceNotificationsParser .parse_maintenance_completed_msg ,
236+ ),
237+ _MOVING_MESSAGE : (
238+ NodeMovingEvent ,
239+ MaintenanceNotificationsParser .parse_moving_msg ,
240+ ),
241+ }
162242
163243
164244class PushNotificationsParser (Protocol ):
165245 """Protocol defining RESP3-specific parsing functionality"""
166246
167247 pubsub_push_handler_func : Callable
168248 invalidation_push_handler_func : Optional [Callable ] = None
249+ node_moving_push_handler_func : Optional [Callable ] = None
250+ maintenance_push_handler_func : Optional [Callable ] = None
169251
170252 def handle_pubsub_push_response (self , response ):
171253 """Handle pubsub push responses"""
172254 raise NotImplementedError ()
173255
174256 def handle_push_response (self , response , ** kwargs ):
175- if response [0 ] not in _INVALIDATION_MESSAGE :
257+ msg_type = response [0 ]
258+ if isinstance (msg_type , bytes ):
259+ msg_type = msg_type .decode ()
260+
261+ if msg_type not in (
262+ _INVALIDATION_MESSAGE ,
263+ * _MAINTENANCE_MESSAGES ,
264+ _MOVING_MESSAGE ,
265+ ):
176266 return self .pubsub_push_handler_func (response )
177- if self .invalidation_push_handler_func :
178- return self .invalidation_push_handler_func (response )
267+
268+ try :
269+ if (
270+ msg_type == _INVALIDATION_MESSAGE
271+ and self .invalidation_push_handler_func
272+ ):
273+ return self .invalidation_push_handler_func (response )
274+
275+ if msg_type == _MOVING_MESSAGE and self .node_moving_push_handler_func :
276+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING [msg_type ][1 ]
277+
278+ notification = parser_function (response )
279+ return self .node_moving_push_handler_func (notification )
280+
281+ if msg_type in _MAINTENANCE_MESSAGES and self .maintenance_push_handler_func :
282+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING [msg_type ][1 ]
283+ notification_type = MSG_TYPE_TO_EVENT_PARSER_MAPPING [msg_type ][0 ]
284+ notification = parser_function (response , notification_type )
285+
286+ if notification is not None :
287+ return self .maintenance_push_handler_func (notification )
288+ except Exception as e :
289+ logger .error (
290+ "Error handling {} message ({}): {}" .format (msg_type , response , e )
291+ )
292+
293+ return None
179294
180295 def set_pubsub_push_handler (self , pubsub_push_handler_func ):
181296 self .pubsub_push_handler_func = pubsub_push_handler_func
182297
183298 def set_invalidation_push_handler (self , invalidation_push_handler_func ):
184299 self .invalidation_push_handler_func = invalidation_push_handler_func
185300
301+ def set_node_moving_push_handler (self , node_moving_push_handler_func ):
302+ self .node_moving_push_handler_func = node_moving_push_handler_func
303+
304+ def set_maintenance_push_handler (self , maintenance_push_handler_func ):
305+ self .maintenance_push_handler_func = maintenance_push_handler_func
306+
186307
187308class AsyncPushNotificationsParser (Protocol ):
188309 """Protocol defining async RESP3-specific parsing functionality"""
189310
190311 pubsub_push_handler_func : Callable
191312 invalidation_push_handler_func : Optional [Callable ] = None
313+ node_moving_push_handler_func : Optional [Callable [..., Awaitable [None ]]] = None
314+ maintenance_push_handler_func : Optional [Callable [..., Awaitable [None ]]] = None
192315
193316 async def handle_pubsub_push_response (self , response ):
194317 """Handle pubsub push responses asynchronously"""
195318 raise NotImplementedError ()
196319
197320 async def handle_push_response (self , response , ** kwargs ):
198321 """Handle push responses asynchronously"""
199- if response [0 ] not in _INVALIDATION_MESSAGE :
322+
323+ msg_type = response [0 ]
324+ if isinstance (msg_type , bytes ):
325+ msg_type = msg_type .decode ()
326+
327+ if msg_type not in (
328+ _INVALIDATION_MESSAGE ,
329+ * _MAINTENANCE_MESSAGES ,
330+ _MOVING_MESSAGE ,
331+ ):
200332 return await self .pubsub_push_handler_func (response )
201- if self .invalidation_push_handler_func :
202- return await self .invalidation_push_handler_func (response )
333+
334+ try :
335+ if (
336+ msg_type == _INVALIDATION_MESSAGE
337+ and self .invalidation_push_handler_func
338+ ):
339+ return await self .invalidation_push_handler_func (response )
340+
341+ if isinstance (msg_type , bytes ):
342+ msg_type = msg_type .decode ()
343+
344+ if msg_type == _MOVING_MESSAGE and self .node_moving_push_handler_func :
345+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING [msg_type ][1 ]
346+ notification = parser_function (response )
347+ return await self .node_moving_push_handler_func (notification )
348+
349+ if msg_type in _MAINTENANCE_MESSAGES and self .maintenance_push_handler_func :
350+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING [msg_type ][1 ]
351+ notification_type = MSG_TYPE_TO_EVENT_PARSER_MAPPING [msg_type ][0 ]
352+ notification = parser_function (response , notification_type )
353+
354+ if notification is not None :
355+ return await self .maintenance_push_handler_func (notification )
356+ except Exception as e :
357+ logger .error (
358+ "Error handling {} message ({}): {}" .format (msg_type , response , e )
359+ )
360+
361+ return None
203362
204363 def set_pubsub_push_handler (self , pubsub_push_handler_func ):
205364 """Set the pubsub push handler function"""
@@ -209,6 +368,12 @@ def set_invalidation_push_handler(self, invalidation_push_handler_func):
209368 """Set the invalidation push handler function"""
210369 self .invalidation_push_handler_func = invalidation_push_handler_func
211370
371+ def set_node_moving_push_handler (self , node_moving_push_handler_func ):
372+ self .node_moving_push_handler_func = node_moving_push_handler_func
373+
374+ def set_maintenance_push_handler (self , maintenance_push_handler_func ):
375+ self .maintenance_push_handler_func = maintenance_push_handler_func
376+
212377
213378class _AsyncRESPBase (AsyncBaseParser ):
214379 """Base class for async resp parsing"""
0 commit comments