feat: 交易日9:31清空全部缓存,保证开盘后数据全新
- services/cache.py 新增 clear_all() 清空整个 cache 表 - daily_collector.py 新增 cache_cleanup_loop() 后台循环:精确 sleep 到最近一个 工作日 9:31 清空缓存,跳过周末,出错1分钟重试 - main.py lifespan 启动清理任务,与采集任务一同优雅退出 与既有"非交易时段 TTL 截止到下次开盘前"的惰性过期形成双保险 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+9
-6
@@ -8,7 +8,7 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
from database import init_db
|
from database import init_db
|
||||||
from routes import stock, collections, shares, sectors, themes, core_stocks
|
from routes import stock, collections, shares, sectors, themes, core_stocks
|
||||||
from services.daily_collector import collector_loop
|
from services.daily_collector import collector_loop, cache_cleanup_loop
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -17,14 +17,17 @@ load_dotenv()
|
|||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
init_db()
|
init_db()
|
||||||
collector_task = asyncio.create_task(collector_loop())
|
collector_task = asyncio.create_task(collector_loop())
|
||||||
|
cache_cleanup_task = asyncio.create_task(cache_cleanup_loop())
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
collector_task.cancel()
|
for t in (collector_task, cache_cleanup_task):
|
||||||
try:
|
t.cancel()
|
||||||
await collector_task
|
for t in (collector_task, cache_cleanup_task):
|
||||||
except asyncio.CancelledError:
|
try:
|
||||||
pass
|
await t
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="AUV API", version="1.0.0", lifespan=lifespan)
|
app = FastAPI(title="AUV API", version="1.0.0", lifespan=lifespan)
|
||||||
|
|||||||
@@ -56,3 +56,13 @@ def clean_expired():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_all():
|
||||||
|
"""清空全部缓存(每日开盘后 9:31 调用,保证当日数据全新)"""
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
conn.execute("DELETE FROM cache")
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from datetime import datetime, time as dtime, timezone, timedelta
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from database import get_connection
|
from database import get_connection
|
||||||
|
from services.cache import clear_all
|
||||||
from services.themes import fetch_theme_list, _build_theme_graph
|
from services.themes import fetch_theme_list, _build_theme_graph
|
||||||
|
|
||||||
_CST = timezone(timedelta(hours=8))
|
_CST = timezone(timedelta(hours=8))
|
||||||
@@ -22,6 +23,9 @@ HOTMAP_TOP_N = 50 # 热点穿透采样题材数:涨幅榜+热度榜
|
|||||||
HOTMAP_CORE_LIMIT = 100 # 热点穿透核心股前100(按覆盖题材数降序)
|
HOTMAP_CORE_LIMIT = 100 # 热点穿透核心股前100(按覆盖题材数降序)
|
||||||
CORE_COVER_THRESHOLD = 2 # 热点穿透核心股门槛:覆盖题材数 ≥2
|
CORE_COVER_THRESHOLD = 2 # 热点穿透核心股门槛:覆盖题材数 ≥2
|
||||||
|
|
||||||
|
# 每日缓存清理:交易日 9:31 清空全部缓存,保证开盘后数据全新
|
||||||
|
CLEANUP_TIME = dtime(9, 31)
|
||||||
|
|
||||||
|
|
||||||
def _is_trading_day(d: datetime) -> bool:
|
def _is_trading_day(d: datetime) -> bool:
|
||||||
"""仅按工作日判断:周一至周五视为交易日,不处理法定节假日"""
|
"""仅按工作日判断:周一至周五视为交易日,不处理法定节假日"""
|
||||||
@@ -185,3 +189,45 @@ async def collector_loop(stop: Optional[asyncio.Event] = None) -> None:
|
|||||||
if stop is not None and stop.is_set():
|
if stop is not None and stop.is_set():
|
||||||
break
|
break
|
||||||
await asyncio.sleep(CHECK_INTERVAL_SECONDS)
|
await asyncio.sleep(CHECK_INTERVAL_SECONDS)
|
||||||
|
|
||||||
|
|
||||||
|
def _next_cleanup_dt(now: datetime) -> datetime:
|
||||||
|
"""计算下一个缓存清理时刻:最近一个工作日 9:31(今天已过则取下一个工作日)"""
|
||||||
|
for days in range(0, 8):
|
||||||
|
d = (now + timedelta(days=days)).date()
|
||||||
|
if d.weekday() >= 5: # 跳过周末
|
||||||
|
continue
|
||||||
|
dt = datetime(d.year, d.month, d.day, CLEANUP_TIME.hour, CLEANUP_TIME.minute, tzinfo=_CST)
|
||||||
|
if dt > now:
|
||||||
|
return dt
|
||||||
|
return now + timedelta(days=1) # 兜底:理论不可达
|
||||||
|
|
||||||
|
|
||||||
|
async def cache_cleanup_loop(stop: Optional[asyncio.Event] = None) -> None:
|
||||||
|
"""后台循环:每个交易日 9:31 清空全部缓存,保证开盘后读到全新数据
|
||||||
|
|
||||||
|
到点前精确 sleep 至 9:31;若进程在 9:31 后启动,则等下一个交易日。
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
now = datetime.now(_CST)
|
||||||
|
next_dt = _next_cleanup_dt(now)
|
||||||
|
delay = max(0, int((next_dt - now).total_seconds()))
|
||||||
|
if stop is not None:
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(stop.wait(), timeout=delay)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass # 到点
|
||||||
|
if stop.is_set():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
|
clear_all()
|
||||||
|
print(f"[cache-cleanup] 已清空全部缓存: {datetime.now(_CST).strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
print("[cache-cleanup] 清理异常:")
|
||||||
|
traceback.print_exc()
|
||||||
|
await asyncio.sleep(60) # 出错 1 分钟后再试
|
||||||
|
|||||||
Reference in New Issue
Block a user