Skip to content

Commit e0847c9

Browse files
committed
Adding maintenance notifications for OSS API enabled connections
1 parent b881d55 commit e0847c9

File tree

2 files changed

+366
-1
lines changed

2 files changed

+366
-1
lines changed

redis/maint_notifications.py

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import threading
66
import time
77
from abc import ABC, abstractmethod
8-
from typing import TYPE_CHECKING, Literal, Optional, Union
8+
from typing import TYPE_CHECKING, List, Literal, Optional, Union
99

1010
from redis.typing import Number
1111

@@ -394,6 +394,137 @@ def __hash__(self) -> int:
394394
return hash((self.__class__.__name__, int(self.id)))
395395

396396

397+
class OSSNodeMigratingNotification(MaintenanceNotification):
398+
"""
399+
Notification for when a Redis OSS API client is used and a node is in the process of migrating slots.
400+
401+
This notification is received when a node starts migrating its slots to another node
402+
during cluster rebalancing or maintenance operations.
403+
404+
Args:
405+
id (int): Unique identifier for this notification
406+
src_node (Optional[str]): Source node address - the notifications
407+
received by the connections to the src node will
408+
receive the dest node address
409+
dest_node (Optional[str]): Destination node address - the notifications
410+
received by the connections to the dst node will
411+
receive the src node address
412+
slots (Optional[List[int]]): List of slots being migrated
413+
"""
414+
415+
DEFAULT_TTL = 30
416+
417+
def __init__(
418+
self,
419+
id: int,
420+
src_node: Optional[str] = None,
421+
dest_node: Optional[str] = None,
422+
slots: Optional[List[int]] = None,
423+
):
424+
super().__init__(id, OSSNodeMigratingNotification.DEFAULT_TTL)
425+
self.slots = slots
426+
self.src_node = src_node
427+
self.dest_node = dest_node
428+
429+
def __repr__(self) -> str:
430+
expiry_time = self.creation_time + self.ttl
431+
remaining = max(0, expiry_time - time.monotonic())
432+
return (
433+
f"{self.__class__.__name__}("
434+
f"id={self.id}, "
435+
f"src_node={self.src_node}, "
436+
f"dest_node={self.dest_node}, "
437+
f"slots={self.slots}, "
438+
f"ttl={self.ttl}, "
439+
f"creation_time={self.creation_time}, "
440+
f"expires_at={expiry_time}, "
441+
f"remaining={remaining:.1f}s, "
442+
f"expired={self.is_expired()}"
443+
f")"
444+
)
445+
446+
def __eq__(self, other) -> bool:
447+
"""
448+
Two OSSNodeMigratingNotification notifications are considered equal if they have the same
449+
id and are of the same type.
450+
"""
451+
if not isinstance(other, OSSNodeMigratingNotification):
452+
return False
453+
return self.id == other.id and type(self) is type(other)
454+
455+
def __hash__(self) -> int:
456+
"""
457+
Return a hash value for the notification to allow
458+
instances to be used in sets and as dictionary keys.
459+
460+
Returns:
461+
int: Hash value based on notification type and id
462+
"""
463+
return hash((self.__class__.__name__, int(self.id)))
464+
465+
466+
class OSSNodeMigratedNotification(MaintenanceNotification):
467+
"""
468+
Notification for when a Redis OSS API client is used and a node has completed migrating slots.
469+
470+
This notification is received when a node has finished migrating all its slots
471+
to other nodes during cluster rebalancing or maintenance operations.
472+
473+
Args:
474+
id (int): Unique identifier for this notification
475+
node_address (Optional[str]): Address of the node that has
476+
completed migration - this is the destination node.
477+
slots (Optional[List[int]]): List of slots that have been migrated
478+
"""
479+
480+
DEFAULT_TTL = 30
481+
482+
def __init__(
483+
self,
484+
id: int,
485+
node_address: Optional[str] = None,
486+
slots: Optional[List[int]] = None,
487+
):
488+
super().__init__(id, OSSNodeMigratedNotification.DEFAULT_TTL)
489+
self.node_address = node_address
490+
self.slots = slots
491+
492+
def __repr__(self) -> str:
493+
expiry_time = self.creation_time + self.ttl
494+
remaining = max(0, expiry_time - time.monotonic())
495+
return (
496+
f"{self.__class__.__name__}("
497+
f"id={self.id}, "
498+
f"node_address={self.node_address}, "
499+
f"slots={self.slots}, "
500+
f"ttl={self.ttl}, "
501+
f"creation_time={self.creation_time}, "
502+
f"expires_at={expiry_time}, "
503+
f"remaining={remaining:.1f}s, "
504+
f"expired={self.is_expired()}"
505+
f")"
506+
)
507+
508+
def __eq__(self, other) -> bool:
509+
"""
510+
Two OSSNodeMigratedNotification notifications are considered equal if they have the same
511+
id and are of the same type.
512+
"""
513+
if not isinstance(other, OSSNodeMigratedNotification):
514+
return False
515+
return self.id == other.id and type(self) is type(other)
516+
517+
def __hash__(self) -> int:
518+
"""
519+
Return a hash value for the notification to allow
520+
instances to be used in sets and as dictionary keys.
521+
522+
Returns:
523+
int: Hash value based on notification type and id
524+
"""
525+
return hash((self.__class__.__name__, int(self.id)))
526+
527+
397528
def _is_private_fqdn(host: str) -> bool:
398529
"""
399530
Determine if an FQDN is likely to be internal/private.

tests/maint_notifications/test_maint_notifications.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
NodeMigratedNotification,
1212
NodeFailingOverNotification,
1313
NodeFailedOverNotification,
14+
OSSNodeMigratingNotification,
15+
OSSNodeMigratedNotification,
1416
MaintNotificationsConfig,
1517
MaintNotificationsPoolHandler,
1618
MaintNotificationsConnectionHandler,
@@ -381,6 +383,238 @@ def test_equality_and_hash(self):
381383
assert hash(notification1) != hash(notification3)
382384

383385

386+
class TestOSSNodeMigratingNotification:
387+
"""Test the OSSNodeMigratingNotification class."""
388+
389+
def test_init_with_defaults(self):
390+
"""Test OSSNodeMigratingNotification initialization with default values."""
391+
with patch("time.monotonic", return_value=1000):
392+
notification = OSSNodeMigratingNotification(id=1)
393+
assert notification.id == 1
394+
assert notification.ttl == OSSNodeMigratingNotification.DEFAULT_TTL
395+
assert notification.creation_time == 1000
396+
assert notification.src_node is None
397+
assert notification.dest_node is None
398+
assert notification.slots is None
399+
400+
def test_init_with_all_parameters(self):
401+
"""Test OSSNodeMigratingNotification initialization with all parameters."""
402+
with patch("time.monotonic", return_value=1000):
403+
slots = [1, 2, 3, 4, 5]
404+
notification = OSSNodeMigratingNotification(
405+
id=1,
406+
src_node="127.0.0.1:6379",
407+
dest_node="127.0.0.1:6380",
408+
slots=slots,
409+
)
410+
assert notification.id == 1
411+
assert notification.ttl == OSSNodeMigratingNotification.DEFAULT_TTL
412+
assert notification.creation_time == 1000
413+
assert notification.src_node == "127.0.0.1:6379"
414+
assert notification.dest_node == "127.0.0.1:6380"
415+
assert notification.slots == slots
416+
417+
def test_default_ttl(self):
418+
"""Test that DEFAULT_TTL is used correctly."""
419+
assert OSSNodeMigratingNotification.DEFAULT_TTL == 30
420+
notification = OSSNodeMigratingNotification(id=1)
421+
assert notification.ttl == 30
422+
423+
def test_repr(self):
424+
"""Test OSSNodeMigratingNotification string representation."""
425+
with patch("time.monotonic", return_value=1000):
426+
notification = OSSNodeMigratingNotification(
427+
id=1,
428+
src_node="127.0.0.1:6379",
429+
dest_node="127.0.0.1:6380",
430+
slots=[1, 2, 3],
431+
)
432+
433+
with patch("time.monotonic", return_value=1005): # 5 seconds later
434+
repr_str = repr(notification)
435+
assert "OSSNodeMigratingNotification" in repr_str
436+
assert "id=1" in repr_str
437+
assert "ttl=30" in repr_str
438+
assert "remaining=25.0s" in repr_str
439+
assert "expired=False" in repr_str
440+
441+
def test_equality_same_id_and_type(self):
442+
"""Test equality for notifications with same id and type."""
443+
notification1 = OSSNodeMigratingNotification(
444+
id=1,
445+
src_node="127.0.0.1:6379",
446+
dest_node="127.0.0.1:6380",
447+
slots=[1, 2, 3],
448+
)
449+
notification2 = OSSNodeMigratingNotification(
450+
id=1,
451+
src_node="127.0.0.1:6381",
452+
dest_node="127.0.0.1:6382",
453+
slots=[4, 5, 6],
454+
)
455+
# Should be equal because id and type are the same
456+
assert notification1 == notification2
457+
458+
def test_equality_different_id(self):
459+
"""Test inequality for notifications with different id."""
460+
notification1 = OSSNodeMigratingNotification(id=1)
461+
notification2 = OSSNodeMigratingNotification(id=2)
462+
assert notification1 != notification2
463+
464+
def test_equality_different_type(self):
465+
"""Test inequality for notifications of different types."""
466+
notification1 = OSSNodeMigratingNotification(id=1)
467+
notification2 = NodeMigratingNotification(id=1, ttl=30)
468+
assert notification1 != notification2
469+
470+
def test_hash_same_id_and_type(self):
471+
"""Test hash for notifications with same id and type."""
472+
notification1 = OSSNodeMigratingNotification(
473+
id=1,
474+
src_node="127.0.0.1:6379",
475+
dest_node="127.0.0.1:6380",
476+
slots=[1, 2, 3],
477+
)
478+
notification2 = OSSNodeMigratingNotification(
479+
id=1,
480+
src_node="127.0.0.1:6381",
481+
dest_node="127.0.0.1:6382",
482+
slots=[4, 5, 6],
483+
)
484+
# Should have same hash because id and type are the same
485+
assert hash(notification1) == hash(notification2)
486+
487+
def test_hash_different_id(self):
488+
"""Test hash for notifications with different id."""
489+
notification1 = OSSNodeMigratingNotification(id=1)
490+
notification2 = OSSNodeMigratingNotification(id=2)
491+
assert hash(notification1) != hash(notification2)
492+
493+
def test_in_set(self):
494+
"""Test that notifications can be used in sets."""
495+
notification1 = OSSNodeMigratingNotification(id=1)
496+
notification2 = OSSNodeMigratingNotification(id=1)
497+
notification3 = OSSNodeMigratingNotification(id=2)
498+
notification4 = OSSNodeMigratingNotification(id=2)
499+
500+
notification_set = {notification1, notification2, notification3, notification4}
501+
assert (
502+
len(notification_set) == 2
503+
) # notification1 and notification2 should be the same
504+
505+
506+
class TestOSSNodeMigratedNotification:
507+
"""Test the OSSNodeMigratedNotification class."""
508+
509+
def test_init_with_defaults(self):
510+
"""Test OSSNodeMigratedNotification initialization with default values."""
511+
with patch("time.monotonic", return_value=1000):
512+
notification = OSSNodeMigratedNotification(id=1)
513+
assert notification.id == 1
514+
assert notification.ttl == OSSNodeMigratedNotification.DEFAULT_TTL
515+
assert notification.creation_time == 1000
516+
assert notification.node_address is None
517+
assert notification.slots is None
518+
519+
def test_init_with_all_parameters(self):
520+
"""Test OSSNodeMigratedNotification initialization with all parameters."""
521+
with patch("time.monotonic", return_value=1000):
522+
slots = [1, 2, 3, 4, 5]
523+
notification = OSSNodeMigratedNotification(
524+
id=1,
525+
node_address="127.0.0.1:6380",
526+
slots=slots,
527+
)
528+
assert notification.id == 1
529+
assert notification.ttl == OSSNodeMigratedNotification.DEFAULT_TTL
530+
assert notification.creation_time == 1000
531+
assert notification.node_address == "127.0.0.1:6380"
532+
assert notification.slots == slots
533+
534+
def test_default_ttl(self):
535+
"""Test that DEFAULT_TTL is used correctly."""
536+
assert OSSNodeMigratedNotification.DEFAULT_TTL == 30
537+
notification = OSSNodeMigratedNotification(id=1)
538+
assert notification.ttl == 30
539+
540+
def test_repr(self):
541+
"""Test OSSNodeMigratedNotification string representation."""
542+
with patch("time.monotonic", return_value=1000):
543+
notification = OSSNodeMigratedNotification(
544+
id=1,
545+
node_address="127.0.0.1:6380",
546+
slots=[1, 2, 3],
547+
)
548+
549+
with patch("time.monotonic", return_value=1010): # 10 seconds later
550+
repr_str = repr(notification)
551+
assert "OSSNodeMigratedNotification" in repr_str
552+
assert "id=1" in repr_str
553+
assert "ttl=30" in repr_str
554+
assert "remaining=20.0s" in repr_str
555+
assert "expired=False" in repr_str
556+
557+
def test_equality_same_id_and_type(self):
558+
"""Test equality for notifications with same id and type."""
559+
notification1 = OSSNodeMigratedNotification(
560+
id=1,
561+
node_address="127.0.0.1:6380",
562+
slots=[1, 2, 3],
563+
)
564+
notification2 = OSSNodeMigratedNotification(
565+
id=1,
566+
node_address="127.0.0.1:6381",
567+
slots=[4, 5, 6],
568+
)
569+
# Should be equal because id and type are the same
570+
assert notification1 == notification2
571+
572+
def test_equality_different_id(self):
573+
"""Test inequality for notifications with different id."""
574+
notification1 = OSSNodeMigratedNotification(id=1)
575+
notification2 = OSSNodeMigratedNotification(id=2)
576+
assert notification1 != notification2
577+
578+
def test_equality_different_type(self):
579+
"""Test inequality for notifications of different types."""
580+
notification1 = OSSNodeMigratedNotification(id=1)
581+
notification2 = NodeMigratedNotification(id=1)
582+
assert notification1 != notification2
583+
584+
def test_hash_same_id_and_type(self):
585+
"""Test hash for notifications with same id and type."""
586+
notification1 = OSSNodeMigratedNotification(
587+
id=1,
588+
node_address="127.0.0.1:6380",
589+
slots=[1, 2, 3],
590+
)
591+
notification2 = OSSNodeMigratedNotification(
592+
id=1,
593+
node_address="127.0.0.1:6381",
594+
slots=[4, 5, 6],
595+
)
596+
# Should have same hash because id and type are the same
597+
assert hash(notification1) == hash(notification2)
598+
599+
def test_hash_different_id(self):
600+
"""Test hash for notifications with different id."""
601+
notification1 = OSSNodeMigratedNotification(id=1)
602+
notification2 = OSSNodeMigratedNotification(id=2)
603+
assert hash(notification1) != hash(notification2)
604+
605+
def test_in_set(self):
606+
"""Test that notifications can be used in sets."""
607+
notification1 = OSSNodeMigratedNotification(id=1)
608+
notification2 = OSSNodeMigratedNotification(id=1)
609+
notification3 = OSSNodeMigratedNotification(id=2)
610+
notification4 = OSSNodeMigratedNotification(id=2)
611+
612+
notification_set = {notification1, notification2, notification3, notification4}
613+
assert (
614+
len(notification_set) == 2
615+
) # notification1 and notification2 should be the same
616+
617+
384618
class TestMaintNotificationsConfig:
385619
"""Test the MaintNotificationsConfig class."""
386620

0 commit comments

Comments
 (0)