Skip to content

Commit fe5fb32

Browse files
committed
fix(auth): resolve flakiness in ClientSideCredentialAccessBoundaryFactoryTest
Synchronize the decision to refresh and task creation to avoid race conditions. Resolves: #12871
1 parent 058efbb commit fe5fb32

2 files changed

Lines changed: 37 additions & 30 deletions

File tree

google-auth-library-java/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,26 @@ public AccessToken generateToken(CredentialAccessBoundary accessBoundary)
227227
*/
228228
@VisibleForTesting
229229
void refreshCredentialsIfRequired() throws IOException {
230-
RefreshType refreshType = determineRefreshType();
230+
RefreshType refreshType;
231+
RefreshTask currentRefreshTask;
232+
233+
// Synchronize both the decision to refresh and the task creation to prevent a race
234+
// condition. Without this, multiple threads might concurrently determine a refresh is
235+
// needed (e.g., in ASYNC mode) and return ASYNC. The first thread would start the refresh
236+
// task. If that task completes quickly and clears the `refreshTask` reference, a subsequent
237+
// thread that also determined a refresh was needed would then see `refreshTask` as null
238+
// and incorrectly start a second, redundant refresh task.
239+
synchronized (refreshLock) {
240+
refreshType = determineRefreshType();
231241

232-
if (refreshType == RefreshType.NONE) {
233-
// No refresh needed, token is still valid.
234-
return;
235-
}
242+
if (refreshType == RefreshType.NONE) {
243+
// No refresh needed, token is still valid.
244+
return;
245+
}
236246

237-
// If a refresh is required, create or retrieve the refresh task.
238-
RefreshTask currentRefreshTask = getOrCreateRefreshTask();
247+
// If a refresh is required, create or retrieve the refresh task.
248+
currentRefreshTask = getOrCreateRefreshTask();
249+
}
239250

240251
// Handle the refresh based on the determined refresh type.
241252
switch (refreshType) {
@@ -283,16 +294,14 @@ void refreshCredentialsIfRequired() throws IOException {
283294
}
284295
}
285296

297+
// Assumes the caller holds refreshLock.
286298
private RefreshType determineRefreshType() {
287-
AccessToken intermediateAccessToken;
288-
synchronized (refreshLock) {
289-
if (intermediateCredentials == null
290-
|| intermediateCredentials.intermediateAccessToken == null) {
291-
// A blocking refresh is needed if the intermediate access token doesn't exist.
292-
return RefreshType.BLOCKING;
293-
}
294-
intermediateAccessToken = intermediateCredentials.intermediateAccessToken;
299+
if (intermediateCredentials == null
300+
|| intermediateCredentials.intermediateAccessToken == null) {
301+
// A blocking refresh is needed if the intermediate access token doesn't exist.
302+
return RefreshType.BLOCKING;
295303
}
304+
AccessToken intermediateAccessToken = intermediateCredentials.intermediateAccessToken;
296305

297306
Date expirationTime = intermediateAccessToken.getExpirationTime();
298307
if (expirationTime == null) {
@@ -322,23 +331,22 @@ private RefreshType determineRefreshType() {
322331
* responsibility of the caller to execute it. The task will clear the single flight slot upon
323332
* completion.
324333
*/
334+
// Assumes the caller holds refreshLock.
325335
private RefreshTask getOrCreateRefreshTask() {
326-
synchronized (refreshLock) {
327-
if (refreshTask != null) {
328-
// An existing refresh task is already in progress. Return a NEW RefreshTask instance with
329-
// the existing task, but set isNew to false. This indicates to the caller that a new
330-
// refresh task was NOT created.
331-
return new RefreshTask(refreshTask.task, false);
332-
}
336+
if (refreshTask != null) {
337+
// An existing refresh task is already in progress. Return a NEW RefreshTask instance with
338+
// the existing task, but set isNew to false. This indicates to the caller that a new
339+
// refresh task was NOT created.
340+
return new RefreshTask(refreshTask.task, false);
341+
}
333342

334-
final ListenableFutureTask<IntermediateCredentials> task =
335-
ListenableFutureTask.create(this::fetchIntermediateCredentials);
343+
final ListenableFutureTask<IntermediateCredentials> task =
344+
ListenableFutureTask.create(this::fetchIntermediateCredentials);
336345

337-
// Store the new refresh task in the refreshTask field before returning. This ensures that
338-
// subsequent calls to this method will return the existing task while it's still in progress.
339-
refreshTask = new RefreshTask(task, true);
340-
return refreshTask;
341-
}
346+
// Store the new refresh task in the refreshTask field before returning. This ensures that
347+
// subsequent calls to this method will return the existing task while it's still in progress.
348+
refreshTask = new RefreshTask(task, true);
349+
return refreshTask;
342350
}
343351

344352
/**

google-auth-library-java/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ void refreshCredentialsIfRequired_blockingMultiThread() throws IOException, Inte
325325
}
326326

327327
@Test
328-
@Disabled("Flaky test: https://github.com/googleapis/google-cloud-java/issues/12871")
329328
void refreshCredentialsIfRequired_asyncMultiThread() throws IOException, InterruptedException {
330329
final ClientSideCredentialAccessBoundaryFactory factory =
331330
getClientSideCredentialAccessBoundaryFactory(RefreshType.ASYNC);

0 commit comments

Comments
 (0)