feat(cache): снапшот-кеш истории вместо pickle + префиксный limit + джиттер TTL + age-sweep
Docker Image CI / build (pull_request) Waiting to run
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user