Poll for run completion every 5 minutes instead of checking once
All checks were successful
Deploy / deploy (push) Successful in 28s

The one-shot VERIFY_DELAY_MINUTES check was too rigid: MassPrice's
processing time varies widely in practice (observed ~1 to over 20
minutes, and its queue can back up further under load — confirmed
live, a run still sitting in "Queued" 15+ minutes in), so a single
check window either misses slow-but-fine completions or has to be set
needlessly long.

Replaced with a poll loop: scheduler.py reschedules another one-off
check every VERIFY_POLL_MINUTES (default 5) until automation's
check_completion() reports "complete", up to VERIFY_MAX_ATTEMPTS
(default 12, ~1 hour) before giving up and marking "not_confirmed" for
good. verify_run() is split into a pure check_completion() (no storage
side effects) so scheduler.py's loop owns the retry/give-up decision;
automation.py just answers "is it done yet".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6TgffxTvYhqxykAbVQg2v
This commit is contained in:
Barely Removable 2026-08-31 23:58:23 -07:00
parent 8b58dadc5a
commit 213837ff97
6 changed files with 151 additions and 90 deletions

View file

@ -189,7 +189,8 @@ def run_rule(rule_id, rule_name=None):
rule id. Logs the outcome to run_history.json and returns a dict:
{"status": "sent"|"failure", "message": ..., "entry_id": ...,
"watermark_activity_id": ...}. On "sent", the caller (scheduler.py)
schedules a delayed verify_run() call using entry_id/watermark_activity_id
starts a poll loop (check_completion(), every VERIFY_POLL_MINUTES,
up to VERIFY_MAX_ATTEMPTS times) using entry_id/watermark_activity_id
to confirm the run actually completed on MassPrice's side."""
status = "failure"
message = ""
@ -257,7 +258,7 @@ def run_rule(rule_id, rule_name=None):
entry_id = _log_history(rule_id, rule_name, status, message)
will_verify = status == "sent" and bool(rule_name)
if will_verify:
_set_verification_pending(entry_id)
set_verification_pending(entry_id)
return {
"status": status,
@ -268,12 +269,11 @@ def run_rule(rule_id, rule_name=None):
}
def verify_run(entry_id, rule_name, watermark_id):
"""Delayed follow-up: check whether the run logged as entry_id
actually completed on MassPrice's side by now. Updates that
run_history.json entry's "verification" field in place."""
verification = "not_confirmed"
verify_message = ""
def check_completion(rule_name, watermark_id):
"""One-shot check: has a new "Complete" entry appeared for rule_name
past watermark_id yet? Returns ("complete", "") or ("not_yet", msg).
Pure check — no storage side effects; the caller (scheduler.py's
poll loop) decides what that means for the stored history entry."""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=LAUNCH_ARGS)
try:
@ -284,21 +284,18 @@ def verify_run(entry_id, rule_name, watermark_id):
activity_id, status_text = _find_latest_activity(page, rule_name)
if activity_id is None:
verify_message = "No matching activity found on MassPrice"
elif watermark_id is not None and activity_id <= watermark_id:
verify_message = "No new completion appeared within the allotted time"
elif status_text and "complete" in status_text.lower():
verification = "complete"
else:
verify_message = f"Latest activity shows \"{status_text}\", not Complete"
return "not_yet", "No matching activity found on MassPrice"
if watermark_id is not None and activity_id <= watermark_id:
return "not_yet", "No new completion has appeared yet"
if status_text and "complete" in status_text.lower():
return "complete", ""
return "not_yet", f'Latest activity shows "{status_text}", not Complete yet'
except Exception as exc:
verify_message = str(exc)
logger.exception("verify_run failed for entry %s", entry_id)
logger.exception("check_completion failed for %s", rule_name)
return "not_yet", str(exc)
finally:
browser.close()
_update_verification(entry_id, verification, verify_message)
def _log_history(rule_id, rule_name, status, message):
entry_id = uuid.uuid4().hex
@ -312,11 +309,11 @@ def _log_history(rule_id, rule_name, status, message):
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": status,
"message": message,
# "pending" only when a verify_run() has actually been
# scheduled for this entry (status == "sent" and rule_name
# was known) — set explicitly by run_rule() below, not here,
# since _log_history() itself doesn't know whether scheduling
# succeeded.
# "pending" only when a completion poll loop has actually
# been started for this entry (status == "sent" and
# rule_name was known) — set explicitly by run_rule() below,
# not here, since _log_history() itself doesn't know whether
# scheduling succeeded.
"verification": None,
"verify_message": None,
},
@ -325,29 +322,44 @@ def _log_history(rule_id, rule_name, status, message):
return entry_id
def _set_verification_pending(entry_id):
history = storage.load("run_history.json", [])
def _find_history_entry(history, entry_id):
for entry in history:
if entry.get("id") == entry_id:
entry["verification"] = "pending"
break
else:
return entry
return None
def set_verification_pending(entry_id):
history = storage.load("run_history.json", [])
entry = _find_history_entry(history, entry_id)
if entry is None:
return
entry["verification"] = "pending"
storage.save("run_history.json", history)
def _update_verification(entry_id, verification, verify_message):
def set_verification_message(entry_id, message):
"""Record the latest poll's note without changing the terminal
state — used between retries, while still "pending"."""
history = storage.load("run_history.json", [])
for entry in history:
if entry.get("id") == entry_id:
entry["verification"] = verification
entry["verify_message"] = verify_message
entry["verified_at"] = datetime.now(timezone.utc).isoformat()
break
else:
entry = _find_history_entry(history, entry_id)
if entry is None:
# Entry aged out of the 200-cap between being logged and this
# delayed check running — nothing to update, not an error.
return
entry["verify_message"] = message
storage.save("run_history.json", history)
def update_verification(entry_id, verification, verify_message):
"""Set a terminal verification result ("complete" or "not_confirmed")."""
history = storage.load("run_history.json", [])
entry = _find_history_entry(history, entry_id)
if entry is None:
return
entry["verification"] = verification
entry["verify_message"] = verify_message
entry["verified_at"] = datetime.now(timezone.utc).isoformat()
storage.save("run_history.json", history)