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:
@@ -100,7 +100,11 @@ class RateLimited(RuntimeError):
|
|||||||
# 429 the whole process stands down until this passes. Bumped from 30 to 60
|
# 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,
|
# 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.
|
# 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
|
# 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().
|
# still the lever the tests reach for via _rate_limited_until.clear().
|
||||||
_rate_limited_until = {}
|
_rate_limited_until = {}
|
||||||
@@ -144,11 +148,16 @@ def rate_limited_until(user_id):
|
|||||||
|
|
||||||
|
|
||||||
def _note_rate_limit(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()
|
now = datetime.datetime.utcnow()
|
||||||
existing = rate_limited_until(user_id)
|
until = now + RATE_LIMIT_BACKOFF
|
||||||
until = (existing + datetime.timedelta(minutes=30)) if existing and existing > now \
|
|
||||||
else (now + RATE_LIMIT_BACKOFF)
|
|
||||||
_rate_limited_until[user_id] = until
|
_rate_limited_until[user_id] = until
|
||||||
try:
|
try:
|
||||||
# Persist alongside the current status so the cooldown survives across
|
# Persist alongside the current status so the cooldown survives across
|
||||||
|
|||||||
@@ -93,6 +93,17 @@ def _wait_for_code(session_id):
|
|||||||
|
|
||||||
|
|
||||||
def _run_login(session_id, user_id, garmin_email, password, is_cn, import_garmin):
|
def _run_login(session_id, user_id, garmin_email, password, is_cn, import_garmin):
|
||||||
|
# Don't even attempt if we're in a rate-limit cooldown: slamming Garmin's
|
||||||
|
# login endpoint just extends the throttle. Fail fast with a clear message so
|
||||||
|
# the user isn't left watching a spinner that will only 429 anyway.
|
||||||
|
if garmin_svc.rate_limited_until(user_id):
|
||||||
|
_set(
|
||||||
|
session_id,
|
||||||
|
"failed",
|
||||||
|
error="Garmin 仍在限制请求频率,请等待冷却窗口结束后再登录。"
|
||||||
|
"反复点击登录会越撞越久,令牌本身没有失效。",
|
||||||
|
)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
Garmin = import_garmin()
|
Garmin = import_garmin()
|
||||||
client = Garmin(is_cn=is_cn)
|
client = Garmin(is_cn=is_cn)
|
||||||
@@ -104,7 +115,18 @@ def _run_login(session_id, user_id, garmin_email, password, is_cn, import_garmin
|
|||||||
garmin_svc.save_token(user_id, client.garth.dumps(), garmin_email)
|
garmin_svc.save_token(user_id, client.garth.dumps(), garmin_email)
|
||||||
_set(session_id, "done", code=None)
|
_set(session_id, "done", code=None)
|
||||||
except Exception as e: # noqa: BLE001 - surfaced to the user via the row
|
except Exception as e: # noqa: BLE001 - surfaced to the user via the row
|
||||||
_set(session_id, "failed", error=f"{type(e).__name__}: {e}"[:500], code=None)
|
# A 429 during login extends the cooldown the same way a sync 429 does,
|
||||||
|
# so the scheduler and future logins back off instead of re-hammering and
|
||||||
|
# keeping Garmin's throttle alive forever.
|
||||||
|
if garmin_svc._is_rate_limited(e):
|
||||||
|
garmin_svc._note_rate_limit(user_id)
|
||||||
|
error = (
|
||||||
|
"Garmin 返回 429 限流(登录接口)。已自动退避 6 小时,"
|
||||||
|
"请等待冷却窗口结束后再试——反复尝试会越撞越久。"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error = f"{type(e).__name__}: {e}"[:500]
|
||||||
|
_set(session_id, "failed", error=error, code=None)
|
||||||
|
|
||||||
|
|
||||||
def start_login(user_id, garmin_email, password, import_garmin=None, is_cn=None):
|
def start_login(user_id, garmin_email, password, import_garmin=None, is_cn=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user