3131
3232package com .google .auth .oauth2 ;
3333
34+ import static com .google .auth .oauth2 .LoggingUtils .log ;
35+
3436import com .google .api .client .util .Clock ;
3537import com .google .api .core .InternalApi ;
3638import com .google .auth .http .HttpTransportFactory ;
3739import com .google .common .annotations .VisibleForTesting ;
3840import com .google .common .util .concurrent .SettableFuture ;
41+ import java .util .concurrent .ExecutorService ;
42+ import java .util .concurrent .LinkedBlockingQueue ;
43+ import java .util .concurrent .ThreadPoolExecutor ;
44+ import java .util .concurrent .TimeUnit ;
45+ import java .util .concurrent .atomic .AtomicInteger ;
3946import java .util .concurrent .atomic .AtomicReference ;
4047import java .util .logging .Level ;
4148import javax .annotation .Nullable ;
@@ -59,7 +66,7 @@ final class RegionalAccessBoundaryManager {
5966 * The default maximum elapsed time in milliseconds for retrying Regional Access Boundary lookup
6067 * requests.
6168 */
62- private static final int DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS = 60000 ;
69+ static final int DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS = 60000 ;
6370
6471 /**
6572 * cachedRAB uses AtomicReference to provide thread-safe, lock-free access to the cached data for
@@ -78,22 +85,62 @@ final class RegionalAccessBoundaryManager {
7885 private final AtomicReference <CooldownState > cooldownState =
7986 new AtomicReference <>(new CooldownState (0 , INITIAL_COOLDOWN_MILLIS ));
8087
88+ // Unbounded thread creation is discouraged in library code to avoid resource
89+ // exhaustion. A shared, bounded executor service ensures a hard limit (5)
90+ // on concurrent refresh tasks, while threadCount provides unique names
91+ // for easier debugging.
92+ private static final AtomicInteger threadCount = new AtomicInteger (0 );
93+
94+ // Bounded executor service ensures hard limits on concurrent refresh tasks and queued tasks
95+ // to avoid resource exhaustion.
96+ private static final int EXECUTOR_POOL_SIZE = 5 ;
97+ private static final int EXECUTOR_QUEUE_CAPACITY = 100 ;
98+
99+ private static final ExecutorService DEFAULT_SHARED_EXECUTOR ;
100+
101+ static {
102+ ThreadPoolExecutor executor =
103+ new ThreadPoolExecutor (
104+ EXECUTOR_POOL_SIZE , // corePoolSize: threads to keep alive
105+ EXECUTOR_POOL_SIZE , // maximumPoolSize: max threads allowed
106+ 1 , // keepAliveTime: time to wait before terminating idle threads
107+ TimeUnit .HOURS , // unit for keepAliveTime
108+ new LinkedBlockingQueue <>(EXECUTOR_QUEUE_CAPACITY ), // work queue with bound
109+ r -> {
110+ Thread t = new Thread (r , "RAB-refresh-" + threadCount .getAndIncrement ());
111+ t .setDaemon (true );
112+ return t ;
113+ });
114+ // Allow core threads to time out so the executor can shrink to 0 when idle.
115+ // Ensures threads are released when idle to avoid unnecessary resource usage.
116+ executor .allowCoreThreadTimeOut (true );
117+ DEFAULT_SHARED_EXECUTOR = executor ;
118+ }
119+
81120 private final transient Clock clock ;
82121 private final int maxRetryElapsedTimeMillis ;
122+ private final ExecutorService executor ;
83123
84124 /**
85125 * Creates a new RegionalAccessBoundaryManager with the default retry timeout of 60 seconds.
86126 *
87127 * @param clock The clock to use for cooldown and expiration checks.
88128 */
89129 RegionalAccessBoundaryManager (Clock clock ) {
90- this (clock , DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS );
130+ this (clock , DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS , DEFAULT_SHARED_EXECUTOR );
91131 }
92132
93133 @ VisibleForTesting
94134 RegionalAccessBoundaryManager (Clock clock , int maxRetryElapsedTimeMillis ) {
135+ this (clock , maxRetryElapsedTimeMillis , DEFAULT_SHARED_EXECUTOR );
136+ }
137+
138+ @ VisibleForTesting
139+ RegionalAccessBoundaryManager (
140+ Clock clock , int maxRetryElapsedTimeMillis , ExecutorService executor ) {
95141 this .clock = clock != null ? clock : Clock .SYSTEM ;
96142 this .maxRetryElapsedTimeMillis = maxRetryElapsedTimeMillis ;
143+ this .executor = executor ;
97144 }
98145
99146 /**
@@ -111,6 +158,11 @@ RegionalAccessBoundary getCachedRAB() {
111158 return null ;
112159 }
113160
161+ @ VisibleForTesting
162+ void setCachedRAB (RegionalAccessBoundary rab ) {
163+ this .cachedRAB .set (rab );
164+ }
165+
114166 /**
115167 * Triggers an asynchronous refresh of the RegionalAccessBoundary if it is not already being
116168 * refreshed and if the cooldown period is not active.
@@ -161,19 +213,17 @@ void triggerAsyncRefresh(
161213 };
162214
163215 try {
164- // We use new Thread() here instead of
165- // CompletableFuture.runAsync() (which uses ForkJoinPool.commonPool()).
166- // This avoids consuming CPU resources since
167- // The common pool has a small, fixed number of threads designed for
168- // CPU-bound tasks.
169- Thread refreshThread = new Thread (refreshTask , "RAB-refresh-thread" );
170- refreshThread .setDaemon (true );
171- refreshThread .start ();
216+ this .executor .submit (refreshTask );
172217 } catch (Exception | Error e ) {
173218 // If scheduling fails (e.g., RejectedExecutionException, OutOfMemoryError for threads),
174219 // the task's finally block will never execute. We must release the lock here.
175- handleRefreshFailure (
176- new Exception ("Regional Access Boundary background refresh failed to schedule" , e ));
220+ log (
221+ LOGGER_PROVIDER ,
222+ Level .FINE ,
223+ null ,
224+ "Could not submit background refresh task for Regional Access Boundary. "
225+ + "This is non-blocking and the library will attempt to refresh on the next access. Error: "
226+ + e .getMessage ());
177227 future .setException (e );
178228 refreshFuture .set (null );
179229 }
@@ -201,13 +251,13 @@ private void handleRefreshFailure(Exception e) {
201251 // concurrent failures from logging redundant messages or incorrectly calculating
202252 // the exponential backoff.
203253 if (cooldownState .compareAndSet (currentCooldownState , next )) {
204- LoggingUtils . log (
254+ log (
205255 LOGGER_PROVIDER ,
206256 Level .FINE ,
207257 null ,
208- "Regional Access Boundary lookup failed; entering cooldown for "
258+ "Regional Access Boundary lookup was not successful; will retry after a cooldown of "
209259 + (next .durationMillis / 60000 )
210- + "m. Error : "
260+ + "m. This is handled automatically. Details : "
211261 + e .getMessage ());
212262 }
213263 }
0 commit comments