davidtran999 commited on
Commit
859c52d
·
verified ·
1 Parent(s): c330e24

Upload hue_portal/core/redis_cache.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. hue_portal/core/redis_cache.py +240 -0
hue_portal/core/redis_cache.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Redis Cache Layer for Query Rewrite and Prefetch Results.
3
+
4
+ This module provides Redis caching for:
5
+ - Query rewrite results (1000 queries, TTL 1 hour)
6
+ - Prefetch results by document_code (TTL 30 minutes)
7
+
8
+ Supports Upstash and Railway Redis free tier.
9
+ """
10
+ import os
11
+ import logging
12
+ import json
13
+ from typing import Optional, Dict, Any, List
14
+ from datetime import timedelta
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Try to import redis
19
+ try:
20
+ import redis
21
+ REDIS_AVAILABLE = True
22
+ except ImportError:
23
+ REDIS_AVAILABLE = False
24
+ logger.warning("[REDIS] redis package not installed. Install with: pip install redis")
25
+
26
+
27
+ class RedisCache:
28
+ """
29
+ Redis cache manager for query rewrites and prefetch results.
30
+
31
+ Supports graceful degradation if Redis is unavailable.
32
+ """
33
+
34
+ def __init__(self, redis_url: Optional[str] = None):
35
+ """
36
+ Initialize Redis cache.
37
+
38
+ Args:
39
+ redis_url: Redis connection URL. If None, reads from REDIS_URL env var.
40
+ """
41
+ self.redis_url = redis_url or os.environ.get("REDIS_URL")
42
+ self.client: Optional[redis.Redis] = None
43
+ self._connected = False
44
+
45
+ if not REDIS_AVAILABLE:
46
+ logger.warning("[REDIS] Redis package not available, caching disabled")
47
+ return
48
+
49
+ if not self.redis_url:
50
+ logger.warning("[REDIS] REDIS_URL not configured, caching disabled")
51
+ return
52
+
53
+ self._connect()
54
+
55
+ def _connect(self) -> None:
56
+ """Connect to Redis server."""
57
+ if not REDIS_AVAILABLE or not self.redis_url:
58
+ return
59
+
60
+ try:
61
+ # Parse Redis URL
62
+ # Format: redis://[:password@]host[:port][/db]
63
+ # Or: rediss:// for SSL
64
+ self.client = redis.from_url(
65
+ self.redis_url,
66
+ decode_responses=True, # Auto-decode strings
67
+ socket_connect_timeout=5,
68
+ socket_timeout=5,
69
+ retry_on_timeout=True,
70
+ health_check_interval=30
71
+ )
72
+
73
+ # Test connection
74
+ self.client.ping()
75
+ self._connected = True
76
+ logger.info("[REDIS] ✅ Connected to Redis successfully")
77
+ except Exception as e:
78
+ logger.warning(f"[REDIS] Failed to connect to Redis: {e}, caching disabled")
79
+ self.client = None
80
+ self._connected = False
81
+
82
+ def is_available(self) -> bool:
83
+ """Check if Redis is available and connected."""
84
+ if not self._connected or not self.client:
85
+ return False
86
+
87
+ try:
88
+ self.client.ping()
89
+ return True
90
+ except Exception:
91
+ self._connected = False
92
+ return False
93
+
94
+ def get(self, key: str) -> Optional[Any]:
95
+ """
96
+ Get value from cache.
97
+
98
+ Args:
99
+ key: Cache key.
100
+
101
+ Returns:
102
+ Cached value or None if not found.
103
+ """
104
+ if not self.is_available():
105
+ return None
106
+
107
+ try:
108
+ value = self.client.get(key)
109
+ if value is None:
110
+ return None
111
+
112
+ # Try to parse as JSON
113
+ try:
114
+ return json.loads(value)
115
+ except (json.JSONDecodeError, TypeError):
116
+ # Return as string if not JSON
117
+ return value
118
+ except Exception as e:
119
+ logger.warning(f"[REDIS] Error getting key '{key}': {e}")
120
+ return None
121
+
122
+ def set(
123
+ self,
124
+ key: str,
125
+ value: Any,
126
+ ttl_seconds: Optional[int] = None
127
+ ) -> bool:
128
+ """
129
+ Set value in cache.
130
+
131
+ Args:
132
+ key: Cache key.
133
+ value: Value to cache (will be JSON-encoded if dict/list).
134
+ ttl_seconds: Time to live in seconds. If None, no expiration.
135
+
136
+ Returns:
137
+ True if successful, False otherwise.
138
+ """
139
+ if not self.is_available():
140
+ return False
141
+
142
+ try:
143
+ # Serialize value to JSON if it's a dict/list
144
+ if isinstance(value, (dict, list)):
145
+ serialized = json.dumps(value, ensure_ascii=False)
146
+ else:
147
+ serialized = str(value)
148
+
149
+ if ttl_seconds:
150
+ self.client.setex(key, ttl_seconds, serialized)
151
+ else:
152
+ self.client.set(key, serialized)
153
+
154
+ return True
155
+ except Exception as e:
156
+ logger.warning(f"[REDIS] Error setting key '{key}': {e}")
157
+ return False
158
+
159
+ def delete(self, key: str) -> bool:
160
+ """
161
+ Delete key from cache.
162
+
163
+ Args:
164
+ key: Cache key.
165
+
166
+ Returns:
167
+ True if successful, False otherwise.
168
+ """
169
+ if not self.is_available():
170
+ return False
171
+
172
+ try:
173
+ self.client.delete(key)
174
+ return True
175
+ except Exception as e:
176
+ logger.warning(f"[REDIS] Error deleting key '{key}': {e}")
177
+ return False
178
+
179
+ def exists(self, key: str) -> bool:
180
+ """
181
+ Check if key exists in cache.
182
+
183
+ Args:
184
+ key: Cache key.
185
+
186
+ Returns:
187
+ True if key exists, False otherwise.
188
+ """
189
+ if not self.is_available():
190
+ return False
191
+
192
+ try:
193
+ return self.client.exists(key) > 0
194
+ except Exception:
195
+ return False
196
+
197
+ def clear_pattern(self, pattern: str) -> int:
198
+ """
199
+ Clear all keys matching pattern.
200
+
201
+ Args:
202
+ pattern: Redis key pattern (e.g., "query_rewrite:*").
203
+
204
+ Returns:
205
+ Number of keys deleted.
206
+ """
207
+ if not self.is_available():
208
+ return 0
209
+
210
+ try:
211
+ keys = self.client.keys(pattern)
212
+ if keys:
213
+ return self.client.delete(*keys)
214
+ return 0
215
+ except Exception as e:
216
+ logger.warning(f"[REDIS] Error clearing pattern '{pattern}': {e}")
217
+ return 0
218
+
219
+
220
+ # Singleton instance
221
+ _redis_cache_instance: Optional[RedisCache] = None
222
+
223
+
224
+ def get_redis_cache(redis_url: Optional[str] = None) -> RedisCache:
225
+ """
226
+ Get or create Redis cache instance.
227
+
228
+ Args:
229
+ redis_url: Optional Redis URL. If None, uses REDIS_URL env var.
230
+
231
+ Returns:
232
+ RedisCache instance.
233
+ """
234
+ global _redis_cache_instance
235
+
236
+ if _redis_cache_instance is None:
237
+ _redis_cache_instance = RedisCache(redis_url=redis_url)
238
+
239
+ return _redis_cache_instance
240
+