feat(versions): daily 03:00 UTC auto-pin scan scheduler
BackgroundScheduler with a single CronTrigger fires scan_all_users_for _auto_pins via asyncio.run_coroutine_threadsafe (mirrors the journal- scheduler pattern). Wired into app startup/shutdown alongside the other schedulers.
This commit is contained in:
@@ -331,6 +331,12 @@ def create_app() -> Quart:
|
|||||||
from fabledassistant.services.event_scheduler import start_event_scheduler
|
from fabledassistant.services.event_scheduler import start_event_scheduler
|
||||||
start_event_scheduler(asyncio.get_running_loop())
|
start_event_scheduler(asyncio.get_running_loop())
|
||||||
|
|
||||||
|
# Start version-pinning scheduler (daily auto-pin scan at 03:00 UTC)
|
||||||
|
from fabledassistant.services.version_pinning_scheduler import (
|
||||||
|
start_version_pinning_scheduler,
|
||||||
|
)
|
||||||
|
start_version_pinning_scheduler(asyncio.get_running_loop())
|
||||||
|
|
||||||
# Voice model loading (enabled via Admin → Config in the UI, or VOICE_ENABLED env var)
|
# Voice model loading (enabled via Admin → Config in the UI, or VOICE_ENABLED env var)
|
||||||
from fabledassistant.services.stt import load_stt_model
|
from fabledassistant.services.stt import load_stt_model
|
||||||
from fabledassistant.services.tts import load_tts_model
|
from fabledassistant.services.tts import load_tts_model
|
||||||
@@ -343,6 +349,10 @@ def create_app() -> Quart:
|
|||||||
stop_journal_scheduler()
|
stop_journal_scheduler()
|
||||||
from fabledassistant.services.event_scheduler import stop_event_scheduler
|
from fabledassistant.services.event_scheduler import stop_event_scheduler
|
||||||
stop_event_scheduler()
|
stop_event_scheduler()
|
||||||
|
from fabledassistant.services.version_pinning_scheduler import (
|
||||||
|
stop_version_pinning_scheduler,
|
||||||
|
)
|
||||||
|
stop_version_pinning_scheduler()
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def serve_index():
|
async def serve_index():
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Daily APScheduler cron for the auto-pin scan.
|
||||||
|
|
||||||
|
Single global job at 03:00 UTC. Runs scan_all_users_for_auto_pins so the
|
||||||
|
system promotes stable note versions before they get aged out of the
|
||||||
|
rolling cap. Off-hours by design — the scan is cheap but not time-
|
||||||
|
critical and doesn't need to interrupt regular activity.
|
||||||
|
|
||||||
|
Mirrors the BackgroundScheduler + threadsafe-async-call pattern used by
|
||||||
|
journal_scheduler.py.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
from fabledassistant.services.version_pinning import scan_all_users_for_auto_pins
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_scheduler: BackgroundScheduler | None = None
|
||||||
|
_loop: asyncio.AbstractEventLoop | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _run_scan_threadsafe() -> None:
|
||||||
|
"""APScheduler invokes this from a worker thread; bridge into the
|
||||||
|
asyncio loop so the scan can await its DB operations."""
|
||||||
|
if _loop is None:
|
||||||
|
logger.warning("version_pinning scheduler: no loop registered")
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _runner():
|
||||||
|
try:
|
||||||
|
results = await scan_all_users_for_auto_pins()
|
||||||
|
total = sum(results.values())
|
||||||
|
if total > 0:
|
||||||
|
logger.info(
|
||||||
|
"auto-pin scan: pinned %d version(s) across %d user(s)",
|
||||||
|
total, len(results),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug("auto-pin scan: no new pins")
|
||||||
|
except Exception:
|
||||||
|
logger.exception("auto-pin scan run failed")
|
||||||
|
|
||||||
|
asyncio.run_coroutine_threadsafe(_runner(), _loop)
|
||||||
|
|
||||||
|
|
||||||
|
def start_version_pinning_scheduler(loop: asyncio.AbstractEventLoop) -> None:
|
||||||
|
global _scheduler, _loop
|
||||||
|
if _scheduler is not None:
|
||||||
|
return
|
||||||
|
_loop = loop
|
||||||
|
_scheduler = BackgroundScheduler()
|
||||||
|
_scheduler.add_job(
|
||||||
|
_run_scan_threadsafe,
|
||||||
|
trigger=CronTrigger(hour=3, minute=0, timezone="UTC"),
|
||||||
|
id="version_pinning_auto_scan",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
_scheduler.start()
|
||||||
|
logger.info("Version pinning scheduler started (daily 03:00 UTC)")
|
||||||
|
|
||||||
|
|
||||||
|
def stop_version_pinning_scheduler() -> None:
|
||||||
|
global _scheduler
|
||||||
|
if _scheduler is not None:
|
||||||
|
_scheduler.shutdown(wait=False)
|
||||||
|
_scheduler = None
|
||||||
|
logger.info("Version pinning scheduler stopped")
|
||||||
Reference in New Issue
Block a user