Files
winnow/scheduler.py
flan 0906342edf fix: 10 correctness bugs from full codebase audit
- immich_api: .get("items") or [] handles {"items": null} without crashing len()
- immich_api: catch TypeError alongside ValueError in filter_recent_assets for
  timezone-naive fileCreatedAt comparisons
- scheduler: catch SystemExit in addition to KeyboardInterrupt so cli.main()
  cannot kill the long-running scheduler process
- scheduler: reseed croniter from wall-clock time after each run so overrunning
  jobs don't schedule an immediate back-to-back rerun
- config: reject negative YEARS_FILTER values with a warning, reset to default 10
- frigate_api: return True (not False) for empty filenames list — callers cannot
  distinguish no-op from network failure on False
- jobs: warn on unrecognised STRATEGY value instead of silently falling back
- jobs: casefold ONLY_PEOPLE / SKIP_PEOPLE matching so "john doe" matches "John Doe"
- executor: <= → < so a same-score candidate can fill a freed replacement slot
- embeddings: set _insightface_loaded=True on GPU+CPU double-failure to prevent
  N re-init attempts (one per asset) when InsightFace is broken for a whole run
2026-06-17 19:12:58 +00:00

65 lines
2.1 KiB
Python

#!/usr/bin/env python3
import logging
import os
import sys
import time
from pathlib import Path
try:
from croniter import croniter
except ImportError:
print("croniter not installed. Run: uv add croniter")
sys.exit(1)
# Imported at module level so models loaded during the first run stay
# resident in memory across all subsequent scheduled runs.
from winnow.cli import main
logger = logging.getLogger(__name__)
def _check_models() -> None:
insightface_home = os.environ.get("INSIGHTFACE_HOME", "/models/.insightface")
buffalo = Path(insightface_home) / "models" / "buffalo_l"
if not buffalo.exists():
print(" InsightFace Buffalo_L not found — will download on first run", flush=True)
def _run_scheduler() -> None:
schedule = os.environ.get("CRON_SCHEDULE")
if not schedule:
print("Error: CRON_SCHEDULE environment variable is required.", flush=True)
sys.exit(1)
try:
Path("/tmp/winnow.pid").write_text(str(os.getpid()))
except OSError as e:
print(f"Warning: could not write PID file: {e}", flush=True)
now = time.time()
cron = croniter(schedule, now)
next_run = cron.get_next(float)
print(f"Next run: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(next_run))}", flush=True)
while True:
now = time.time()
if now >= next_run:
print(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] Starting winnow run...", flush=True)
_check_models()
try:
main()
print("winnow run complete", flush=True)
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
logger.error("winnow run failed: %s", e, exc_info=True)
print(f"winnow run failed: {e}", flush=True)
cron = croniter(schedule, time.time())
next_run = cron.get_next(float)
print(f"Next run: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(next_run))}", flush=True)
time.sleep(min(60, max(1, next_run - time.time())))
if __name__ == "__main__":
_run_scheduler()