194 lines
5.7 KiB
Python
194 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Legacy cache compatibility shim.
|
||
|
||
Sales/dashboard cache state lives in services.cache_manager. This module keeps
|
||
older imports working and only owns growth-analysis and slow-query helpers.
|
||
"""
|
||
|
||
import os
|
||
import pickle
|
||
from datetime import datetime, timezone, timedelta
|
||
from pathlib import Path
|
||
from services.cache_manager import (
|
||
_SALES_DF_CACHE,
|
||
_SALES_PROCESSED_CACHE,
|
||
_SALES_OPTIONS_CACHE,
|
||
_SALES_ANALYSIS_RESULT_CACHE,
|
||
_SALES_CACHE_TTL,
|
||
_DASHBOARD_DATA_CACHE,
|
||
_DASHBOARD_CACHE_TTL,
|
||
clear_sales_cache,
|
||
clear_dashboard_cache,
|
||
)
|
||
|
||
# 台北時區
|
||
TAIPEI_TZ = timezone(timedelta(hours=8))
|
||
|
||
# 快取 TTL 設定:sales TTL 以 cache_manager 為單一來源
|
||
_SALES_OPTIONS_TTL = 21600 # 選項快取: 6 小時
|
||
_SALES_RESULT_TTL = _SALES_CACHE_TTL
|
||
|
||
# ==========================================
|
||
# 成長分析快取
|
||
# ==========================================
|
||
_GROWTH_ANALYSIS_CACHE = {
|
||
'chart_data': None,
|
||
'kpi': None,
|
||
'timestamp': None,
|
||
'source_fingerprint': None,
|
||
}
|
||
_GROWTH_CACHE_TTL = 1800 # 成長分析快取: 30 分鐘
|
||
_BASE_DIR = Path(__file__).resolve().parents[1]
|
||
_GROWTH_SHARED_CACHE_FILE = _BASE_DIR / "data" / "growth_analysis_cache.pkl"
|
||
|
||
# ==========================================
|
||
# 慢查詢監控
|
||
# ==========================================
|
||
_SLOW_QUERY_STATS = {
|
||
'total_queries': 0,
|
||
'slow_queries': 0,
|
||
'very_slow_queries': 0,
|
||
'total_query_time_ms': 0,
|
||
'last_slow_query': None,
|
||
'last_slow_query_time': None,
|
||
}
|
||
_SLOW_QUERY_THRESHOLD_MS = 1000 # 慢查詢閾值: 1秒
|
||
_VERY_SLOW_QUERY_THRESHOLD_MS = 5000 # 極慢查詢閾值: 5秒
|
||
|
||
|
||
def track_query_time(query_name, duration_ms):
|
||
"""追蹤查詢時間,更新慢查詢統計"""
|
||
global _SLOW_QUERY_STATS
|
||
_SLOW_QUERY_STATS['total_queries'] += 1
|
||
_SLOW_QUERY_STATS['total_query_time_ms'] += duration_ms
|
||
|
||
if duration_ms >= _VERY_SLOW_QUERY_THRESHOLD_MS:
|
||
_SLOW_QUERY_STATS['very_slow_queries'] += 1
|
||
_SLOW_QUERY_STATS['slow_queries'] += 1
|
||
_SLOW_QUERY_STATS['last_slow_query'] = query_name
|
||
_SLOW_QUERY_STATS['last_slow_query_time'] = datetime.now(TAIPEI_TZ).isoformat()
|
||
elif duration_ms >= _SLOW_QUERY_THRESHOLD_MS:
|
||
_SLOW_QUERY_STATS['slow_queries'] += 1
|
||
_SLOW_QUERY_STATS['last_slow_query'] = query_name
|
||
_SLOW_QUERY_STATS['last_slow_query_time'] = datetime.now(TAIPEI_TZ).isoformat()
|
||
|
||
|
||
def get_slow_query_stats():
|
||
"""取得慢查詢統計資料"""
|
||
return _SLOW_QUERY_STATS.copy()
|
||
|
||
|
||
def clear_growth_cache():
|
||
"""清除成長分析快取"""
|
||
global _GROWTH_ANALYSIS_CACHE
|
||
_GROWTH_ANALYSIS_CACHE = {
|
||
'chart_data': None,
|
||
'kpi': None,
|
||
'timestamp': None,
|
||
'source_fingerprint': None,
|
||
}
|
||
try:
|
||
if os.path.exists(_GROWTH_SHARED_CACHE_FILE):
|
||
os.remove(_GROWTH_SHARED_CACHE_FILE)
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
def clear_all_cache():
|
||
"""清除所有快取"""
|
||
clear_sales_cache()
|
||
clear_dashboard_cache()
|
||
clear_growth_cache()
|
||
|
||
|
||
def is_cache_valid(timestamp, ttl_seconds):
|
||
"""
|
||
檢查快取是否有效
|
||
|
||
Args:
|
||
timestamp: 快取時間戳記
|
||
ttl_seconds: 快取有效期(秒)
|
||
|
||
Returns:
|
||
bool: True 表示快取有效
|
||
"""
|
||
if timestamp is None:
|
||
return False
|
||
now = datetime.now(TAIPEI_TZ)
|
||
if timestamp.tzinfo is None:
|
||
timestamp = timestamp.replace(tzinfo=TAIPEI_TZ)
|
||
age = (now - timestamp).total_seconds()
|
||
return age < ttl_seconds
|
||
|
||
|
||
def get_growth_cache():
|
||
"""取得成長分析快取"""
|
||
return _GROWTH_ANALYSIS_CACHE
|
||
|
||
|
||
def _is_growth_payload_valid(payload, source_fingerprint=None):
|
||
if not payload:
|
||
return False
|
||
if not payload.get('chart_data') or not payload.get('kpi'):
|
||
return False
|
||
if not is_cache_valid(payload.get('timestamp'), _GROWTH_CACHE_TTL):
|
||
return False
|
||
if source_fingerprint is not None:
|
||
return payload.get('source_fingerprint') == source_fingerprint
|
||
return True
|
||
|
||
|
||
def _load_growth_cache_file(source_fingerprint=None):
|
||
"""讀取跨 worker 成長分析快取,避免冷 worker 重新掃明細表。"""
|
||
global _GROWTH_ANALYSIS_CACHE
|
||
try:
|
||
if not os.path.exists(_GROWTH_SHARED_CACHE_FILE):
|
||
return False
|
||
with open(_GROWTH_SHARED_CACHE_FILE, 'rb') as f:
|
||
payload = pickle.load(f)
|
||
if not _is_growth_payload_valid(payload, source_fingerprint):
|
||
return False
|
||
_GROWTH_ANALYSIS_CACHE = payload
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _write_growth_cache_file(payload):
|
||
"""原子寫入成長分析共享快取。"""
|
||
cache_file = str(_GROWTH_SHARED_CACHE_FILE)
|
||
tmp_file = f"{cache_file}.{os.getpid()}.tmp"
|
||
try:
|
||
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
|
||
with open(tmp_file, 'wb') as f:
|
||
pickle.dump(payload, f, protocol=pickle.HIGHEST_PROTOCOL)
|
||
os.replace(tmp_file, cache_file)
|
||
except Exception:
|
||
try:
|
||
if os.path.exists(tmp_file):
|
||
os.remove(tmp_file)
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
def set_growth_cache(chart_data, kpi, source_fingerprint=None):
|
||
"""設定成長分析快取"""
|
||
global _GROWTH_ANALYSIS_CACHE
|
||
payload = {
|
||
'chart_data': chart_data,
|
||
'kpi': kpi,
|
||
'timestamp': datetime.now(TAIPEI_TZ),
|
||
'source_fingerprint': source_fingerprint,
|
||
}
|
||
_GROWTH_ANALYSIS_CACHE = payload
|
||
_write_growth_cache_file(payload)
|
||
|
||
|
||
def is_growth_cache_valid(source_fingerprint=None):
|
||
"""檢查成長分析快取是否有效"""
|
||
if _is_growth_payload_valid(_GROWTH_ANALYSIS_CACHE, source_fingerprint):
|
||
return True
|
||
return _load_growth_cache_file(source_fingerprint)
|