feat(cache): снапшот-кеш истории вместо pickle + префиксный limit + джиттер TTL + age-sweep
Docker Image CI / build (pull_request) Waiting to run

Кеш истории (data/tgcache/) больше не хранит pickle живых объектов pyrogram.
Новый модуль message_snapshot.py: при записи из Message извлекается JSON-словарь
по явному allowlist (снапшот), при чтении восстанавливается duck-typed
CachedMessage (CachedStr с .html, mutable, deepcopy-safe), неотличимый для
пайплайна рендера от Message. post_parser.py и rss_generator.py НЕ изменены.

tg_cache.py переписан на generic JSON-store (pickle удалён полностью):
- _store_entry/_load_entry: атомарная запись через уникальный .tmp.<uuid4> +
  os.replace, finally чистит свой tmp; версия/TTL/битый JSON -> None;
- пути <safe_key>.history.json / .chatinfo.json;
- пакет B: префиксный limit (miss только когда cached_limit<limit и есть ещё);
- пакет F: джиттер TTL при записи (random.uniform(0.8,1.0), на чтении random не
  зовётся) + sweep_tgcache(age) + cleanup_legacy_cache_files;
- api_server.py: старт-cleanup+sweep в lifespan, per-pass sweep в cache_media_files,
  удаление legacy data/media_file_ids.json.

closes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 00:09:08 +03:00
parent 3c9ce72b51
commit 8d317e36c8
5 changed files with 1106 additions and 101 deletions
+19
View File
@@ -43,6 +43,7 @@ from file_io import (DB_PATH, init_db_sync, get_all_media_file_ids_sync,
update_media_file_access_sync, update_media_file_access_bulk_sync,
remove_media_file_ids_sync,
get_mime_type_sync, set_mime_type_sync)
from tg_cache import cleanup_legacy_cache_files, sweep_tgcache
# Global python-magic instance for MIME type detection
magic_mime = magic.Magic(mime=True)
@@ -240,6 +241,19 @@ async def lifespan(_: FastAPI):
# Initialize SQLite database (creates table if not present)
await asyncio.to_thread(init_db_sync, DB_PATH)
# One-shot startup maintenance of the history/chatinfo cache: drop legacy pickle files
# (which are now always a miss), age-sweep stale tgcache entries, and remove the
# pre-SQLite data/media_file_ids.json legacy dump (no longer read by any code).
await asyncio.to_thread(cleanup_legacy_cache_files)
await asyncio.to_thread(sweep_tgcache)
legacy_media_ids = os.path.join("data", "media_file_ids.json")
if os.path.exists(legacy_media_ids):
try:
await asyncio.to_thread(os.remove, legacy_media_ids)
logger.info(f"legacy_media_file_ids_removed: {legacy_media_ids}")
except OSError as e:
logger.warning(f"legacy_media_file_ids_remove_error: {e}")
await client.start()
# Supervise the background tasks: if either dies (not via cancellation) it is logged
# CRITICAL and restarted, so a crash can no longer silently stop cache sweeping or downloads.
@@ -911,6 +925,11 @@ async def cache_media_files() -> None:
logger.error(f"Failed to remove old entries from SQLite: {str(e)}")
await download_new_files(updated_media_files, cache_dir)
# Age-sweep the history/chatinfo cache once per pass (dead channels, orphaned
# uuid tmp files). MUST run off-loop via to_thread (blocking filesystem walk).
await asyncio.to_thread(sweep_tgcache)
await asyncio.sleep(delay) # Check every delay seconds
except Exception as e: