Initial scaffold: Flask webui + APScheduler + Playwright automation for MassPrice rule scheduling
Some checks failed
Deploy / deploy (push) Failing after 51s
Some checks failed
Deploy / deploy (push) Failing after 51s
Drives massprice.tcgplayer.com's Run Rule action per-rule on a user-configured schedule (multiple times/day, day-of-week toggles), replacing MassPrice's native scheduler (capped at 15 schedules account-wide, one run/day, with observed clock drift). Deployed via Forgejo push-to-deploy on Unraid, matching arr-summary/card-grader. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H6TgffxTvYhqxykAbVQg2v
This commit is contained in:
commit
2fdb9be9b9
19 changed files with 1197 additions and 0 deletions
93
scheduler.py
Normal file
93
scheduler.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""In-process cron scheduling for rule runs.
|
||||
|
||||
A single background worker thread owns every Playwright run — both
|
||||
APScheduler-triggered runs and manual "Run Now" clicks are pushed onto
|
||||
one queue.Queue so exactly one browser session is ever active at a time.
|
||||
This must run in exactly one process; see gunicorn.conf.py's workers=1.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
import automation
|
||||
import storage
|
||||
|
||||
logger = logging.getLogger("massprice.scheduler")
|
||||
|
||||
_work_queue: "queue.Queue" = queue.Queue()
|
||||
# Timezone must be set at construction time, not via a later .configure()
|
||||
# call — APScheduler resolves a timezone immediately in __init__ via
|
||||
# tzlocal's system lookup, which fails inside this container image for
|
||||
# an arbitrary $TZ (no zoneinfo/tzdata wiring), so the explicit TZ env
|
||||
# var has to win before that lookup ever runs.
|
||||
_scheduler = BackgroundScheduler(timezone=pytz.timezone(os.environ.get("TZ", "UTC")))
|
||||
_started = False
|
||||
|
||||
VALID_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
|
||||
|
||||
def _worker_loop():
|
||||
while True:
|
||||
rule_id, rule_name = _work_queue.get()
|
||||
try:
|
||||
automation.run_rule(rule_id, rule_name)
|
||||
except Exception:
|
||||
logger.exception("Unhandled error running rule %s", rule_id)
|
||||
finally:
|
||||
_work_queue.task_done()
|
||||
|
||||
|
||||
def enqueue_run(rule_id, rule_name=None):
|
||||
_work_queue.put((rule_id, rule_name))
|
||||
|
||||
|
||||
def _job_id(rule_id, index):
|
||||
return f"rule-{rule_id}-{index}"
|
||||
|
||||
|
||||
def rebuild_jobs():
|
||||
"""Clear and re-add every APScheduler job from schedules.json."""
|
||||
for job in _scheduler.get_jobs():
|
||||
job.remove()
|
||||
|
||||
schedules = storage.load("schedules.json", {})
|
||||
for rule_id, rule in schedules.items():
|
||||
if not rule.get("enabled", True):
|
||||
continue
|
||||
for idx, entry in enumerate(rule.get("schedules", [])):
|
||||
days = [d for d in entry.get("days", []) if d in VALID_DAYS]
|
||||
time_str = entry.get("time")
|
||||
if not days or not time_str:
|
||||
continue
|
||||
hour, minute = time_str.split(":")
|
||||
trigger = CronTrigger(
|
||||
day_of_week=",".join(days),
|
||||
hour=int(hour),
|
||||
minute=int(minute),
|
||||
)
|
||||
_scheduler.add_job(
|
||||
enqueue_run,
|
||||
trigger=trigger,
|
||||
args=[rule_id, rule.get("name")],
|
||||
id=_job_id(rule_id, idx),
|
||||
replace_existing=True,
|
||||
misfire_grace_time=300,
|
||||
)
|
||||
logger.info("Rebuilt %d scheduled job(s)", len(_scheduler.get_jobs()))
|
||||
|
||||
|
||||
def start(tz=None):
|
||||
# tz is accepted for backwards-compat call sites but is a no-op now
|
||||
# that timezone is fixed at construction time above.
|
||||
global _started
|
||||
if _started:
|
||||
return
|
||||
rebuild_jobs()
|
||||
_scheduler.start()
|
||||
threading.Thread(target=_worker_loop, daemon=True).start()
|
||||
_started = True
|
||||
Loading…
Add table
Add a link
Reference in a new issue