Files
ewoooc/services/cache_manager.py
OoO 13fa165ee2 refactor(cache): 統一 cache SOT 並啟用 gunicorn preload
ADR-017 Phase 3f-2:新增 services/cache_manager.py,讓 sales/import/export/daily/dashboard 共用同一份 in-memory cache;cache_service 改為相容 shim;Dockerfile/docker-compose 啟用 gunicorn --preload。
2026-04-29 21:35:56 +08:00

126 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快取單一來源模組。
ADR-017 Phase 3f-2: 將 sales/import/export/daily 會共同碰到的
module-level cache 收斂到這裡,避免各 route 各自持有一份 dict。
"""
import time
class FingerprintCache:
"""TTL + fingerprint 的小型 in-memory cache。"""
def __init__(self, name, fingerprint_fn):
self.name = name
self._store = {}
self._fp_fn = fingerprint_fn
def get(self, key, ttl=300):
entry = self._store.get(key)
if not entry:
return None
if time.time() - entry['ts'] > ttl:
return None
try:
if self._fp_fn() != entry['fp']:
return None
except Exception:
pass
return entry['data']
def set(self, key, data):
try:
fp = self._fp_fn()
except Exception:
fp = None
self._store[key] = {'data': data, 'ts': time.time(), 'fp': fp}
def clear(self):
self._store.clear()
_SALES_DF_CACHE = {}
_SALES_PROCESSED_CACHE = {}
_SALES_OPTIONS_CACHE = {}
_SALES_ANALYSIS_RESULT_CACHE = {}
_SALES_CACHE_MAX_ENTRIES = 10
_SALES_CACHE_TTL = 600
_DAILY_SALES_PROCESSED_CACHE = {}
_DASHBOARD_DATA_CACHE = {
'consolidated_data': None,
'consolidated_timestamp': None,
'today_start': None,
'full_data': None,
'full_timestamp': None,
}
_DASHBOARD_CACHE_TTL = 1800
def cleanup_sales_cache():
"""清理 sales 處理後快取的過期與超額條目。"""
current_time = time.time()
expired_keys = [
key for key, value in _SALES_PROCESSED_CACHE.items()
if value.get('time') and current_time - value['time'] > _SALES_CACHE_TTL
]
for key in expired_keys:
_SALES_PROCESSED_CACHE.pop(key, None)
if len(_SALES_PROCESSED_CACHE) > _SALES_CACHE_MAX_ENTRIES:
sorted_items = sorted(
[(key, value.get('time', 0)) for key, value in _SALES_PROCESSED_CACHE.items()],
key=lambda item: item[1],
)
for key, _ in sorted_items[:-_SALES_CACHE_MAX_ENTRIES]:
_SALES_PROCESSED_CACHE.pop(key, None)
def set_sales_processed_cache(key, entry, aliases=()):
"""寫入 sales cache並可同步寫入別名 key。"""
entry.setdefault('time', time.time())
_SALES_PROCESSED_CACHE[key] = entry
for alias in aliases:
_SALES_PROCESSED_CACHE[alias] = entry
cleanup_sales_cache()
def clear_sales_cache_for_table(table_name):
"""匯入資料後清除指定表對應的所有 sales cache key。"""
_SALES_DF_CACHE.pop(table_name, None)
keys_to_delete = [
key for key in list(_SALES_PROCESSED_CACHE.keys())
if key == table_name or key.startswith(f"{table_name}_")
]
for key in keys_to_delete:
_SALES_PROCESSED_CACHE.pop(key, None)
def clear_sales_cache():
"""清除所有 sales cache。"""
_SALES_DF_CACHE.clear()
_SALES_PROCESSED_CACHE.clear()
_SALES_OPTIONS_CACHE.clear()
_SALES_ANALYSIS_RESULT_CACHE.clear()
def clear_daily_sales_cache():
"""清除當日業績 cache。"""
_DAILY_SALES_PROCESSED_CACHE.clear()
def clear_dashboard_cache():
"""清除商品看板 cache。"""
_DASHBOARD_DATA_CACHE.clear()
_DASHBOARD_DATA_CACHE.update({
'consolidated_data': None,
'consolidated_timestamp': None,
'today_start': None,
'full_data': None,
'full_timestamp': None,
})