From 0f336b3d0c8ad7b2e67b6319185f0f1bee5326ae Mon Sep 17 00:00:00 2001 From: Barely Removable Date: Mon, 31 Aug 2026 20:25:25 -0700 Subject: [PATCH] Fix crash-loop: CronTrigger resolves its own timezone independent of the scheduler's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The America/Phoenix TZ fix earlier only passed timezone= to BackgroundScheduler's constructor. That wasn't enough: APScheduler's CronTrigger independently calls tzlocal's get_localzone() in its own __init__ when not given an explicit timezone, regardless of what the scheduler itself is configured with. This never surfaced in testing because schedules.json was always empty — the rebuild_jobs() loop that constructs CronTrigger never executed. It crashed for real once a real schedule existed (confirmed live: the already-configured "Pokemon Singles >10" 4x/day schedule crash-looped the container on every boot). Fixed by passing the same resolved timezone to every CronTrigger call. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01H6TgffxTvYhqxykAbVQg2v --- app.py | 2 +- scheduler.py | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index e6afb15..8fa95e0 100644 --- a/app.py +++ b/app.py @@ -8,7 +8,7 @@ import automation import scheduler import storage -APP_VERSION = "0.4.0" +APP_VERSION = "0.4.1" APP_VERSION_DATE = "2026-09-01" app = Flask(__name__) diff --git a/scheduler.py b/scheduler.py index f1778eb..f996b58 100644 --- a/scheduler.py +++ b/scheduler.py @@ -20,12 +20,19 @@ 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"))) + +# Resolved once and reused everywhere a timezone is needed. Must be +# passed explicitly at construction time, never left to be inferred — +# both BackgroundScheduler AND CronTrigger independently call tzlocal's +# get_localzone() in their own __init__ if not given one, which fails +# inside this container image for an arbitrary $TZ (no zoneinfo/tzdata +# wiring). Passing it to the scheduler alone is NOT enough — a +# CronTrigger built without its own explicit timezone still resolves +# get_localzone() itself, regardless of what the scheduler is configured +# with, and crashes the same way. Every CronTrigger(...) below must pass +# timezone=_TZ explicitly. +_TZ = pytz.timezone(os.environ.get("TZ", "UTC")) +_scheduler = BackgroundScheduler(timezone=_TZ) _started = False VALID_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] @@ -69,6 +76,7 @@ def rebuild_jobs(): day_of_week=",".join(days), hour=int(hour), minute=int(minute), + timezone=_TZ, ) _scheduler.add_job( enqueue_run,