dy-dashboard/services/simple_cache.py
2026-10-28 11:14:17 +08:00

110 lines
2.8 KiB
Python

"""简单的跨进程文件缓存。
Gunicorn worker 共享缓存文件;任一进程删除或重建缓存后,其他 worker
会通过文件签名变化立即放弃旧的内存副本。
"""
import hashlib
import os
import pickle
import tempfile
import time
_cache = {}
_file_signatures = {}
CACHE_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'tmp_cache',
'simple',
)
def _path(key):
os.makedirs(CACHE_DIR, exist_ok=True)
filename = hashlib.md5(key.encode('utf-8')).hexdigest() + '.cache'
return os.path.join(CACHE_DIR, filename)
def _signature(path):
try:
stat = os.stat(path)
return stat.st_ino, stat.st_mtime_ns, stat.st_size
except OSError:
return None
def get(key):
path = _path(key)
current_signature = _signature(path)
entry = _cache.get(key)
if entry is not None and current_signature == _file_signatures.get(key) and current_signature is not None:
if entry['expire'] > time.time():
return entry['data']
delete(key)
return None
# 文件已被其他 worker 删除或替换,不能继续使用本进程的旧副本。
_cache.pop(key, None)
_file_signatures.pop(key, None)
if current_signature is None:
return None
try:
with open(path, 'rb') as f:
entry = pickle.load(f)
except (OSError, EOFError, pickle.PickleError, ValueError, TypeError):
return None
if not isinstance(entry, dict) or entry.get('expire', 0) <= time.time():
delete(key)
return None
_cache[key] = entry
_file_signatures[key] = _signature(path)
return entry['data']
def set(key, data, ttl=60):
entry = {'data': data, 'expire': time.time() + ttl}
path = _path(key)
fd, temp_path = tempfile.mkstemp(dir=CACHE_DIR, suffix='.tmp')
try:
with os.fdopen(fd, 'wb') as f:
pickle.dump(entry, f, protocol=pickle.HIGHEST_PROTOCOL)
os.replace(temp_path, path)
except Exception:
try:
os.unlink(temp_path)
except OSError:
pass
raise
_cache[key] = entry
_file_signatures[key] = _signature(path)
def delete(key):
"""删除指定缓存项;共享文件消失后其他 worker 会立即失效。"""
_cache.pop(key, None)
_file_signatures.pop(key, None)
try:
os.remove(_path(key))
except FileNotFoundError:
pass
def clear(key=None):
if key:
delete(key)
return
_cache.clear()
_file_signatures.clear()
try:
for filename in os.listdir(CACHE_DIR):
if filename.endswith('.cache'):
try:
os.remove(os.path.join(CACHE_DIR, filename))
except FileNotFoundError:
pass
except FileNotFoundError:
pass