File size: 2,302 Bytes
9a6dbb3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""LRU+TTL cache for captcha answers, keyed by perceptual hash."""

from __future__ import annotations

import time
from collections import OrderedDict
from dataclasses import dataclass
from threading import Lock
from typing import Optional


@dataclass
class _Entry:
    answer: str
    solver: str
    confidence: float
    expires_at: float
    hits: int = 0


class SolveCache:
    """Thread-safe in-memory LRU cache with TTL.

    Keys are perceptual hashes (hex strings). Values are the solved answer
    plus the solver that produced it. Entries expire after `ttl_seconds`
    and are evicted LRU when over capacity.
    """

    def __init__(self, ttl_seconds: int = 3600, max_entries: int = 10_000):
        self.ttl = ttl_seconds
        self.max = max_entries
        self._store: OrderedDict[str, _Entry] = OrderedDict()
        self._lock = Lock()
        self.hits = 0
        self.misses = 0

    def get(self, key: str) -> Optional[_Entry]:
        now = time.time()
        with self._lock:
            entry = self._store.get(key)
            if entry is None:
                self.misses += 1
                return None
            if entry.expires_at < now:
                del self._store[key]
                self.misses += 1
                return None
            entry.hits += 1
            self.hits += 1
            self._store.move_to_end(key)
            return entry

    def set(self, key: str, answer: str, solver: str, confidence: float) -> None:
        now = time.time()
        with self._lock:
            self._store[key] = _Entry(
                answer=answer,
                solver=solver,
                confidence=confidence,
                expires_at=now + self.ttl,
            )
            self._store.move_to_end(key)
            while len(self._store) > self.max:
                self._store.popitem(last=False)

    def stats(self) -> dict:
        with self._lock:
            total = self.hits + self.misses
            return {
                "size": len(self._store),
                "max": self.max,
                "hits": self.hits,
                "misses": self.misses,
                "hit_rate": (self.hits / total) if total else 0.0,
            }

    def clear(self) -> None:
        with self._lock:
            self._store.clear()