[BUG] AccessTokenCache/SimpleTokenCache: concurrent callers wait forever when the caller refreshing the token is cancelled
I maintainer di solito rispondono entro 1 giorno
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
Describe the bug
When several callers ask AccessTokenCache (or SimpleTokenCache) for a token at the same time and there is no valid cached token, one caller performs the refresh and the others wait on it. If the refreshing caller is cancelled before the refresh completes, the waiting callers never complete. They stay pending until their own subscribers give up, even after later callers have successfully fetched a new token.
BearerTokenAuthenticationPolicy uses AccessTokenCache on the async path, so an async client can hang its in-flight requests when the request that happened to start the token refresh is cancelled. Typical causes are a timeout(...), a block(Duration) that expires, or a disposed subscription such as a client disconnect in a WebFlux app, while a slow token request (for example the first managed identity token) is in progress.
Cause
In AccessTokenCache.retrieveToken the refresh runs inside
Mono.using(() -> wip,
ignored -> tokenRefresh.materialize()
.flatMap(processTokenRefreshResult(sinksOne, now, fallback))
.doOnError(sinksOne::tryEmitError),
w -> w.set(null));
Waiting callers subscribe to sinksOne.asMono(). On cancellation the cleanup only clears wip; sinksOne is never completed, so the waiters never receive a signal. SimpleTokenCache.getToken has the same pattern. The existing test SimpleTokenCacheTests.wipResetsOnCancel checks that wip is cleared, but not that waiting callers complete.
To Reproduce
Through an async pipeline with BearerTokenAuthenticationPolicy: two concurrent requests, a token request that takes 2 seconds, and a 500 ms timeout on the first request only.
AtomicInteger tokenCalls = new AtomicInteger();
TokenCredential credential = request -> Mono.delay(Duration.ofSeconds(2))
.map(ignored -> new AccessToken("token-" + tokenCalls.incrementAndGet(), OffsetDateTime.now().plusHours(1)));
HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(httpClient) // any client returning 200
.policies(new BearerTokenAuthenticationPolicy(credential, "https://example.azure.net/.default"))
.build();
CompletableFuture<Integer> a = pipeline.send(new HttpRequest(HttpMethod.GET, "https://example.azure.net/a"))
.map(HttpResponse::getStatusCode).timeout(Duration.ofMillis(500)).toFuture();
CompletableFuture<Integer> b = pipeline.send(new HttpRequest(HttpMethod.GET, "https://example.azure.net/b"))
.map(HttpResponse::getStatusCode).toFuture();
With azure-core 1.60.0, request A fails with its own TimeoutException and request B never completes. It is still pending after a third request has succeeded:
request A: TimeoutException (its own timeout)
request B: still waiting after 10 seconds
request C status 200, request B done: false, token calls: 1
The same thing using AccessTokenCache directly:
AtomicInteger calls = new AtomicInteger();
TokenCredential credential = request -> Mono.delay(Duration.ofSeconds(1))
.map(ignored -> new AccessToken("token-" + calls.incrementAndGet(), OffsetDateTime.now().plusHours(1)));
AccessTokenCache cache = new AccessTokenCache(credential);
TokenRequestContext context = new TokenRequestContext().addScopes("https://example.azure.net/.default");
Disposable first = cache.getToken(context, false).subscribe(); // starts the refresh
CompletableFuture<AccessToken> second = cache.getToken(context, false).toFuture(); // waits on that refresh
first.dispose(); // refreshing caller cancelled
try {
System.out.println("second caller got " + second.get(5, TimeUnit.SECONDS).getToken());
} catch (TimeoutException e) {
System.out.println("second caller: no token after 5 seconds");
}
System.out.println("new caller got " + cache.getToken(context, false).block(Duration.ofSeconds(5)).getToken());
System.out.println("second caller done: " + second.isDone());
Output with azure-core 1.59.1 and 1.60.0:
second caller: no token after 5 seconds
new caller got token-1
second caller done: false
With random cancellations it is not a corner case. In a local stress run (40 concurrent callers per round, about a third of them cancelled at random times, 40 rounds), about 8% of the callers that were not cancelled never completed on 1.60.0: 87 of 1,053 with AccessTokenCache and 83 of 1,054 with SimpleTokenCache.
Expected behavior
When the refresh a caller is waiting on ends without a token because the refreshing caller was cancelled, the waiting caller should retry the acquisition instead of waiting forever.
Setup
- Library:
com.azure:azure-core1.59.1 and 1.60.0 (reproduced with both), same code on currentmain - Java 25
- Lingua principale
- Java
- Stelle
- 2.6k
- Fork
- 2.2k
- Merge medio
- 1g 22h
- PR unite (30g)
- 213
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di Azure/azure-sdk-for-java
-
mgmt, in track 1 migration guide, call out that file-based authentication method is not recommendedForse di nuovo libera Una pull request per questa issue è stata chiusa senza essere unita. ApertaAzure.Identity
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 74/100
Azure/azure-sdk-for-java#48894 ·
I maintainer di solito rispondono entro 1 giorno
-
[BUG] - Handler.close livelocks on FAIL_NON_SERIALIZED while holding the ReactorConnection monitorForse già presa @conniey l’ha presa 1 giorno fa. ApertaAzure.Core.AMQP Client customer-reported needs-team-attention question
Azure/azure-sdk-for-java#50879 · 2 commenti · 1 reazione · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
[Design] Support External Evaluation Policy (Invoke) in management SDKsForse già presa @weidongxu-microsoft l’ha presa 6 giorni fa. Aperta
Azure/azure-sdk-for-java#50818 · 2 reazioni · 2 assegnatari ·
I maintainer di solito rispondono entro 1 giorno
-
[BUG] azure-core-http-vertx: Streaming request bodies hang on Vert.x 5.x (NoSuchMethodError on HttpClientRequest.reset)Forse già presa @alzimmermsft l’ha presa 7 giorni fa. ApertaAzure.Core Client customer-reported needs-team-attention question
Azure/azure-sdk-for-java#50793 · 6 commenti · 1 reazione · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
[FEATURE REQ] ChangeFeedProcessor event handler fully reactive implementationForse già presa @FabianMeiswinkel l’ha presa 7 giorni fa. ApertaClient Cosmos customer-reported feature-request needs-team-attention Service Attention
Difficoltà 5/5 Più di una settimana Idoneità per principianti 12/100
Azure/azure-sdk-for-java#50758 · 3 commenti · 1 reazione · 2 assegnatari ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di Azure/azure-sdk-for-java
Issue simili
-
approved
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
checkstyle/checkstyle#22129 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 80/100
objectionary/lints#1577 ·
I maintainer di solito rispondono entro 2 giorni
-
ChaosEngine: backup-remote doctor row passes while consumer mode hides bot memory from gitForse già presa @MohabMohie l’ha presa oggi. Apertaagentic
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
ShaftHQ/SHAFT_ENGINE#7263 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100