File size: 6,566 Bytes
0fea6ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c0aef4
0fea6ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Resolve TP53 once. Never pay for it again.

THE PROBLEM
-----------
Every gene-symbol and accession lookup goes out to Ensembl, UniProt or NCBI,
every time, for every user. `exon.py` has a process-local dict, but a Hugging
Face Space sleeps on idle and restarts cold, so in practice the same handful of
genes β€” TP53, GFP, Cas9, pUC19 β€” are re-fetched from scratch forever.

That costs three things: latency on the request a user is actually waiting on,
a shared outbound IP burning someone else's rate limit, and correctness β€” when
Ensembl is slow or throttling, a lookup that has succeeded a thousand times
fails, and the user is told their gene might not exist.

WHY THIS IS THE HONEST KIND OF "GETS SMARTER"
---------------------------------------------
It compounds with use and involves no model, no training and no inference. The
thousandth user asking for TP53 gets an instant answer *because* nine hundred
and ninety-nine people asked first. That is a real flywheel, and unlike a
learned one it cannot be wrong in a way nobody notices β€” the cached value is
byte-identical to what the database returned.

WHAT MAY AND MAY NOT BE CACHED
------------------------------
Only resolutions keyed by a PUBLIC identifier: (gene symbol, organism) or an
accession. Those keys are not personal, and the values are public database
records, which is why one shared cache across all users is correct rather than
a leak.

A pasted sequence is never cached. Not by content, not by hash, not as a key.
`kind == "sequence"` is refused at the door β€” see :func:`cacheable`. The
standing rule is that a user's own sequence never leaves the Space, and a
cross-user cache is very much leaving.

STALENESS
---------
Database records change: RefSeq versions increment, Ensembl re-annotates. So
entries carry a TTL and are re-fetched after it. The TTL is long because these
records are stable on the timescale of a design project, and a stale-by-a-week
CDS is a far smaller problem than the lookup failing outright.
"""
from __future__ import annotations

import re
import threading
import time
from collections import OrderedDict
from typing import Any, Dict, Optional

# Entries older than this are re-fetched. Sequence records are stable over
# weeks; this is about eventually noticing a re-annotation, not about
# freshness in any real-time sense.
TTL_SECONDS = 14 * 24 * 3600           # 14 days
MAX_ENTRIES = 2_000                    # in-process ceiling; ~50 MB worst case

# Never cached. A pasted sequence is the user's own data and a shared cache is
# by definition cross-user.
UNCACHEABLE_KINDS = {"sequence", "empty", "unknown"}

_LOCK = threading.Lock()
_MEM: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
_STATS = {"hits": 0, "misses": 0, "stores": 0, "evictions": 0, "refused": 0}


def make_key(kind: str, identifier: str, organism: str = "") -> str:
    """Stable cache key. Case- and whitespace-insensitive.

    Organism is part of the key because TP53 exists in dozens of species and
    they are different sequences β€” a key that dropped it would serve human
    TP53 to someone who asked for zebrafish, which is worse than a miss.
    """
    ident = re.sub(r"\s+", "", (identifier or "")).upper()
    org = re.sub(r"\s+", " ", (organism or "")).strip().lower()
    return f"{(kind or '').lower()}|{ident}|{org}"


def cacheable(kind: str, result: Optional[Dict[str, Any]] = None) -> bool:
    """May a resolution of this kind be stored in a SHARED cache?

    The gate that keeps user sequences out. Deliberately a whitelist-free
    check on the one thing that disqualifies β€” a pasted sequence β€” rather than
    a list of approved kinds, so a new public identifier type is cacheable the
    day it is added instead of silently bypassing the cache forever.
    """
    if (kind or "").lower() in UNCACHEABLE_KINDS:
        return False
    if result is not None and not result.get("ok"):
        # A failure is not cached. Ensembl being down for ten seconds must not
        # become "this gene does not exist" for the next fortnight.
        return False
    return True


def get(kind: str, identifier: str, organism: str = "") -> Optional[Dict[str, Any]]:
    """A previously resolved record, or None. Never raises."""
    if not cacheable(kind):
        return None
    key = make_key(kind, identifier, organism)
    now = time.time()
    with _LOCK:
        entry = _MEM.get(key)
        if entry is None:
            _STATS["misses"] += 1
            return None
        if now - entry["at"] > TTL_SECONDS:
            _MEM.pop(key, None)
            _STATS["misses"] += 1
            return None
        _MEM.move_to_end(key)          # LRU: a hit is a recency signal
        _STATS["hits"] += 1
        # A copy, so a caller mutating the result cannot poison the cache for
        # everyone else β€” the failure mode of a shared cache that is very hard
        # to trace back later.
        out = dict(entry["value"])
        out["cached"] = True
        out["cached_age_seconds"] = int(now - entry["at"])
        return out


def put(kind: str, identifier: str, result: Dict[str, Any],
        organism: str = "") -> bool:
    """Store a successful resolution. Returns whether it was stored."""
    if not cacheable(kind, result):
        with _LOCK:
            _STATS["refused"] += 1
        return False
    if not (result or {}).get("sequence"):
        return False
    key = make_key(kind, identifier, organism)
    # `cached` is a property of the read, not of the record. Storing it would
    # make the first served copy claim it came from cache.
    value = {k: v for k, v in result.items()
             if k not in ("cached", "cached_age_seconds")}
    with _LOCK:
        _MEM[key] = {"value": value, "at": time.time()}
        _MEM.move_to_end(key)
        _STATS["stores"] += 1
        while len(_MEM) > MAX_ENTRIES:
            _MEM.popitem(last=False)
            _STATS["evictions"] += 1
    return True


def stats() -> Dict[str, Any]:
    """Hit rate and size. The number worth watching is `hit_rate`: it is the
    whole claim that this compounds."""
    with _LOCK:
        total = _STATS["hits"] + _STATS["misses"]
        return {
            **_STATS,
            "entries": len(_MEM),
            "hit_rate": round(_STATS["hits"] / total, 3) if total else 0.0,
            "ttl_days": TTL_SECONDS // 86400,
        }


def clear() -> None:
    """Tests, and the admin path when a record is known to have changed."""
    with _LOCK:
        _MEM.clear()
        for k in _STATS:
            _STATS[k] = 0