fix(sync): 限流退避持久化到 DB,调度器与手动同步均尊重退避期

db.py: 修复迁移非幂等——gunicorn 双 worker 并发 init_db 时后到者遇 Duplicate column 会让整个服务起不来,改为吞掉 duplicate column 错误。garmin.py/scheduler.py: 退避期写入 sync_status.rate_limited_until,自动同步与手动同步在退避期内跳过,不再反复撞击 Garmin 把限流窗口越撞越深。
This commit is contained in:
ericwyuan
2026-08-28 14:54:10 +08:00
parent a4bedc263f
commit 11869c6a8d
3 changed files with 125 additions and 8 deletions

View File

@@ -422,6 +422,10 @@ MIGRATIONS = {
# Which part of the sync is running. "0 / 730 天" says nothing about
# what is actually happening for the several minutes of it.
("stage", "VARCHAR(64)"),
# When Garmin answered 429 we stand the account down. Persisted so every
# gunicorn worker and a restart agree on the cooldown — a process-local
# dict let each worker re-hit Garmin and keep the limit alive forever.
("rate_limited_until", "DATETIME"),
],
"health_data": [
# activity / energy
@@ -492,9 +496,18 @@ def _migrate(cur):
for name, coltype in columns:
if name in present:
continue
# SQLite has no "ADD COLUMN IF NOT EXISTS"; the membership check
# above is what keeps this idempotent on both backends.
cur.execute(f"ALTER TABLE {table} ADD COLUMN {name} {coltype}")
# The membership check above keeps this idempotent for the normal
# case. But gunicorn runs 2 workers that both call init_db() on
# boot; under that race the loser can decide to add a column the
# winner already added (ADD COLUMN commits implicitly and becomes
# visible a hair after the loser's probe). Swallow the
# duplicate-column error so a concurrent boot can't take the whole
# service down. Same guard covers SQLite's "duplicate column name".
try:
cur.execute(f"ALTER TABLE {table} ADD COLUMN {name} {coltype}")
except Exception as e: # noqa: BLE001 - only "already exists" is safe to ignore
if "duplicate column" not in str(e).lower():
raise
# --- Public API -------------------------------------------------------------