加速成長分析指紋快取
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s

This commit is contained in:
OoO
2026-05-19 13:24:11 +08:00
parent 4a0a8bf75b
commit 1d3da03eee
5 changed files with 84 additions and 4 deletions

View File

@@ -228,6 +228,12 @@ def import_excel():
message = '匯入完成,但所有資料皆已存在 (重複),無新增數據。'
clear_sales_cache_for_table(table_name)
if table_name == 'realtime_sales_monthly':
try:
from services.cache_service import clear_growth_cache
clear_growth_cache()
except Exception as cache_error:
sys_log.warning(f"[Web] [Cache] 成長分析快取清除失敗: {cache_error}")
sys_log.info(f"[Web] [Cache] 已清除業績分析快取: {table_name}")
return jsonify({'status': 'success', 'message': message, 'rows': rows_imported, 'table': table_name})
@@ -241,6 +247,12 @@ def import_excel():
df.to_sql(table_name, con=engine, if_exists='replace', index=False)
clear_sales_cache_for_table(table_name)
if table_name == 'realtime_sales_monthly':
try:
from services.cache_service import clear_growth_cache
clear_growth_cache()
except Exception as cache_error:
sys_log.warning(f"[Web] [Cache] 成長分析快取清除失敗: {cache_error}")
sys_log.info(f"[Web] [Cache] 已清除業績分析快取: {table_name}")
return jsonify({

View File

@@ -466,6 +466,18 @@ def _get_growth_source_fingerprint(engine, table_name='realtime_sales_monthly'):
if not inspect(engine).has_table(table_name):
return None
table_name = validate_table_name(table_name)
if engine.dialect.name == 'postgresql':
from services.cache_service import (
get_growth_source_fingerprint_cache,
set_growth_source_fingerprint_cache,
)
cache_key = f"{engine.url}|{table_name}"
cached_fingerprint = get_growth_source_fingerprint_cache(cache_key)
if cached_fingerprint is not None:
return cached_fingerprint
else:
cache_key = None
table_ref = f'"{table_name}"'
if engine.dialect.name == 'postgresql':
fingerprint_sql = text(f"""
@@ -481,7 +493,10 @@ def _get_growth_source_fingerprint(engine, table_name='realtime_sales_monthly'):
""")
with engine.connect() as conn:
row = conn.execute(fingerprint_sql).fetchone()
return (row[0], row[1]) if row else None
fingerprint = (row[0], row[1]) if row else None
if cache_key is not None:
set_growth_source_fingerprint_cache(cache_key, fingerprint)
return fingerprint
except Exception as exc:
sys_log.warning(f"[GrowthAnalysis] 快取指紋查詢失敗,保守沿用 TTL: {exc}")
return None

View File

@@ -9,6 +9,7 @@ older imports working and only owns growth-analysis and slow-query helpers.
import os
import pickle
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
from services.cache_manager import (
@@ -42,6 +43,8 @@ _GROWTH_ANALYSIS_CACHE = {
_GROWTH_CACHE_TTL = 1800 # 成長分析快取: 30 分鐘
_BASE_DIR = Path(__file__).resolve().parents[1]
_GROWTH_SHARED_CACHE_FILE = _BASE_DIR / "data" / "growth_analysis_cache.pkl"
_GROWTH_SOURCE_FINGERPRINT_CACHE = {}
_GROWTH_SOURCE_FINGERPRINT_TTL = 60
# ==========================================
# 慢查詢監控
@@ -89,6 +92,7 @@ def clear_growth_cache():
'timestamp': None,
'source_fingerprint': None,
}
_GROWTH_SOURCE_FINGERPRINT_CACHE.clear()
try:
if os.path.exists(_GROWTH_SHARED_CACHE_FILE):
os.remove(_GROWTH_SHARED_CACHE_FILE)
@@ -128,6 +132,35 @@ def get_growth_cache():
return _GROWTH_ANALYSIS_CACHE
def get_growth_source_fingerprint_cache(cache_key):
"""讀取短 TTL source fingerprint避免每次命中頁面快取仍掃大表 COUNT。"""
entry = _GROWTH_SOURCE_FINGERPRINT_CACHE.get(cache_key)
if not entry:
return None
if time.time() - entry.get('time', 0) >= _GROWTH_SOURCE_FINGERPRINT_TTL:
_GROWTH_SOURCE_FINGERPRINT_CACHE.pop(cache_key, None)
return None
return entry.get('fingerprint')
def set_growth_source_fingerprint_cache(cache_key, fingerprint):
"""寫入 source fingerprint 短快取None 不快取,維持 fail-open。"""
if fingerprint is None:
return
_GROWTH_SOURCE_FINGERPRINT_CACHE[cache_key] = {
'fingerprint': fingerprint,
'time': time.time(),
}
def clear_growth_source_fingerprint_cache(cache_key=None):
"""清除 source fingerprint 快取;匯入流程會呼叫 clear_growth_cache 一併處理。"""
if cache_key is None:
_GROWTH_SOURCE_FINGERPRINT_CACHE.clear()
else:
_GROWTH_SOURCE_FINGERPRINT_CACHE.pop(cache_key, None)
def _is_growth_payload_valid(payload, source_fingerprint=None):
if not payload:
return False

View File

@@ -701,9 +701,13 @@ class ImportService:
session.close()
logger.info(f"任務 {job_id} 匯入成功: {total_rows}")
# cache 失效改靠 _get_data_fingerprintDB max(snapshot_date)+count(*)
# 寫入後指紋自動跳號4 worker 下一次 request 時各自偵測失效,
# 取代不可靠的 N-POST hack命中率僅 9.4%,見 web-researcher 報告)。
try:
from services.cache_service import clear_growth_cache
clear_growth_cache()
except Exception as cache_error:
logger.warning(f"任務 {job_id} 成長分析快取清除失敗: {cache_error}")
# daily_sales cache 失效仍靠 _get_data_fingerprintDB max(snapshot_date)+count(*)
# growth cache 另有 shared file + source fingerprint匯入後主動清掉避免短 TTL 內看到舊圖。
return True
except Exception as e:

View File

@@ -159,6 +159,22 @@ def test_clear_growth_cache_removes_shared_file(tmp_path, monkeypatch):
assert not shared_cache.exists()
def test_growth_source_fingerprint_short_cache_clears_with_growth_cache(tmp_path, monkeypatch):
from services import cache_service
shared_cache = tmp_path / "growth_analysis_cache.pkl"
monkeypatch.setattr(cache_service, "_GROWTH_SHARED_CACHE_FILE", shared_cache)
cache_service.clear_growth_cache()
cache_service.set_growth_source_fingerprint_cache("growth:source", ("2026-05-17", 10))
assert cache_service.get_growth_source_fingerprint_cache("growth:source") == ("2026-05-17", 10)
cache_service.clear_growth_cache()
assert cache_service.get_growth_source_fingerprint_cache("growth:source") is None
def test_daily_sales_shared_view_cache_roundtrip(tmp_path, monkeypatch):
from routes import daily_sales_routes