File size: 3,463 Bytes
594ed40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import redis.asyncio as redis
import json
from typing import Optional, Any
import sys
import os

# Add project root to sys.path to allow importing hot_cache
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))

from backend.app.config import settings
from hot_cache import LRUCache


class CacheService:
    """
    Cache service class using Redis with a fallback to in-memory cache.
    """

    _client: Optional[redis.Redis] = None
    _redis_unavailable: bool = False
    _memory_cache: LRUCache = LRUCache(max_size=100)

    async def _get_redis_client(self) -> Optional[redis.Redis]:
        """Initializes and returns a Redis client if available."""
        if self._redis_unavailable:
            return None

        if self._client is None:
            try:
                print("--- Initializing Redis Client ---")
                # Set a timeout to avoid long waits if Redis is not running
                client = redis.from_url(
                    settings.REDIS_URL, socket_connect_timeout=1, encoding="utf-8", decode_responses=True
                )
                await client.ping()
                self._client = client
                print("--- Redis Client Initialized Successfully. Redis caching is active. ---")
            except Exception as e:
                print(f"--- Redis connection failed: {e}. Falling back to in-memory cache. ---")
                self._redis_unavailable = True
                self._client = None
        return self._client

    async def get(self, key: str) -> Optional[Any]:
        """Fetches a value from the cache by key."""
        client = await self._get_redis_client()
        if client:
            try:
                cached_value = await client.get(key)
                if cached_value:
                    print(f"REDIS CACHE HIT for key: {key}")
                    return json.loads(cached_value)
            except Exception as e:
                print(f"Redis GET error: {e}. Disabling Redis for this session.")
                self.__class__._redis_unavailable = True # Use class attribute to disable for all instances
                self.__class__._client = None

        # Fallback to in-memory cache
        value = self._memory_cache.get(key)
        if value:
            print(f"MEMORY CACHE HIT for key: {key}")
            return value

        print(f"CACHE MISS for key: {key}")
        return None

    async def set(self, key: str, value: Any, ttl: int = 3600):
        """Sets a key-value pair in the cache with a TTL."""
        client = await self._get_redis_client()
        if client:
            try:
                value_to_cache = json.dumps(value, default=str)
                await client.set(key, value_to_cache, ex=ttl)
                print(f"REDIS CACHE SET for key: {key}")
                return
            except Exception as e:
                print(f"Redis SET error: {e}. Disabling Redis for this session.")
                self.__class__._redis_unavailable = True # Use class attribute to disable for all instances
                self.__class__._client = None

        # Fallback to in-memory cache
        self._memory_cache[key] = value
        print(f"MEMORY CACHE SET for key: {key}")

# --- Dependency Injection Function ---
async def get_cache_service() -> "CacheService":
    """
    Factory function for dependency injection.
    Returns an instance of the CacheService.
    """
    return CacheService()