forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathredis_pubsub.py
More file actions
216 lines (181 loc) · 7.35 KB
/
Copy pathredis_pubsub.py
File metadata and controls
216 lines (181 loc) · 7.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""
Redis Pub/Sub manager for distributed cache invalidation.
This module provides a pub/sub system for synchronizing cache invalidation
across multiple backend instances in a distributed system.
"""
import json
import logging
import threading
import time
from typing import Any, Callable, Dict, List, Optional, cast
import redis
logger = logging.getLogger(__name__)
# redis.Redis is untyped under strict Pyright; treat the client/pubsub objects
# as Any at this SDK boundary.
RedisClientT = Any
class RedisPubSubManager:
"""
Manages Redis pub/sub for distributed cache invalidation.
Features:
- Background subscription thread
- Automatic reconnection on failure
- Event-based callback system
- Graceful shutdown
Example:
pubsub = RedisPubSubManager(redis_client)
pubsub.register_callback('cache_key*', lambda keys: print(f"Invalidate {keys}"))
pubsub.start()
pubsub.publish_invalidation(['cache_key_1', 'cache_key_2'])
pubsub.stop()
"""
CHANNEL = 'cache_invalidation'
RECONNECT_DELAY = 5 # seconds
def __init__(self, redis_client: RedisClientT) -> None:
"""
Initialize pub/sub manager.
Args:
redis_client: Redis client instance
"""
self.redis_client: RedisClientT = redis_client
self.pubsub: Optional[Any] = None
self.subscriber_thread: Optional[threading.Thread] = None
self.running = False
self.callbacks: Dict[str, List[Callable[[List[str]], None]]] = {}
self.lock = threading.Lock()
def start(self) -> None:
"""Start the pub/sub subscription thread."""
if self.running:
logger.warning("PubSub manager already running")
return
self.running = True
try:
pubsub = self.redis_client.pubsub()
pubsub.subscribe(self.CHANNEL)
self.pubsub = pubsub
self.subscriber_thread = threading.Thread(
target=self._subscribe_loop, daemon=True, name='redis-pubsub-subscriber'
)
self.subscriber_thread.start()
logger.info(f"Started Redis pub/sub subscription on channel: {self.CHANNEL}")
except Exception as e:
logger.error(f"Failed to start Redis pub/sub: {e}")
self.running = False
raise
def stop(self) -> None:
"""Stop the subscription and clean up."""
self.running = False
if self.pubsub:
try:
self.pubsub.unsubscribe(self.CHANNEL)
except Exception as e:
logger.error(f"Error closing pub/sub connection: {e}")
if self.subscriber_thread:
self.subscriber_thread.join(timeout=5)
if self.pubsub:
try:
self.pubsub.close()
except Exception as e:
logger.error(f"Error closing pub/sub connection: {e}")
logger.info("Stopped Redis pub/sub manager")
def register_callback(self, key_pattern: str, callback: Callable[[List[str]], None]) -> None:
"""
Register a callback for cache invalidation events.
Args:
key_pattern: Pattern to match cache keys (supports '*' wildcard at end)
callback: Function to call with list of invalidated keys
"""
with self.lock:
if key_pattern not in self.callbacks:
self.callbacks[key_pattern] = []
self.callbacks[key_pattern].append(callback)
logger.debug(f"Registered callback for pattern: {key_pattern}")
def publish_invalidation(self, keys: List[str]) -> None:
"""
Publish cache invalidation event.
Args:
keys: List of cache keys to invalidate
"""
message = {'event': 'invalidate', 'keys': keys, 'timestamp': time.time()}
try:
self.redis_client.publish(self.CHANNEL, json.dumps(message))
logger.debug(f"Published invalidation for keys: {keys}")
except Exception as e:
logger.error(f"Failed to publish invalidation: {e}")
def _subscribe_loop(self) -> None:
"""Background loop for receiving pub/sub messages."""
while self.running:
try:
if self.pubsub is None:
time.sleep(self.RECONNECT_DELAY)
continue
message: Optional[Dict[str, Any]] = self.pubsub.get_message(timeout=1.0)
if message and message.get('type') == 'message':
self._handle_message(message.get('data'))
except redis.ConnectionError as e:
if not self.running:
break
logger.error(f"Redis connection error in pub/sub: {e}")
self._reconnect()
except Exception as e:
if not self.running:
break
logger.error(f"Error in pub/sub loop: {e}")
time.sleep(self.RECONNECT_DELAY)
def _reconnect(self) -> None:
"""Attempt to reconnect to Redis pub/sub."""
logger.info("Attempting to reconnect to Redis pub/sub...")
time.sleep(self.RECONNECT_DELAY)
try:
if self.pubsub:
self.pubsub.close()
pubsub = self.redis_client.pubsub()
pubsub.subscribe(self.CHANNEL)
self.pubsub = pubsub
logger.info("Successfully reconnected to Redis pub/sub")
except Exception as e:
logger.error(f"Failed to reconnect: {e}")
def _handle_message(self, data: Any) -> None:
"""
Handle incoming pub/sub message.
Args:
data: Raw message data
"""
try:
if isinstance(data, bytes):
payload = data.decode('utf-8')
elif isinstance(data, str):
payload = data
else:
return
message = json.loads(payload)
event = message.get('event')
keys_raw = message.get('keys', [])
keys: List[str] = [str(k) for k in cast(List[Any], keys_raw)] if isinstance(keys_raw, list) else []
if event == 'invalidate':
logger.debug(f"Received invalidation for keys: {keys}")
self._trigger_callbacks(keys)
except Exception as e:
logger.error(f"Error handling pub/sub message: {e}")
def _trigger_callbacks(self, keys: List[str]) -> None:
"""
Trigger registered callbacks for invalidated keys.
Args:
keys: List of invalidated keys
"""
with self.lock:
for key in keys:
# Match exact keys
if key in self.callbacks:
for callback in self.callbacks[key]:
try:
callback([key])
except Exception as e:
logger.error(f"Error in callback for key {key}: {e}")
# Match wildcard patterns
for pattern, callbacks in self.callbacks.items():
if pattern.endswith('*') and key.startswith(pattern[:-1]):
for callback in callbacks:
try:
callback([key])
except Exception as e:
logger.error(f"Error in callback for pattern {pattern}: {e}")