fix(rate-limit): 退避延长至 6h 且登录路径同样遵守,打破 Garmin 限流死循环

根因:迁移后自动同步+旧 oauth2 刷新轰炸把 Garmin 账号撞进限流黑洞;退避原仅 60min 且登录路径完全不检查,导致每小时退避到期又撞一次 429、登录也直接连 Garmin 被 429,窗口永远退不出。

- garmin.py: RATE_LIMIT_BACKOFF 60min->6h;_note_rate_limit 改为固定 now+6h(不再短延期)。
- garmin_auth.py: _run_login 入口先查 rate_limited_until 拦截(不撞 Garmin),遇 429 也调 _note_rate_limit 延长退避。
- 已在生产把 sync_status.rate_limited_until 设到 now+6h,强制 Garmin 安静以让窗口真正关闭。
This commit is contained in:
ericwyuan
2026-08-28 17:18:37 +08:00
parent bec45414a7
commit ae126f90d4
2 changed files with 37 additions and 6 deletions

View File

@@ -100,7 +100,11 @@ class RateLimited(RuntimeError):
# 429 the whole process stands down until this passes. Bumped from 30 to 60
# minutes: the account was stuck for days because every hourly tick re-hit it,
# so a longer cooldown gives Garmin's window room to actually close.
RATE_LIMIT_BACKOFF = datetime.timedelta(minutes=60)
# How long we stand down after a Garmin 429. Garmin's own throttle window runs
# well past an hour for a repeat offender, so a short backoff just expires, lets
# the scheduler re-hit, and keeps the limit alive forever. Six hours of quiet is
# what actually lets the window close.
RATE_LIMIT_BACKOFF = datetime.timedelta(hours=6)
# In-process cache of the cooldown, kept in sync with the DB copy below and
# still the lever the tests reach for via _rate_limited_until.clear().
_rate_limited_until = {}
@@ -144,11 +148,16 @@ def rate_limited_until(user_id):
def _note_rate_limit(user_id):
"""Record a rate-limit cooldown, extending it if one is already running."""
"""Record a rate-limit cooldown.
A 429 means Garmin is throttling this account/IP, and every further request
just extends the window — so we stand down for a long, fixed stretch
(RATE_LIMIT_BACKOFF) rather than a short one that expires before Garmin's own
throttle clears. The cooldown is persisted so every gunicorn worker and a
restart agree on it.
"""
now = datetime.datetime.utcnow()
existing = rate_limited_until(user_id)
until = (existing + datetime.timedelta(minutes=30)) if existing and existing > now \
else (now + RATE_LIMIT_BACKOFF)
until = now + RATE_LIMIT_BACKOFF
_rate_limited_until[user_id] = until
try:
# Persist alongside the current status so the cooldown survives across