mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
* fix(cache): store keys too long to be a filename The calendar plugin's cache key joins every calendar id the user picked. On hdpi it passed 300 bytes; ext4 caps a filename at 255, so every write (the temp file, the direct-write fallback and the home-directory fallback) failed with ENAMETOOLONG, once an hour, and the final warning said "(permission denied)" whatever the error was. DiskCache.get_cache_path keeps a key of up to 200 UTF-8 bytes as its filename, exactly as before, and turns a longer one into its first 183 bytes (cut on a character boundary) plus a 16-hex-digit hash of the whole key. The temp file adds 15 bytes, so the longest name is 215. The shortened stem is itself short, so the web UI's cache list, which names a key by its filename, deletes the same file. The give-up warning now names the real error. Validated on ledpi's ext4: the old module drops the hdpi-shaped key, the new one writes a 205-byte filename and reads it back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cache): judge a memory hit by the record's own timestamp A record loaded from disk went into the memory tier timed from the load, so get(key, max_age=300) could return data close to 600 s old: after a restart, after the memory sweep, or in a second process. A stored ttl was stretched the same way. #728's _fresh_cached works around it for the scoreboard; every other caller was exposed. get_cached_data and load_cache now also check a memory hit against the record's embedded timestamp, with DiskCache.get's rule that a stored ttl wins over max_age. A stale copy is dropped and the read falls through to disk, which returns the other process's newer write if there is one. Records without a timestamp keep the memory tier's own clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.1 KiB
Python
96 lines
3.1 KiB
Python
"""The memory tier never serves a record older than the reader asked for.
|
|
|
|
A record read from disk went into the memory tier timed from the read, not
|
|
from when it was written, so get(max_age=300) could hand out data up to twice
|
|
that old: after a restart, after the memory sweep, or in a second process that
|
|
loaded a record once and kept serving it.
|
|
"""
|
|
|
|
import time
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from src.cache_manager import CacheManager
|
|
|
|
|
|
class Clock:
|
|
def __init__(self, now):
|
|
self.now = now
|
|
|
|
def __call__(self):
|
|
return self.now
|
|
|
|
|
|
@pytest.fixture
|
|
def clock(monkeypatch):
|
|
fake = Clock(1_800_000_000.0)
|
|
monkeypatch.setattr(time, "time", fake)
|
|
return fake
|
|
|
|
|
|
def _manager(path):
|
|
# No disk sweep: it judges files by their real mtime against the fake
|
|
# clock and would delete them as months old.
|
|
with patch('src.cache_manager.CacheManager._get_writable_cache_dir',
|
|
return_value=str(path)), \
|
|
patch('src.cache_manager.CacheManager.start_cleanup_thread'):
|
|
return CacheManager()
|
|
|
|
|
|
def test_a_record_loaded_late_expires_on_its_own_timestamp(tmp_path, clock):
|
|
writer = _manager(tmp_path)
|
|
writer.set("weather_current", {"t": 1})
|
|
|
|
reader = _manager(tmp_path) # a restart, or the other process
|
|
clock.now += 250
|
|
assert reader.get("weather_current", max_age=300) == {"t": 1}
|
|
|
|
clock.now += 100 # the data is 350 s old; it sat in memory for 100 s
|
|
assert reader.get("weather_current", max_age=300) is None
|
|
|
|
|
|
def test_a_stored_ttl_bounds_the_memory_copy_too(tmp_path, clock):
|
|
writer = _manager(tmp_path)
|
|
writer.set("odds_espn_football_nfl_401", {"spread": 6.5}, ttl=60)
|
|
|
|
reader = _manager(tmp_path)
|
|
clock.now += 55
|
|
assert reader.get("odds_espn_football_nfl_401", max_age=3600) == {"spread": 6.5}
|
|
|
|
clock.now += 60
|
|
assert reader.get("odds_espn_football_nfl_401", max_age=3600) is None
|
|
|
|
|
|
def test_a_stale_memory_copy_gives_way_to_a_newer_write_on_disk(tmp_path, clock):
|
|
writer = _manager(tmp_path)
|
|
reader = _manager(tmp_path)
|
|
writer.set("stocks_AAPL", {"price": 1})
|
|
assert reader.get("stocks_AAPL", max_age=300) == {"price": 1}
|
|
|
|
clock.now += 280
|
|
writer.set("stocks_AAPL", {"price": 2})
|
|
clock.now += 40 # reader's copy: 40 s in memory, 320 s old
|
|
|
|
assert reader.get("stocks_AAPL", max_age=300) == {"price": 2}
|
|
|
|
|
|
def test_fresh_records_are_still_served_from_memory(tmp_path, clock):
|
|
manager = _manager(tmp_path)
|
|
manager.set("news_NFL", {"items": []})
|
|
clock.now += 100
|
|
|
|
with patch.object(manager._disk_cache_component, "get") as disk_get:
|
|
assert manager.get("news_NFL", max_age=300) == {"items": []}
|
|
disk_get.assert_not_called()
|
|
|
|
|
|
def test_max_age_none_and_records_without_a_timestamp_never_expire(tmp_path, clock):
|
|
manager = _manager(tmp_path)
|
|
manager.set("plugin_health_x", {"ok": True})
|
|
manager.save_cache("raw_record", {"no": "timestamp"})
|
|
clock.now += 10 ** 6
|
|
|
|
assert manager.get("plugin_health_x", max_age=None) == {"ok": True}
|
|
assert manager.get_cached_data("raw_record", max_age=None) == {"no": "timestamp"}
|