daily_auto_refresh.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. """每日自动刷新 —— 后台定时拉取 arXiv 新论文、智能缓存与用户行为触发."""
  2. from __future__ import annotations
  3. import asyncio
  4. import logging
  5. import time
  6. from typing import TYPE_CHECKING
  7. from ...settings import get_settings
  8. import contextlib
  9. if TYPE_CHECKING:
  10. from fastapi import FastAPI
  11. logger = logging.getLogger(__name__)
  12. _daily_compute_lock: asyncio.Lock | None = None
  13. def get_daily_compute_lock() -> asyncio.Lock:
  14. global _daily_compute_lock
  15. if _daily_compute_lock is None:
  16. _daily_compute_lock = asyncio.Lock()
  17. return _daily_compute_lock
  18. _EXCLUDE_MEANINGFUL_PREFIXES: tuple[str, ...] = (
  19. "/health",
  20. "/api/papers/meta/summary",
  21. "/api/papers/reading/calendar",
  22. )
  23. def request_updates_meaningful_activity(method: str, path: str) -> bool:
  24. p = path or ""
  25. if p in ("/", "/health"):
  26. return False
  27. for pref in _EXCLUDE_MEANINGFUL_PREFIXES:
  28. if p.startswith(pref):
  29. return False
  30. return not (method.upper() == "GET" and p.startswith("/api/papers/daily"))
  31. def touch_meaningful_activity_if_needed(app: FastAPI, method: str, path: str) -> None:
  32. if not request_updates_meaningful_activity(method, path):
  33. return
  34. with contextlib.suppress(Exception):
  35. app.state.last_meaningful_activity_monotonic = time.monotonic()
  36. async def daily_auto_refresh_loop(app: FastAPI) -> None:
  37. s = get_settings()
  38. if not s.papergraph_daily_auto_refresh:
  39. logger.info("每日论文后台自动刷新已关闭(PAPERGRAPH_DAILY_AUTO_REFRESH=0)")
  40. return
  41. idle = max(15, s.papergraph_daily_auto_refresh_idle_sec)
  42. poll = max(30, s.papergraph_daily_auto_refresh_poll_sec)
  43. grace = max(10, s.papergraph_daily_auto_refresh_startup_grace_sec)
  44. logger.info("每日论文后台自动刷新已启用:idle=%ss poll=%ss startup_grace=%ss", idle, poll, grace)
  45. await asyncio.sleep(grace)
  46. from starlette.concurrency import run_in_threadpool
  47. from ...api.dependencies import get_db_path, get_searcher
  48. from ...models.schemas import DailyPapersRequest
  49. from ...services.daily.daily_cache_store import get_cache
  50. from ...services.daily.daily_service import compute_daily_papers as compute_daily
  51. import datetime as _dt
  52. from ...services.papers.papers_helpers import daily_paper_identity_sig
  53. def _cache_nonempty(cached) -> bool:
  54. if not cached:
  55. return False
  56. try:
  57. return bool(cached.get("arxiv_selected") or []) or bool(cached.get("personalized") or [])
  58. except Exception:
  59. return False
  60. lock = get_daily_compute_lock()
  61. date_key = _dt.datetime.now().strftime("%Y-%m-%d")
  62. while True:
  63. try:
  64. await asyncio.sleep(poll)
  65. ts = getattr(app.state, "last_meaningful_activity_monotonic", None)
  66. if ts is not None and time.monotonic() - ts < idle:
  67. continue
  68. db_path = get_db_path()
  69. if _cache_nonempty(await run_in_threadpool(get_cache, db_path, date_key=date_key, cache_key='default')):
  70. continue
  71. if lock.locked():
  72. continue
  73. async with lock:
  74. if _cache_nonempty(await run_in_threadpool(get_cache, db_path, date_key=date_key, cache_key='default')):
  75. continue
  76. ts2 = getattr(app.state, "last_meaningful_activity_monotonic", None)
  77. if ts2 is not None and time.monotonic() - ts2 < idle:
  78. continue
  79. settings = get_settings()
  80. from ...services.papers import papers_converters
  81. body = DailyPapersRequest(force_refresh=False)
  82. logger.info("每日论文:后台自动拉取开始(当日无有效缓存且系统空闲)")
  83. await compute_daily(
  84. body=body, db_path=db_path, searcher=get_searcher(),
  85. daily_paper_identity_sig_fn=daily_paper_identity_sig,
  86. daily_arxiv_cs_categories=settings.get_daily_arxiv_cs_categories(),
  87. papergraph_to_api_fn=papers_converters.litpaper_to_api_paper,
  88. logger=logger,
  89. )
  90. logger.info("每日论文:后台自动拉取完成")
  91. except asyncio.CancelledError:
  92. logger.info("每日论文后台自动刷新任务已取消")
  93. raise
  94. except Exception:
  95. logger.exception("每日论文后台自动拉取失败(将按 poll 间隔重试)")
  96. def spawn_daily_auto_refresh(app: FastAPI) -> asyncio.Task:
  97. return asyncio.create_task(daily_auto_refresh_loop(app), name="papergraph_daily_auto_refresh")