-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathcache.py
More file actions
281 lines (240 loc) · 9.43 KB
/
Copy pathcache.py
File metadata and controls
281 lines (240 loc) · 9.43 KB
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
"""Storage facade.
Upstream owns all cache logic in this module. The ElfHosted fork moves the
per-backend implementation into the ``storage`` package so a Postgres backend
can be selected via ``DATABASE_URL``, while composite poster BYTES move to the
``blobstore`` package (local FS default, S3/CDN via ``OBJECT_STORE_URL``).
Public function names and signatures are preserved exactly so every
``from cache import …`` callsite works unchanged on either backend. A thin
metrics wrapper around the most-trafficked lookups feeds cache hit/miss
counters to /metrics; the wrappers are otherwise pass-throughs. Backend
selection lives in storage/__init__.py.
Note: the final-poster trio (get/set/is_fresh) is ASYNC here because the
bytes live in the blobstore. Callers must ``await`` them.
Cherry-pick guide:
* Upstream changes to cache logic map almost 1-to-1 to
storage/sqlite_backend.py (which is seeded from upstream cache.py).
* The Postgres backend mirrors the same signatures; a new upstream cache
function needs a parallel addition in storage/postgres_backend.py and an
entry in storage/__init__.py's _PUBLIC_API.
* ``get_db`` is deliberately NOT re-exported. Upstream hands callers a raw
SQLite connection; here the backend may be Postgres, so any upstream code
that reaches for get_db() needs a named backend function instead (see
list_composite_request_params, which replaces the one such query in
main.py's trending refresh).
"""
# Pass-through re-exports (no instrumentation).
from storage import (
BACKEND_KIND,
init_db,
prune_caches,
prune_local_caches,
ping,
close,
get_cached_final_poster_url,
get_cached_final_poster_redirect,
set_cached_rating,
set_cached_quality,
get_cached_trending_snapshot,
set_cached_trending_snapshot,
set_cached_tmdb_poster,
set_cached_tmdb_logo,
set_cached_tmdb_metadata,
delete_cached_tmdb_metadata,
is_digital_release,
count_digital_releases,
add_digital_releases,
delete_cached_final_poster,
invalidate_final_posters,
composite_l1_stats,
release_status_ttl_seconds,
release_status_expiry,
get_cached_movie_release_info,
set_cached_movie_release_info,
get_cached_tvdb_json,
set_cached_tvdb_json,
get_app_state,
set_app_state,
claim_app_state_slot,
list_composite_request_params,
get_cached_imdb_to_tmdb,
set_cached_imdb_to_tmdb,
delete_cached_imdb_to_tmdb,
set_cached_release_status,
set_cached_text_detection,
get_cache_stats,
)
# Instrumented lookups — imported under private aliases, wrapped below.
from storage import (
get_cached_final_poster as _raw_get_final_poster,
get_cached_final_poster_entry as _raw_get_final_poster_entry,
set_cached_final_poster as _raw_set_final_poster,
is_cached_final_poster_fresh as _raw_is_final_fresh,
get_cached_rating as _raw_get_rating,
get_cached_quality as _raw_get_quality,
get_cached_tmdb_metadata as _raw_get_tmdb_metadata,
get_cached_tmdb_poster as _raw_get_tmdb_poster,
get_cached_tmdb_logo as _raw_get_tmdb_logo,
get_cached_release_status as _raw_get_release_status,
get_cached_text_detection as _raw_get_text_detection,
)
import metrics as _metrics
def _record(table: str, hit: bool) -> None:
_metrics.cache_lookups_total.labels(
table=table, result="hit" if hit else "miss",
).inc()
# --- Final composite poster (async — bytes live in the blobstore) ---------
async def get_cached_final_poster(cache_key):
r = await _raw_get_final_poster(cache_key)
_record("final_poster", r is not None)
return r
async def get_cached_final_poster_entry(cache_key):
"""(jpeg_bytes, expires_at) or None. Upstream reads composites through this
so the response can carry the composite's own Cache-Control deadline."""
r = await _raw_get_final_poster_entry(cache_key)
_record("final_poster", r is not None)
return r
async def is_cached_final_poster_fresh(cache_key) -> int | None:
"""Lightweight freshness probe — metadata row + TTL only, no blob fetch.
Lets /poster and /p 302 straight to the CDN when a public URL exists.
Returns the composite's expires_at when fresh, else None (truthy/falsy, so
boolean call sites are unaffected)."""
expires_at = await _raw_is_final_fresh(cache_key)
_record("final_poster", expires_at is not None)
return expires_at
async def set_cached_final_poster(
cache_key, jpeg_bytes, request_params=None, ttl_override=None
):
"""Async pass-through to the storage backend's blobstore-aware writer.
Returns the unix time this composite expires."""
return await _raw_set_final_poster(
cache_key, jpeg_bytes, request_params, ttl_override
)
# --- Sync lookups ----------------------------------------------------------
def get_cached_rating(imdb_id):
r = _raw_get_rating(imdb_id)
_record("rating", r is not None)
return r
def get_cached_quality(imdb_id, release_date=None):
r = _raw_get_quality(imdb_id, release_date)
_record("quality", r is not None)
return r
def get_cached_tmdb_metadata(cache_key):
r = _raw_get_tmdb_metadata(cache_key)
_record("tmdb_metadata", r is not None)
return r
def get_cached_tmdb_poster(cache_key):
r = _raw_get_tmdb_poster(cache_key)
_record("tmdb_poster", r is not None)
return r
def get_cached_tmdb_logo(cache_key):
r = _raw_get_tmdb_logo(cache_key)
_record("tmdb_logo", r is not None)
return r
def get_cached_release_status(cache_key):
r = _raw_get_release_status(cache_key)
_record("release_status", r is not None)
return r
def get_cached_text_detection(cache_key):
# None means "not cached"; True/False are both cache hits.
r = _raw_get_text_detection(cache_key)
_record("text_detection", r is not None)
return r
__all__ = [
"BACKEND_KIND",
"init_db",
"prune_caches",
"prune_local_caches",
"ping",
"close",
"get_cache_stats",
"get_cached_final_poster",
"get_cached_final_poster_entry",
"get_cached_final_poster_url",
"is_cached_final_poster_fresh",
"get_cached_final_poster_redirect",
"set_cached_final_poster",
"delete_cached_final_poster",
"invalidate_final_posters",
"composite_l1_stats",
"get_cached_rating",
"set_cached_rating",
"get_cached_quality",
"set_cached_quality",
"get_cached_trending_snapshot",
"set_cached_trending_snapshot",
"get_cached_tmdb_poster",
"set_cached_tmdb_poster",
"get_cached_tmdb_logo",
"set_cached_tmdb_logo",
"get_cached_tmdb_metadata",
"set_cached_tmdb_metadata",
"delete_cached_tmdb_metadata",
"get_cached_release_status",
"set_cached_release_status",
"get_cached_text_detection",
"set_cached_text_detection",
"is_digital_release",
"count_digital_releases",
"add_digital_releases",
"get_cached_imdb_to_tmdb",
"set_cached_imdb_to_tmdb",
"delete_cached_imdb_to_tmdb",
"release_status_ttl_seconds",
"release_status_expiry",
"get_cached_movie_release_info",
"set_cached_movie_release_info",
"get_cached_tvdb_json",
"set_cached_tvdb_json",
"get_app_state",
"set_app_state",
"claim_app_state_slot",
"list_composite_request_params",
]
# ---------------------------------------------------------------------------
# Backend attribute proxy
# ---------------------------------------------------------------------------
#
# Upstream owns every cache internal in this module, and upstream's tests reach
# straight for them: `cache._safe_cache_path`, `cache._quality_cache_context`,
# and `cache._initialised` / `cache._local.conn` to point the connection at an
# in-memory database. Splitting the implementation into storage/ moved all of
# that out from under them.
#
# The previous port answered that by editing each test to import the backend
# instead. That does not scale — v1.2.0 added four more test classes that poke
# at internals, and every upstream release would hand us more test files to
# re-patch and then re-merge.
#
# So instead the facade forwards: anything this module does not define itself
# is read from, and written to, the active backend. Upstream's tests run
# unmodified against either backend, and the only fork-owned test file is the
# one testing a fork-only feature.
#
# Reads that hit a name defined above (the wrapped lookups, the re-exports)
# resolve normally and never reach __getattr__; writes to those names stay
# local, which is what a caller monkeypatching the facade would mean.
import sys as _sys
import types as _types
import storage as _storage
class _BackendFacade(_types.ModuleType):
def __getattr__(self, name):
# Only called when normal module lookup has already failed.
try:
return getattr(_storage._backend, name)
except AttributeError:
raise AttributeError(
f"module 'cache' has no attribute {name!r} "
f"(nor does the active backend "
f"{_storage._backend.__name__!r})"
) from None
def __setattr__(self, name, value):
if name in self.__dict__ or name.startswith("__"):
object.__setattr__(self, name, value)
else:
setattr(_storage._backend, name, value)
def __delattr__(self, name):
if name in self.__dict__:
object.__delattr__(self, name)
else:
delattr(_storage._backend, name)
_sys.modules[__name__].__class__ = _BackendFacade