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:
@@ -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 -------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user