From f5eaac285760f922f80ac6afa96498c9ca789489 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 11 Nov 2025 18:51:33 +0600 Subject: [PATCH 1/4] feat: implement per-account token refresh synchronization Introduces a more granular, per-account locking mechanism for refreshing OAuth tokens in `OidcTokenRefresher`. This change replaces the global `synchronized` block with account-specific locks, allowing different accounts to refresh their tokens in parallel. This mitigates race conditions for a single account while improving performance by not blocking token refreshes for unrelated accounts. Key changes: - Use `ConcurrentHashMap` to manage locks and track ongoing refresh operations for each account. - If a token refresh is already in progress for an account, subsequent requests for the same account will wait for the ongoing operation to complete. - In `AccountRemovedReceiver`, a cleanup routine is added to remove the corresponding lock and any tracked operations when an account is deleted, preventing memory leaks. --- .../android/sso/OidcTokenRefresher.kt | 169 ++++++++++++++---- .../receiver/AccountRemovedReceiver.kt | 6 + 2 files changed, 143 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt index 803733f5f..e4613cf6c 100644 --- a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt +++ b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt @@ -23,13 +23,12 @@ import android.content.Context import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.network.HttpClient.HttpClientEntryPoint import dagger.hilt.android.EntryPointAccessors -import kotlinx.coroutines.runBlocking import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException import net.openid.appauth.ClientAuthentication -import org.jetbrains.annotations.Blocking import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException +import java.util.concurrent.ConcurrentHashMap import java.util.logging.Level import java.util.logging.Logger @@ -39,6 +38,17 @@ import java.util.logging.Logger object OidcTokenRefresher { private val logger: Logger = Logger.getGlobal() + // ConcurrentHashMap to store account-specific lock objects + // This allows different accounts to refresh tokens in parallel while + // ensuring only one refresh operation happens per account at a time + private val accountLocks = ConcurrentHashMap() + + // ConcurrentHashMap to track ongoing refresh operations per account + // Key: account identifier (name|type), Value: CompletableFuture for the ongoing operation + // This prevents duplicate refresh requests for the same account + private val ongoingRefreshOperations = + ConcurrentHashMap>() + /** * Refreshes the current AuthState and updates it. Uses the current one if it's still valid, * or requests a new one if necessary. @@ -47,19 +57,16 @@ object OidcTokenRefresher { * 1. Invoke the AppAuth library's authorization service to refresh tokens. * 2. Update AccountManager on successful refresh or log failures. * - * **Threading:** This method uses [runBlocking] and therefore must **not** be - * called from the Main/UI thread. It is annotated with `@Blocking` to signal - * blocking behavior. - * - * This method is synchronized / thread-safe so that it can be called for - * multiple HTTP requests at the same time. + * This method implements account-specific synchronization to allow different accounts + * to refresh tokens in parallel, while preventing multiple refresh operations for the + * same account. If a refresh is already ongoing for an account, other threads requesting + * refresh for the same account will wait for the ongoing operation to complete. * * Returns an updated AuthState if token refresh is successful; * Throws [AuthorizationException.TokenRequestErrors.INVALID_GRANT] for invalid grant or null otherwise. */ @JvmStatic - @Blocking @Throws(AuthorizationException::class) fun refreshAuthState( context: Context, @@ -67,60 +74,158 @@ object OidcTokenRefresher { getClientAuth: () -> ClientAuthentication?, readAuthState: () -> AuthState?, writeAuthState: (AuthState) -> Unit - ): AuthState? = synchronized(javaClass) { - val authState = readAuthState() ?: return null - // Use cached authState if possible - if (authState.isAuthorized && authState.accessToken != null && !authState.needsTokenRefresh) { - if (BuildConfig.DEBUG) { - logger.finest("$account is using cached AuthState: ${authState.jsonSerializeString()}") + ): AuthState? { + if (account == null) { + return synchronized(javaClass) { + performTokenRefresh(context, null, getClientAuth, readAuthState, writeAuthState) } - return authState } - // Check for AuthorizationException - val authorizationException = authState.authorizationException - if (authorizationException != null && isInvalidGrant(authorizationException)) { - throw AuthorizationException.TokenRequestErrors.INVALID_GRANT - } + val accountKey = generateAccountKey(account.name, account.type) - logger.info("$account is requesting fresh access token") - if (BuildConfig.DEBUG) { - logger.finest("AuthState before update = ${authState.jsonSerializeString()}") + // Get the account-specific lock first to coordinate all access for this account + val accountLock = accountLocks.computeIfAbsent(accountKey) { Any() } + + return synchronized(accountLock) { + if (BuildConfig.DEBUG) { + logger.finest("[Thread-${Thread.currentThread().id}] Entered synchronized block for $account") + } + + // Check if there's already an ongoing refresh operation for this account after acquiring the lock + // This prevents multiple simultaneous refresh requests for the same account + val ongoingOperation = ongoingRefreshOperations[accountKey] + if (ongoingOperation != null) { + logger.info( + "[Thread-${Thread.currentThread().id}] is waiting for " + + "ongoing refresh operation for $account" + ) + // Wait for the ongoing operation to complete and return its result + // This ensures we don't have multiple threads attempting to refresh the same account + return@synchronized try { + ongoingOperation.join() + } catch (e: CompletionException) { + logger.log( + Level.SEVERE, + "[Thread-${Thread.currentThread().id}] Ongoing refresh operation failed for account: $account", + e + ) + null + } + } + + val authState = readAuthState() ?: return@synchronized null + + val authorizationException = authState.authorizationException + if (authorizationException != null && isInvalidGrant(authorizationException)) { + logger.info("Invalid grant detected for $account, requiring re-authentication") + throw AuthorizationException.TokenRequestErrors.INVALID_GRANT + } + + // Use cached authState if possible (no actual refresh needed) + if (authState.isAuthorized && authState.accessToken != null && !authState.needsTokenRefresh) { + if (BuildConfig.DEBUG) { + logger.finest("[Thread-${Thread.currentThread().id}] $account is using cached AuthState") + logger.finest("[Thread-${Thread.currentThread().id}] Exiting synchronized block for $account (cached)") + } + return@synchronized authState + } + + // We determined a refresh is actually needed, so create and register the operation + val newOperation = CompletableFuture() + ongoingRefreshOperations[accountKey] = newOperation + + if (BuildConfig.DEBUG) { + logger.finest("[Thread-${Thread.currentThread().id}] Initiating actual token refresh for $account") + } + + return@synchronized try { + val result = performTokenRefresh( + context, + account, + getClientAuth, + { authState }, // Pass the already-read auth state + writeAuthState + ) + if (BuildConfig.DEBUG) { + logger.finest("[Thread-${Thread.currentThread().id}] Completed token refresh for $account") + } + newOperation.complete(result) + result + } catch (e: Throwable) { + newOperation.completeExceptionally(e) + throw e + } finally { + ongoingRefreshOperations.remove(accountKey) + } } + } + + private fun generateAccountKey(accountName: String, accountType: String) = + "$accountName|$accountType" + + private fun performTokenRefresh( + context: Context, + account: Account?, + getClientAuth: () -> ClientAuthentication?, + readAuthState: () -> AuthState?, + writeAuthState: (AuthState) -> Unit + ): AuthState? { + val authState = readAuthState() ?: return null val clientAuth = getClientAuth() ?: return null + + logger.info("[Thread-${Thread.currentThread().id}] $account is requesting fresh access token") + val authService = EntryPointAccessors.fromApplication(context, HttpClientEntryPoint::class.java) .authorizationService() val authStateFuture = CompletableFuture() - return@synchronized try { + try { authState.performActionWithFreshTokens( - authService, clientAuth + authService, + clientAuth ) { accessToken, _, exception -> writeAuthState(authState) when { accessToken != null -> { - logger.info("Token refreshed for $account") + logger.info("[Thread-${Thread.currentThread().id}] Token refreshed for $account") if (BuildConfig.DEBUG) { - logger.finest("Updated authState = ${authState.jsonSerializeString()}") - } + logger.fine("[Thread-${Thread.currentThread().id}] AuthState stored for $account after successful refresh.") + } authStateFuture.complete(authState) } exception != null -> { + logger.warning("[Thread-${Thread.currentThread().id}] Token refresh failed for $account: $exception") authStateFuture.completeExceptionally(exception) } } } - authStateFuture.join() + logger.info("[Thread-${Thread.currentThread().id}] Token refresh process completed for $account") + val result: AuthState? = authStateFuture.join() + + return result } catch (e: CompletionException) { - logger.log(Level.SEVERE, "Couldn't obtain access token", e) - null + logger.log( + Level.SEVERE, + "[Thread-${Thread.currentThread().id}] Couldn't obtain access token for $account", + e + ) + return null } finally { authService.dispose() } } + /** + * Cleanup method to remove locks when accounts are removed. + */ + fun removeAccountLock(accountName: String, accountType: String) { + val accountKey = generateAccountKey(accountName, accountType) + accountLocks.remove(accountKey) + ongoingRefreshOperations.remove(accountKey) + } + // Checks whether the given AuthorizationException indicates an invalid grant (requires re-login). private fun isInvalidGrant(ex: AuthorizationException?): Boolean { val invalidGrant = AuthorizationException.TokenRequestErrors.INVALID_GRANT diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt index b1d6726a2..7785fd567 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt @@ -25,6 +25,7 @@ import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.ui.signout.OpenIdEndSessionActivity import at.bitfire.davdroid.util.AuthStatePrefUtils +import com.nextcloud.android.sso.OidcTokenRefresher import com.owncloud.android.lib.common.OwnCloudClientManagerFactory class AccountRemovedReceiver : BroadcastReceiver() { @@ -39,6 +40,11 @@ class AccountRemovedReceiver : BroadcastReceiver() { val ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton() ownCloudClientManager.removeClientForByName(accountName) + // Clean up token refresh locks and ongoing operations + intent.extras?.getString(AccountManager.KEY_ACCOUNT_TYPE)?.let { accountType -> + OidcTokenRefresher.removeAccountLock(accountName, accountType) + } + clearOidcSession( intent = intent, context = context, -- GitLab From 13dbf0ce2525ec7c807de11b847dc802bfd4f443 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 12 Nov 2025 15:26:34 +0600 Subject: [PATCH 2/4] refactor: improve token refresh concurrency with finer-grained locking The token refresh logic in `OidcTokenRefresher` is improved to minimize the time spent in `synchronized` blocks, thereby increasing concurrency for token refresh operations across different accounts. Key changes: - The `synchronized` block is now only used to check for and register an ongoing refresh operation. The actual network I/O for `performTokenRefresh` is moved outside of the lock. - This prevents a long-running refresh for one account from blocking other threads that need to check the status or start a refresh for a different account. - Instead of re-reading the `AuthState` within `performTokenRefresh`, the state read inside the initial `synchronized` block is captured and passed along. This avoids potential race conditions where the state could change between the check and the refresh action. - The `AuthState` is now persisted by `writeAuthState` only after a successful token refresh, preventing the storage of a failed state. - Cleanup of the `ongoingRefreshOperations` map is made safer by removing the entry only if it matches the completed operation's future. --- .../android/sso/OidcTokenRefresher.kt | 178 +++++++++--------- 1 file changed, 91 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt index e4613cf6c..34821e03f 100644 --- a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt +++ b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt @@ -37,37 +37,36 @@ import java.util.logging.Logger */ object OidcTokenRefresher { private val logger: Logger = Logger.getGlobal() - - // ConcurrentHashMap to store account-specific lock objects - // This allows different accounts to refresh tokens in parallel while - // ensuring only one refresh operation happens per account at a time private val accountLocks = ConcurrentHashMap() - - // ConcurrentHashMap to track ongoing refresh operations per account - // Key: account identifier (name|type), Value: CompletableFuture for the ongoing operation - // This prevents duplicate refresh requests for the same account private val ongoingRefreshOperations = ConcurrentHashMap>() + @JvmStatic + @Throws(AuthorizationException::class) /** - * Refreshes the current AuthState and updates it. Uses the current one if it's still valid, - * or requests a new one if necessary. + * Refreshes the AuthState for a given account if necessary. * - * It will: - * 1. Invoke the AppAuth library's authorization service to refresh tokens. - * 2. Update AccountManager on successful refresh or log failures. + * This method handles concurrent requests for the same account by ensuring that only one + * token refresh operation is executed at a time. If a refresh is already in progress for an + * account, subsequent requests for the same account will wait for the ongoing operation to + * complete and receive its result. This prevents redundant network requests and potential + * race conditions. Refresh operations for different accounts can run in parallel. * - * This method implements account-specific synchronization to allow different accounts - * to refresh tokens in parallel, while preventing multiple refresh operations for the - * same account. If a refresh is already ongoing for an account, other threads requesting - * refresh for the same account will wait for the ongoing operation to complete. + * If the current tokens are still valid and do not need a refresh, the existing AuthState is + * returned immediately without a network request. * - * Returns an updated AuthState if token refresh is successful; - * Throws [AuthorizationException.TokenRequestErrors.INVALID_GRANT] for invalid grant or null otherwise. + * @param context The application context. + * @param account The account for which to refresh the token. If null, perform refresh for a + * newly set up account. + * @param getClientAuth A function that provides the [ClientAuthentication]. + * @param readAuthState A function that reads the current [AuthState]. + * @param writeAuthState A function that persists the updated [AuthState] to storage after a + * successful refresh. + * @return The updated [AuthState] if the refresh was successful or not needed, or `null` if an + * error occurred. + * @throws AuthorizationException.TokenRequestErrors.INVALID_GRANT if the refresh token is + * invalid and re-authentication is required. */ - - @JvmStatic - @Throws(AuthorizationException::class) fun refreshAuthState( context: Context, account: Account?, @@ -82,80 +81,91 @@ object OidcTokenRefresher { } val accountKey = generateAccountKey(account.name, account.type) - - // Get the account-specific lock first to coordinate all access for this account val accountLock = accountLocks.computeIfAbsent(accountKey) { Any() } - return synchronized(accountLock) { + // Variables to communicate work outside the synchronized block + var existingOp: CompletableFuture? = null + var starterOp: CompletableFuture? = null + var capturedAuthState: AuthState? = null + + // Short synchronized section: decide what to do + synchronized(accountLock) { if (BuildConfig.DEBUG) { - logger.finest("[Thread-${Thread.currentThread().id}] Entered synchronized block for $account") + logger.finest("[Thread-${Thread.currentThread().id}] Entered sync for $account") } - // Check if there's already an ongoing refresh operation for this account after acquiring the lock - // This prevents multiple simultaneous refresh requests for the same account - val ongoingOperation = ongoingRefreshOperations[accountKey] - if (ongoingOperation != null) { - logger.info( - "[Thread-${Thread.currentThread().id}] is waiting for " + - "ongoing refresh operation for $account" - ) - // Wait for the ongoing operation to complete and return its result - // This ensures we don't have multiple threads attempting to refresh the same account - return@synchronized try { - ongoingOperation.join() - } catch (e: CompletionException) { - logger.log( - Level.SEVERE, - "[Thread-${Thread.currentThread().id}] Ongoing refresh operation failed for account: $account", - e - ) - null + // If someone already started a refresh, capture that future and wait outside. + existingOp = ongoingRefreshOperations[accountKey] + if (existingOp != null) { + if (BuildConfig.DEBUG) { + logger.info("[Thread-${Thread.currentThread().id}] Observed ongoing refresh for $account") } + return@synchronized } - val authState = readAuthState() ?: return@synchronized null + // No ongoing operation. Read auth state and decide if refresh is needed. + val authState = readAuthState() ?: return@synchronized + capturedAuthState = authState val authorizationException = authState.authorizationException if (authorizationException != null && isInvalidGrant(authorizationException)) { - logger.info("Invalid grant detected for $account, requiring re-authentication") throw AuthorizationException.TokenRequestErrors.INVALID_GRANT } - // Use cached authState if possible (no actual refresh needed) if (authState.isAuthorized && authState.accessToken != null && !authState.needsTokenRefresh) { if (BuildConfig.DEBUG) { - logger.finest("[Thread-${Thread.currentThread().id}] $account is using cached AuthState") - logger.finest("[Thread-${Thread.currentThread().id}] Exiting synchronized block for $account (cached)") + logger.finest("[Thread-${Thread.currentThread().id}] Using cached AuthState for $account") } - return@synchronized authState + // no refresh needed. return current authState. + starterOp = null + return@synchronized } - // We determined a refresh is actually needed, so create and register the operation - val newOperation = CompletableFuture() - ongoingRefreshOperations[accountKey] = newOperation - + // Need a refresh. Create and register a future, then perform refresh outside the lock. + val newOp = CompletableFuture() + ongoingRefreshOperations[accountKey] = newOp + starterOp = newOp if (BuildConfig.DEBUG) { - logger.finest("[Thread-${Thread.currentThread().id}] Initiating actual token refresh for $account") + logger.finest("[Thread-${Thread.currentThread().id}] Registered new refresh operation for $account") } + } - return@synchronized try { - val result = performTokenRefresh( - context, - account, - getClientAuth, - { authState }, // Pass the already-read auth state - writeAuthState + // If there was an existing operation, wait for it (outside synchronized). + existingOp?.let { + return try { + it.join() + } catch (e: CompletionException) { + logger.log( + Level.SEVERE, + "[Thread-${Thread.currentThread().id}] Ongoing refresh failed for $account", + e ) - if (BuildConfig.DEBUG) { - logger.finest("[Thread-${Thread.currentThread().id}] Completed token refresh for $account") - } - newOperation.complete(result) - result - } catch (e: Throwable) { - newOperation.completeExceptionally(e) - throw e - } finally { - ongoingRefreshOperations.remove(accountKey) + null + } + } + + // If starterOp is null here it means we returned cached state inside synchronized. + val op = starterOp ?: return capturedAuthState + + // Perform the actual refresh outside lock. Complete the future and clean up. + try { + val result = performTokenRefresh( + context, + account, + getClientAuth, + { capturedAuthState }, // pass the captured one to avoid re-reading under race + writeAuthState + ) + op.complete(result) + return result + } catch (e: Throwable) { + op.completeExceptionally(e) + throw e + } finally { + // remove only if the same future is still registered + ongoingRefreshOperations.remove(accountKey, op) + if (BuildConfig.DEBUG) { + logger.finest("[Thread-${Thread.currentThread().id}] Cleanup done for $account") } } } @@ -178,33 +188,31 @@ object OidcTokenRefresher { val authService = EntryPointAccessors.fromApplication(context, HttpClientEntryPoint::class.java) .authorizationService() - val authStateFuture = CompletableFuture() + val authStateFuture = CompletableFuture() try { authState.performActionWithFreshTokens( authService, clientAuth ) { accessToken, _, exception -> - writeAuthState(authState) when { accessToken != null -> { + // Persist only on success. + writeAuthState(authState) logger.info("[Thread-${Thread.currentThread().id}] Token refreshed for $account") - if (BuildConfig.DEBUG) { - logger.fine("[Thread-${Thread.currentThread().id}] AuthState stored for $account after successful refresh.") - } authStateFuture.complete(authState) } - exception != null -> { logger.warning("[Thread-${Thread.currentThread().id}] Token refresh failed for $account: $exception") authStateFuture.completeExceptionally(exception) } + else -> { + // Unexpected: neither token nor exception. treat as failure. + authStateFuture.completeExceptionally(IllegalStateException("No token, no exception")) + } } } - logger.info("[Thread-${Thread.currentThread().id}] Token refresh process completed for $account") - val result: AuthState? = authStateFuture.join() - - return result + return authStateFuture.join() } catch (e: CompletionException) { logger.log( Level.SEVERE, @@ -217,16 +225,12 @@ object OidcTokenRefresher { } } - /** - * Cleanup method to remove locks when accounts are removed. - */ fun removeAccountLock(accountName: String, accountType: String) { val accountKey = generateAccountKey(accountName, accountType) accountLocks.remove(accountKey) ongoingRefreshOperations.remove(accountKey) } - // Checks whether the given AuthorizationException indicates an invalid grant (requires re-login). private fun isInvalidGrant(ex: AuthorizationException?): Boolean { val invalidGrant = AuthorizationException.TokenRequestErrors.INVALID_GRANT return ex?.code == invalidGrant.code && ex.error == invalidGrant.error -- GitLab From 9950348e9e07ec73fc18ebe7d8bec5b3109c50a6 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 12 Nov 2025 21:24:36 +0600 Subject: [PATCH 3/4] refactor: change to `this` from `javaClass` as lock object in synchronized block As OidcTokenRefresher is a Kotlin `object`, by design, there'll be a single instance of it shared by all other classes. Although, they are semantically different, both achieve the same behaviour here. --- .../main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt index 34821e03f..181d9b470 100644 --- a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt +++ b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt @@ -75,7 +75,7 @@ object OidcTokenRefresher { writeAuthState: (AuthState) -> Unit ): AuthState? { if (account == null) { - return synchronized(javaClass) { + return synchronized(this) { performTokenRefresh(context, null, getClientAuth, readAuthState, writeAuthState) } } -- GitLab From 70515e5c5a9ff12b21d2cd7bbe1a716dd4991d4a Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 12 Nov 2025 21:28:29 +0600 Subject: [PATCH 4/4] refactor: improve variable names for clarity in OidcTokenRefresher --- .../android/sso/OidcTokenRefresher.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt index 181d9b470..f3849b1cb 100644 --- a/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt +++ b/app/src/main/java/com/nextcloud/android/sso/OidcTokenRefresher.kt @@ -84,8 +84,8 @@ object OidcTokenRefresher { val accountLock = accountLocks.computeIfAbsent(accountKey) { Any() } // Variables to communicate work outside the synchronized block - var existingOp: CompletableFuture? = null - var starterOp: CompletableFuture? = null + var existingOperation: CompletableFuture? = null + var starterOperation: CompletableFuture? = null var capturedAuthState: AuthState? = null // Short synchronized section: decide what to do @@ -95,8 +95,8 @@ object OidcTokenRefresher { } // If someone already started a refresh, capture that future and wait outside. - existingOp = ongoingRefreshOperations[accountKey] - if (existingOp != null) { + existingOperation = ongoingRefreshOperations[accountKey] + if (existingOperation != null) { if (BuildConfig.DEBUG) { logger.info("[Thread-${Thread.currentThread().id}] Observed ongoing refresh for $account") } @@ -117,21 +117,21 @@ object OidcTokenRefresher { logger.finest("[Thread-${Thread.currentThread().id}] Using cached AuthState for $account") } // no refresh needed. return current authState. - starterOp = null + starterOperation = null return@synchronized } // Need a refresh. Create and register a future, then perform refresh outside the lock. - val newOp = CompletableFuture() - ongoingRefreshOperations[accountKey] = newOp - starterOp = newOp + val newOperation = CompletableFuture() + ongoingRefreshOperations[accountKey] = newOperation + starterOperation = newOperation if (BuildConfig.DEBUG) { logger.finest("[Thread-${Thread.currentThread().id}] Registered new refresh operation for $account") } } // If there was an existing operation, wait for it (outside synchronized). - existingOp?.let { + existingOperation?.let { return try { it.join() } catch (e: CompletionException) { @@ -145,7 +145,7 @@ object OidcTokenRefresher { } // If starterOp is null here it means we returned cached state inside synchronized. - val op = starterOp ?: return capturedAuthState + val refreshOperation = starterOperation ?: return capturedAuthState // Perform the actual refresh outside lock. Complete the future and clean up. try { @@ -156,14 +156,14 @@ object OidcTokenRefresher { { capturedAuthState }, // pass the captured one to avoid re-reading under race writeAuthState ) - op.complete(result) + refreshOperation.complete(result) return result } catch (e: Throwable) { - op.completeExceptionally(e) + refreshOperation.completeExceptionally(e) throw e } finally { // remove only if the same future is still registered - ongoingRefreshOperations.remove(accountKey, op) + ongoingRefreshOperations.remove(accountKey, refreshOperation) if (BuildConfig.DEBUG) { logger.finest("[Thread-${Thread.currentThread().id}] Cleanup done for $account") } -- GitLab