File size: 2,046 Bytes
8b96826
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""MongoDB key-value cache with TTL. Replaces SQLite. Fail-open on every operation."""

import functools
import hashlib
import json
import logging
from datetime import datetime, timezone, timedelta

from app import config

logger = logging.getLogger("saarthi.cache")

_collection = None


def _get_collection():
    global _collection
    if _collection is None:
        from app.db import get_db
        col = get_db()["api_cache"]
        col.create_index("expires_at", expireAfterSeconds=0)
        _collection = col
    return _collection


def get(key: str):
    """Return cached value or None. Any failure is a miss."""
    try:
        doc = _get_collection().find_one({"key": key}, {"_id": 0, "value": 1})
        return doc["value"] if doc else None
    except Exception as error:
        logger.warning("Cache read failed (treating as miss): %s", error)
        return None


def set(key: str, value, ttl_seconds: int):
    """Write to cache. Any failure is silently skipped."""
    try:
        expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)
        _get_collection().replace_one(
            {"key": key},
            {"key": key, "value": value, "expires_at": expires_at},
            upsert=True,
        )
    except Exception as error:
        logger.warning("Cache write failed (skipping): %s", error)


def cached(ttl_seconds: int):
    """Decorator: cache a function's JSON-serializable result by its arguments."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            raw = json.dumps(
                [func.__module__, func.__name__, args, kwargs],
                sort_keys=True, default=str,
            )
            key = hashlib.sha256(raw.encode()).hexdigest()
            hit = get(key)
            if hit is not None:
                return hit
            result = func(*args, **kwargs)
            if result is not None:
                set(key, result, ttl_seconds)
            return result
        return wrapper
    return decorator