feat(web): cache dashboard aggregations with version invalidation

This commit is contained in:
kairee committed 2026-11-01 03:27:49 +08:00
1 parent 91aa3e1f95
commit 9fd277c05f
15 files changed
+399 -30

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""看板缓存与版本失效测试(离线): uv run python tests/test_cache.py"""
import os
import sys
import tempfile
import time
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
from reaction_board.database import Database # noqa: E402
from reaction_board.repository import Repository # noqa: E402
from reaction_board.web.cache import TTLCache, build_cache_key # noqa: E402
def test_ttl_cache_expiry_and_params():
now = [1000.0]
cache = TTLCache(clock=lambda: now[0], max_items=32)
key = build_cache_key("db", "videos", 0, {"sort": "heat", "page": 1})
assert cache.get(key) is None
cache.set(key, 10.0, {"value": 1})
now[0] += 5
assert cache.get(key) == {"value": 1}
now[0] += 6
assert cache.get(key) is None
same = build_cache_key("db", "videos", 0, {"page": 1, "sort": "heat"})
assert same == key
other = build_cache_key("db", "videos", 1, {"page": 1, "sort": "heat"})
assert other != key
def test_ttl_cache_prunes_to_max_items():
now = [0.0]
cache = TTLCache(clock=lambda: now[0], max_items=16)
for index in range(40):
cache.set(("k", index), 100.0, index)
assert len(cache) <= 16
def test_repository_cache_version():
with tempfile.TemporaryDirectory() as tmp:
db = Database(Path(tmp) / "app.db")
db.initialize()
try:
repo = Repository(db)
assert repo.cache_version("videos") == 0
assert repo.bump_cache_version("videos") == 1
assert repo.bump_cache_version("videos") == 2
assert repo.cache_version("videos") == 2
assert repo.cache_version("fans") == 0
repo.bump_cache_version("fans")
assert repo.cache_version("fans") == 1
finally:
db.close()
def main_test():
test_ttl_cache_expiry_and_params()
test_ttl_cache_prunes_to_max_items()
test_repository_cache_version()
print("OK: 缓存 TTL/键/版本失效测试全部通过")
if __name__ == "__main__":
main_test()
+41
View File
@@ -11,6 +11,8 @@ sys.stdout.reconfigure(encoding="utf-8")
from reaction_board.config import ConfigStore # noqa: E402
from reaction_board.database import Database # noqa: E402
from reaction_board.repository import Repository # noqa: E402
from reaction_board.web.cache import DASHBOARD_CACHE # noqa: E402
from reaction_board.web.queries import DashboardQueries # noqa: E402
from tests.web_fixtures import DATES, build_db, write_config # noqa: E402
@@ -116,6 +118,7 @@ def test_growth_and_cold_start():
db.write(lambda conn, day=day: conn.execute(
"DELETE FROM snapshot WHERE stat_date = ?", (day,)
))
DASHBOARD_CACHE.clear()
cold = q.list_videos(sort="growth", growth_days=2)
assert cold["available"] is False
assert cold["items"] == []
@@ -197,6 +200,39 @@ def test_up_queries():
db.close()
def test_query_cache_invalidation():
with tempfile.TemporaryDirectory() as tmp:
db, q = setup(tmp)
try:
DASHBOARD_CACHE.clear()
first = q.overview()
db.write(lambda conn: conn.execute(
"UPDATE snapshot SET \"view\" = 999999 WHERE aid=101 AND stat_date=?",
(DATES[-1],),
))
cached = q.overview()
assert cached["totals"]["view"] == first["totals"]["view"]
repo = Repository(db)
repo.bump_cache_version("videos")
refreshed = q.overview()
assert refreshed["totals"]["view"] > first["totals"]["view"]
fans_first = q.fans(days=3)
db.write(lambda conn: conn.execute(
"UPDATE up_stat SET follower=999999 WHERE mid=37754047 AND stat_date=?",
(DATES[-1],),
))
fans_cached = q.fans(days=3)
assert fans_cached["follower"] == fans_first["follower"]
repo.bump_cache_version("fans")
fans_refreshed = q.fans(days=3)
assert fans_refreshed["follower"] == 999999
finally:
db.close()
def test_fans_custom_range_modes():
with tempfile.TemporaryDirectory() as tmp:
db, q = setup(tmp)
@@ -214,6 +250,7 @@ def test_fans_custom_range_modes():
"(stat_date, mid, follower, following, captured_at, ok) "
"VALUES ('2025-01-01', 37754047, 500, NULL, 1, 1)"
))
DASHBOARD_CACHE.clear()
monthly = q.fans(start="2025-01-01", end="2026-10-31")
assert monthly["mode"] == "monthly"
assert monthly["span_days"] == 669
@@ -229,6 +266,7 @@ def test_fans_custom_range_modes():
"VALUES ('2026-10-15', 37754047, 5000, NULL, 1, 1), "
"('2026-10-16', 37754047, 5000, NULL, 1, 1)"
))
DASHBOARD_CACHE.clear()
monthly = q.fans(start="2025-01-01", end="2026-10-31")
assert monthly["dates"][-1] == "2026-10-16"
assert monthly["series"][-1] == 5000
@@ -266,6 +304,7 @@ def test_fans_candidates_system_and_export():
"VALUES (?, ?, ?, NULL, 1, 1)",
("2026-10-01", 37754047, 900),
))
DASHBOARD_CACHE.clear()
recent = q.fans(days=90)
assert recent["recent_days"] == 14
assert [point["stat_date"] for point in recent["recent_points"]] == list(DATES)
@@ -281,6 +320,7 @@ def test_fans_candidates_system_and_export():
"DELETE FROM up_stat WHERE mid=37754047 AND stat_date=?",
("2026-10-30",),
))
DASHBOARD_CACHE.clear()
gapped = q.fans(days=3)
assert gapped["delta_1d"] is None
assert gapped["delta_7d"] is None
@@ -312,6 +352,7 @@ def main_test():
test_growth_and_cold_start()
test_video_detail_and_trend()
test_up_queries()
test_query_cache_invalidation()
test_fans_custom_range_modes()
test_fans_candidates_system_and_export()
print("OK: Web 查询层总览/榜单/详情/UP/粉丝/候选/系统测试全部通过")