|
| 1 | +import logging |
| 2 | +import pickle |
| 3 | + |
1 | 4 | from aizynthfinder.context.config import Configuration |
2 | 5 |
|
| 6 | +from src.reagentai.common.utils.redis import RedisManager |
3 | 7 | from src.reagentai.models.retrosynthesis import RouteCollection |
4 | 8 |
|
| 9 | +logger = logging.getLogger(__name__) |
| 10 | + |
5 | 11 |
|
6 | 12 | class RetrosynthesisCache: |
7 | 13 | """ |
8 | 14 | A cache for storing retrosynthesis routes based on target SMILES strings. |
9 | | - This class provides methods to add, retrieve, and clear cached routes. |
10 | | - It also maintains a configuration for the AiZynthFinder instance used in retrosynthesis. |
| 15 | + Supports both in-memory and Redis backends with automatic fallback. |
11 | 16 | """ |
12 | 17 |
|
13 | | - routes_cache: dict[str, RouteCollection] = {} |
| 18 | + # Class-level cache for fast access |
| 19 | + _memory_cache: dict[str, RouteCollection] = {} |
14 | 20 | finder_config: Configuration | None = None |
| 21 | + _cache_prefix = "retrosynthesis" |
| 22 | + _default_ttl = 86400 # 24 hours |
| 23 | + |
| 24 | + @classmethod |
| 25 | + def _serialize_data(cls, data: RouteCollection) -> bytes: |
| 26 | + """Serialize RouteCollection for Redis storage.""" |
| 27 | + try: |
| 28 | + return pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL) |
| 29 | + except Exception as e: |
| 30 | + logger.error(f"Failed to serialize data: {e}") |
| 31 | + raise |
15 | 32 |
|
16 | 33 | @classmethod |
17 | | - def add(cls, target_smile: str, data: RouteCollection): |
18 | | - cls.routes_cache[target_smile] = data |
| 34 | + def _deserialize_data(cls, data: bytes) -> RouteCollection: |
| 35 | + """Deserialize RouteCollection from Redis storage.""" |
| 36 | + try: |
| 37 | + return pickle.loads(data) |
| 38 | + except Exception as e: |
| 39 | + logger.error(f"Failed to deserialize data: {e}") |
| 40 | + raise |
| 41 | + |
| 42 | + @classmethod |
| 43 | + def _get_cache_key(cls, target_smile: str) -> str: |
| 44 | + """Generate standardized cache key.""" |
| 45 | + normalized_smile = target_smile.strip().lower() |
| 46 | + return f"{cls._cache_prefix}:{normalized_smile}" |
| 47 | + |
| 48 | + @classmethod |
| 49 | + def add(cls, target_smile: str, data: RouteCollection, ttl: int | None = None) -> bool: |
| 50 | + """Add route collection to cache.""" |
| 51 | + if not target_smile or not data: |
| 52 | + logger.warning("Invalid input for cache add operation") |
| 53 | + return False |
| 54 | + |
| 55 | + # Always store in memory cache |
| 56 | + cls._memory_cache[target_smile] = data |
| 57 | + |
| 58 | + # Attempt Redis storage |
| 59 | + ttl = ttl or cls._default_ttl |
| 60 | + cache_key = cls._get_cache_key(target_smile) |
| 61 | + |
| 62 | + with RedisManager.get_client() as redis_client: |
| 63 | + if redis_client: |
| 64 | + try: |
| 65 | + serialized_data = cls._serialize_data(data) |
| 66 | + result = redis_client.setex(cache_key, ttl, serialized_data) |
| 67 | + if result: |
| 68 | + logger.debug(f"Cached to Redis: {cache_key}") |
| 69 | + return True |
| 70 | + except Exception as e: |
| 71 | + logger.warning(f"Failed to cache to Redis: {e}") |
| 72 | + |
| 73 | + logger.debug(f"Cached to memory only: {target_smile}") |
| 74 | + return True |
19 | 75 |
|
20 | 76 | @classmethod |
21 | 77 | def get(cls, target_smile: str) -> RouteCollection | None: |
22 | | - return cls.routes_cache.get(target_smile) |
| 78 | + """Retrieve route collection from cache.""" |
| 79 | + if not target_smile: |
| 80 | + return None |
| 81 | + |
| 82 | + # Check memory cache first |
| 83 | + if target_smile in cls._memory_cache: |
| 84 | + logger.debug(f"Cache hit (memory): {target_smile}") |
| 85 | + return cls._memory_cache[target_smile] |
| 86 | + |
| 87 | + # Check Redis cache |
| 88 | + cache_key = cls._get_cache_key(target_smile) |
| 89 | + |
| 90 | + with RedisManager.get_client() as redis_client: |
| 91 | + if redis_client: |
| 92 | + try: |
| 93 | + cached_data = redis_client.get(cache_key) |
| 94 | + if cached_data and isinstance(cached_data, bytes): |
| 95 | + data = cls._deserialize_data(cached_data) |
| 96 | + cls._memory_cache[target_smile] = data |
| 97 | + logger.debug(f"Cache hit (Redis): {target_smile}") |
| 98 | + return data |
| 99 | + except Exception as e: |
| 100 | + logger.warning(f"Failed to retrieve from Redis: {e}") |
| 101 | + |
| 102 | + logger.debug(f"Cache miss: {target_smile}") |
| 103 | + return None |
| 104 | + |
| 105 | + @classmethod |
| 106 | + def delete(cls, target_smile: str) -> bool: |
| 107 | + """Delete specific entry from cache.""" |
| 108 | + if not target_smile: |
| 109 | + return False |
| 110 | + |
| 111 | + cls._memory_cache.pop(target_smile, None) |
| 112 | + cache_key = cls._get_cache_key(target_smile) |
| 113 | + |
| 114 | + with RedisManager.get_client() as redis_client: |
| 115 | + if redis_client: |
| 116 | + try: |
| 117 | + result = redis_client.delete(cache_key) |
| 118 | + logger.debug(f"Deleted from cache: {target_smile}") |
| 119 | + return bool(result) |
| 120 | + except Exception as e: |
| 121 | + logger.warning(f"Failed to delete from Redis: {e}") |
| 122 | + |
| 123 | + return True |
| 124 | + |
| 125 | + @classmethod |
| 126 | + def clear(cls) -> bool: |
| 127 | + """Clear all cached routes.""" |
| 128 | + cls._memory_cache.clear() |
| 129 | + |
| 130 | + with RedisManager.get_client() as redis_client: |
| 131 | + if redis_client: |
| 132 | + try: |
| 133 | + pipeline = redis_client.pipeline() |
| 134 | + for key in redis_client.scan_iter(match=f"{cls._cache_prefix}:*", count=100): |
| 135 | + pipeline.delete(key) |
| 136 | + pipeline.execute() |
| 137 | + logger.info("Cleared Redis cache") |
| 138 | + return True |
| 139 | + except Exception as e: |
| 140 | + logger.warning(f"Failed to clear Redis cache: {e}") |
| 141 | + |
| 142 | + logger.info("Cleared memory cache") |
| 143 | + return True |
23 | 144 |
|
24 | 145 | @classmethod |
25 | | - def clear(cls): |
26 | | - cls.routes_cache.clear() |
| 146 | + def close(cls): |
| 147 | + """Close Redis connections and cleanup resources.""" |
| 148 | + RedisManager.close() |
0 commit comments