115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
"""Large strings: memoize instead of re-paying ~15 regexes on every request.
|
|
|
|
``_redact_fn_cached`` excluded every string > 16,384 characters from the LRU,
|
|
to avoid evicting the thousands of small recurring strings and inflating RSS.
|
|
Measured consequence on a real 22 MB session: 59 large strings (29 unique,
|
|
0.76 MB) went through the full regex set on EVERY request, for an always
|
|
identical result -- 1.68 s per request, i.e. 99.9% of the recurring redaction
|
|
cost once the small-string cache is warm.
|
|
|
|
The fix adds a SECOND cache, dedicated to large strings, bounded both by entry
|
|
count AND by a maximum size per entry, so that RSS stays capped. Giant strings
|
|
(> cap) deliberately remain uncached.
|
|
|
|
Contract verified here:
|
|
1. a large string is redacted exactly as before;
|
|
2. the same large string is not recomputed on the second call;
|
|
3. the two caches stay separate (no cross-eviction);
|
|
4. a string above the cap is never retained.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from api import helpers # noqa: E402
|
|
from api.helpers import ( # noqa: E402
|
|
_REDACT_CACHE_MAX_TEXT_LEN,
|
|
_REDACT_LARGE_CACHE_MAX_TEXT_LEN,
|
|
_redact_fn_cached,
|
|
_redact_fn_uncached,
|
|
)
|
|
|
|
SECRET = "gh" + "p_" + "0123456789abcdefghijklmnopqrstuvwxyzAB"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_large_cache():
|
|
"""Do not let this file pollute the process-wide global state.
|
|
|
|
The cache is a module singleton shared by the whole suite. Clearing it
|
|
without restoring it makes the following tests start cold, which disturbs
|
|
those that depend on freshness windows or response times. We start from an
|
|
empty cache AND clear it again on exit, so that this file has no observable
|
|
effect outside of itself.
|
|
"""
|
|
helpers._redact_fn_large_lru.cache_clear()
|
|
yield
|
|
helpers._redact_fn_large_lru.cache_clear()
|
|
|
|
|
|
def _big(secret: str, size: int) -> str:
|
|
"""String > small-cache threshold, carrying a secret.
|
|
|
|
The filler must be long enough to reach ``size`` even beyond the large
|
|
cache cap, otherwise truncation yields a string that is too short and the
|
|
test no longer verifies what it claims.
|
|
"""
|
|
unit = "lorem ipsum dolor sit amet "
|
|
filler = unit * (size // len(unit) + 1)
|
|
text = (filler + secret + filler)[:size]
|
|
assert len(text) == size
|
|
return text
|
|
|
|
|
|
def test_large_string_redaction_matches_uncached():
|
|
text = _big(SECRET, _REDACT_CACHE_MAX_TEXT_LEN + 5000)
|
|
assert len(text) > _REDACT_CACHE_MAX_TEXT_LEN
|
|
assert _redact_fn_cached(text) == _redact_fn_uncached(text)
|
|
assert SECRET not in _redact_fn_cached(text)
|
|
|
|
|
|
def test_large_string_is_memoized():
|
|
helpers._redact_fn_large_lru.cache_clear()
|
|
text = _big(SECRET, _REDACT_CACHE_MAX_TEXT_LEN + 7000)
|
|
|
|
first = _redact_fn_cached(text)
|
|
misses_after_first = helpers._redact_fn_large_lru.cache_info().misses
|
|
|
|
second = _redact_fn_cached(text)
|
|
info = helpers._redact_fn_large_lru.cache_info()
|
|
|
|
assert second == first
|
|
assert info.hits >= 1, "large string recomputed on second call"
|
|
assert info.misses == misses_after_first, "unexpected additional miss"
|
|
|
|
|
|
def test_small_strings_do_not_use_the_large_cache():
|
|
helpers._redact_fn_large_lru.cache_clear()
|
|
small = f"small text {SECRET}"
|
|
assert len(small) <= _REDACT_CACHE_MAX_TEXT_LEN
|
|
_redact_fn_cached(small)
|
|
info = helpers._redact_fn_large_lru.cache_info()
|
|
assert info.hits == 0 and info.misses == 0, "small string routed to the large cache"
|
|
|
|
|
|
def test_giant_string_is_not_retained():
|
|
"""Above the cap: correct, but never cached (bounded RSS)."""
|
|
helpers._redact_fn_large_lru.cache_clear()
|
|
giant = _big(SECRET, _REDACT_LARGE_CACHE_MAX_TEXT_LEN + 10000)
|
|
assert len(giant) > _REDACT_LARGE_CACHE_MAX_TEXT_LEN
|
|
|
|
out = _redact_fn_cached(giant)
|
|
|
|
assert out == _redact_fn_uncached(giant)
|
|
assert SECRET not in out
|
|
info = helpers._redact_fn_large_lru.cache_info()
|
|
assert info.currsize == 0, "giant string retained in cache"
|
|
|
|
|
|
def test_large_cache_is_bounded():
|
|
assert helpers._redact_fn_large_lru.cache_info().maxsize is not None
|
|
assert helpers._redact_fn_large_lru.cache_info().maxsize <= 128
|