Commit 0d0e807
Replace async-disabling mechanism with retry backoff on refresh failure (#1315)
## Summary
Replace the async-disabling mechanism on token refresh failure with a
1-minute retry backoff, allowing the SDK to recover from transient
errors without waiting for a full token expiry.
## Why
When an asynchronous token refresh failed, the `Refreshable` class set a
`_refresh_err` flag that completely disabled async refresh. The only way
to clear this flag was through a blocking refresh, which only triggers
when the token fully expires. This meant the SDK could not recover from
transient refresh failures (e.g. a brief network blip) until the token
expired — potentially tens of minutes later — even though the underlying
issue may have resolved in seconds.
This PR replaces the binary disable flag with a short cooldown: after a
failed async refresh, the `_stale_after` threshold is pushed 1 minute
into the future so the token appears fresh for a brief backoff period.
Once the cooldown elapses the token becomes stale again and a new async
refresh is attempted, giving the SDK a chance to recover proactively.
## What changed
### Interface changes
None.
### Behavioral changes
- **Async refresh retry on failure** — Previously, a failed async
refresh disabled all future async attempts until a blocking refresh on
expiry. Now, the SDK waits 1 minute (`_ASYNC_REFRESH_RETRY_BACKOFF`) and
then retries the async refresh. This makes token refresh more resilient
to transient errors.
- **Late async result guard** — When a slow async refresh completes
after a blocking refresh already obtained a newer token, the stale async
result is now discarded instead of overwriting the fresher token.
### Internal changes
- **`_stale_after` replaces `_stale_duration`** — Staleness is now
tracked as an absolute timestamp (`_stale_after`) instead of a relative
`timedelta` (`_stale_duration`). This simplifies `_token_state()` to a
direct comparison rather than computing `expiry - now` and comparing
against a duration.
- **`_handle_failed_async_refresh()`** — New method that advances
`_stale_after` by the backoff period, replacing the `_refresh_err` flag.
- **`_now()` helper** — Centralises "current time" so that naive and
timezone-aware `datetime` objects from different token sources are
compared consistently.
- **`_use_dynamic_stale_duration` renamed to
`_use_legacy_stale_duration`** — Inverted boolean to clarify intent: the
legacy path is the one where callers supply an explicit
`stale_duration`.
- **`_MockRefreshable.refresh()` no longer mutates `self._token`** — The
mock now returns the token without setting `self._token` as a side
effect, avoiding a data race between async and blocking refresh threads.
The production code's `_update_token` handles storage.
## How is this tested?
Tests are rewritten to be fully deterministic by introducing a
`_ManualExecutor` that replaces the real `ThreadPoolExecutor`. Async
refreshes are queued but only execute when `executor.run_all()` is
called, eliminating all `time.sleep()` calls and thread synchronization
from async-path tests. This makes the test suite faster and removes
flakiness from timing-dependent assertions.
New test cases:
- `test_repeated_calls_during_async_failure_cooldown_do_not_refresh` —
verifies that calls during the cooldown period do not trigger additional
async refreshes.
- `test_call_after_async_failure_cooldown_refreshes_token_async` —
verifies that a call after the cooldown elapses triggers a new async
refresh that succeeds.
- `test_late_async_refresh_does_not_overwrite_blocking_refresh` —
verifies that a slow async refresh completing after a blocking refresh
does not overwrite the newer token.
- `test_stale_after_is_recomputed_after_blocking_refresh` — verifies
that `_stale_after` is recomputed from the refreshed token after a
blocking refresh.
- `test_stale_after_computation` — verifies that `_stale_after` is
computed correctly for both the dynamic and legacy stale-duration paths.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>1 parent 61089f5 commit 0d0e807
3 files changed
Lines changed: 305 additions & 190 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
251 | | - | |
252 | | - | |
253 | | - | |
254 | 251 | | |
255 | 252 | | |
256 | 253 | | |
| 254 | + | |
| 255 | + | |
257 | 256 | | |
258 | 257 | | |
259 | 258 | | |
| |||
272 | 271 | | |
273 | 272 | | |
274 | 273 | | |
275 | | - | |
| 274 | + | |
| 275 | + | |
276 | 276 | | |
277 | 277 | | |
278 | 278 | | |
279 | 279 | | |
280 | 280 | | |
| 281 | + | |
| 282 | + | |
281 | 283 | | |
282 | 284 | | |
283 | | - | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
284 | 293 | | |
285 | 294 | | |
286 | 295 | | |
| |||
290 | 299 | | |
291 | 300 | | |
292 | 301 | | |
293 | | - | |
| 302 | + | |
294 | 303 | | |
295 | 304 | | |
| 305 | + | |
| 306 | + | |
296 | 307 | | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
302 | 311 | | |
303 | | - | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
304 | 324 | | |
305 | 325 | | |
306 | 326 | | |
| |||
334 | 354 | | |
335 | 355 | | |
336 | 356 | | |
337 | | - | |
338 | | - | |
| 357 | + | |
| 358 | + | |
339 | 359 | | |
340 | | - | |
| 360 | + | |
341 | 361 | | |
342 | 362 | | |
343 | 363 | | |
344 | 364 | | |
345 | 365 | | |
346 | 366 | | |
347 | | - | |
348 | | - | |
349 | | - | |
350 | 367 | | |
351 | 368 | | |
352 | 369 | | |
| |||
360 | 377 | | |
361 | 378 | | |
362 | 379 | | |
| 380 | + | |
363 | 381 | | |
364 | 382 | | |
365 | 383 | | |
366 | 384 | | |
367 | 385 | | |
368 | 386 | | |
369 | 387 | | |
370 | | - | |
371 | | - | |
| 388 | + | |
| 389 | + | |
372 | 390 | | |
373 | 391 | | |
374 | 392 | | |
375 | | - | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
376 | 396 | | |
377 | 397 | | |
378 | | - | |
| 398 | + | |
379 | 399 | | |
380 | 400 | | |
381 | 401 | | |
382 | 402 | | |
383 | 403 | | |
384 | | - | |
| 404 | + | |
385 | 405 | | |
386 | 406 | | |
387 | 407 | | |
| |||
0 commit comments