18
18
import static java .util .concurrent .TimeUnit .MILLISECONDS ;
19
19
20
20
import java .time .Duration ;
21
- import java .util .concurrent . ScheduledExecutorService ;
21
+ import java .util .Random ;
22
22
import java .util .concurrent .ScheduledThreadPoolExecutor ;
23
+ import java .util .concurrent .Semaphore ;
24
+ import java .util .concurrent .SynchronousQueue ;
25
+ import java .util .concurrent .ThreadLocalRandom ;
26
+ import java .util .concurrent .ThreadPoolExecutor ;
23
27
import java .util .concurrent .atomic .AtomicBoolean ;
28
+ import java .util .concurrent .atomic .AtomicLong ;
24
29
import software .amazon .awssdk .annotations .SdkProtectedApi ;
25
30
import software .amazon .awssdk .annotations .SdkTestInternalApi ;
26
31
import software .amazon .awssdk .utils .Logger ;
27
32
import software .amazon .awssdk .utils .ThreadFactoryBuilder ;
28
- import software .amazon .awssdk .utils .Validate ;
29
33
30
34
/**
31
35
* A {@link CachedSupplier.PrefetchStrategy} that will run a single thread in the background to update the value. A call to
37
41
public class NonBlocking implements CachedSupplier .PrefetchStrategy {
38
42
private static final Logger log = Logger .loggerFor (NonBlocking .class );
39
43
44
+ /**
45
+ * The maximum number of concurrent refreshes allowed across all NonBlocking instances. This is to limit the amount of
46
+ * traffic that can be generated by background refreshes. If this is exceeded, a background refresh gets skipped. This just
47
+ * increases the chance of latency being pushed to the cached supplier caller, which is preferable to running out of memory.
48
+ */
49
+ private static final int MAX_CONCURRENT_REFRESHES = 100 ;
50
+
51
+ /**
52
+ * By default, how often we periodically call get() on the cached supplier. This may not necessarily call the downstream
53
+ * service if the cache is not stale.
54
+ */
55
+ private static final Duration DEFAULT_REFRESH_FREQUENCY = Duration .ofSeconds (60 );
56
+
57
+ /**
58
+ * By default, how much we jitter the {@link #DEFAULT_REFRESH_FREQUENCY}. This is done to prevent the case that a large
59
+ * number of NonBlocking instances are created at once, so they all try to refresh at the same time.
60
+ */
61
+ private static final Duration DEFAULT_REFRESH_FREQUENCY_JITTER = Duration .ofSeconds (10 );
62
+
63
+ /**
64
+ * The {@link Random} instance used for calculating jitter. See {@link #DEFAULT_REFRESH_FREQUENCY_JITTER}.
65
+ */
66
+ private static final Random JITTER_RANDOM = new Random ();
67
+
68
+ /**
69
+ * Threads used to periodically kick off credential refreshes based on the {@link #asyncRefreshFrequency}.
70
+ */
71
+ private static final ScheduledThreadPoolExecutor SCHEDULER =
72
+ new ScheduledThreadPoolExecutor (3 , new ThreadFactoryBuilder ().threadNamePrefix ("sdk-cached-supplier-scheduler" )
73
+ .daemonThreads (true )
74
+ .build ());
75
+
76
+ /**
77
+ * Threads used to do the actual work of refreshing the credentials (because the cached supplier might block, so we don't
78
+ * want the work to be done by a small thread pool). This executor is created as unbounded, but in reality it is limited by
79
+ * the {@link #MAX_CONCURRENT_REFRESHES} via {@link #BACKGROUND_REFRESH_LEASE}.
80
+ */
81
+ private static final ThreadPoolExecutor EXECUTOR =
82
+ new ThreadPoolExecutor (0 ,
83
+ Integer .MAX_VALUE ,
84
+ DEFAULT_REFRESH_FREQUENCY .toMillis () + DEFAULT_REFRESH_FREQUENCY_JITTER .toMillis () + 5_000 ,
85
+ MILLISECONDS ,
86
+ new SynchronousQueue <>(),
87
+ new ThreadFactoryBuilder ().daemonThreads (true ).build ());
88
+ /**
89
+ * A set of leases used to prevent concurrent refreshes beyond the limit described in {@link #MAX_CONCURRENT_REFRESHES}. If
90
+ * a lease cannot be acquired, the refresh is skipped.
91
+ */
92
+ private static final Semaphore BACKGROUND_REFRESH_LEASE = new Semaphore (MAX_CONCURRENT_REFRESHES );
93
+
94
+ /**
95
+ * An incrementing number, used to uniquely identify an instance of NonBlocking in the {@link #asyncThreadName}.
96
+ */
97
+ private static final AtomicLong THREAD_NUMBER = new AtomicLong (0 );
98
+
40
99
/**
41
100
* Whether we are currently refreshing the supplier. This is used to make sure only one caller is blocking at a time.
42
101
*/
43
102
private final AtomicBoolean currentlyRefreshing = new AtomicBoolean (false );
44
103
104
+ /**
105
+ * Name of the thread refreshing the cache for this strategy.
106
+ */
107
+ private final String asyncThreadName ;
108
+
45
109
/**
46
110
* How frequently to automatically refresh the supplier in the background.
47
111
*/
48
112
private final Duration asyncRefreshFrequency ;
49
113
50
114
/**
51
- * Single threaded executor to asynchronous refresh the value.
115
+ * Whether this strategy has been shutdown (and should stop doing background refreshes)
52
116
*/
53
- private final ScheduledExecutorService executor ;
117
+ private volatile boolean shutdown = false ;
54
118
55
119
/**
56
120
* Create a non-blocking prefetch strategy that uses the provided value for the name of the background thread that will be
57
121
* performing the update.
58
122
*/
59
123
public NonBlocking (String asyncThreadName ) {
60
- this (asyncThreadName , Duration .ofMinutes (1 ));
124
+ this (asyncThreadName , defaultRefreshFrequency ());
125
+ }
126
+
127
+ private static Duration defaultRefreshFrequency () {
128
+ // We jitter the default refresh frequency with each instance, so that objects created at the same time will not all be
129
+ // refreshing at the exact same times.
130
+ int jitter = Math .toIntExact (DEFAULT_REFRESH_FREQUENCY_JITTER .toMillis ());
131
+ long asyncRefreshFrequency = DEFAULT_REFRESH_FREQUENCY .toMillis () +
132
+ JITTER_RANDOM .nextInt (jitter * 2 + 1 ) - jitter ;
133
+ return Duration .ofMillis (asyncRefreshFrequency );
61
134
}
62
135
63
136
@ SdkTestInternalApi
64
137
NonBlocking (String asyncThreadName , Duration asyncRefreshFrequency ) {
65
- this .executor = newExecutor ( asyncThreadName );
138
+ this .asyncThreadName = asyncThreadName + THREAD_NUMBER . getAndIncrement ( );
66
139
this .asyncRefreshFrequency = asyncRefreshFrequency ;
67
140
}
68
141
69
- private static ScheduledExecutorService newExecutor (String asyncThreadName ) {
70
- Validate .paramNotBlank (asyncThreadName , "asyncThreadName" );
71
- return new ScheduledThreadPoolExecutor (1 , new ThreadFactoryBuilder ().daemonThreads (true )
72
- .threadNamePrefix (asyncThreadName )
73
- .build ());
142
+ @ SdkTestInternalApi
143
+ static ThreadPoolExecutor executor () {
144
+ return EXECUTOR ;
74
145
}
75
146
76
147
@ Override
@@ -79,10 +150,35 @@ public void initializeCachedSupplier(CachedSupplier<?> cachedSupplier) {
79
150
}
80
151
81
152
private void scheduleRefresh (CachedSupplier <?> cachedSupplier ) {
82
- executor .schedule (() -> {
153
+ SCHEDULER .schedule (() -> {
154
+ Thread .currentThread ().setName (asyncThreadName );
155
+
156
+ if (shutdown ) {
157
+ return ;
158
+ }
159
+
160
+ if (!BACKGROUND_REFRESH_LEASE .tryAcquire ()) {
161
+ log .warn (() -> "Skipped a background refresh to limit SDK resource consumption. Are you closing your SDK "
162
+ + "resources?" );
163
+ scheduleRefresh (cachedSupplier );
164
+ return ;
165
+ }
166
+
83
167
try {
84
- cachedSupplier .get ();
85
- } finally {
168
+ EXECUTOR .execute (() -> {
169
+ try {
170
+ Thread .currentThread ().setName (asyncThreadName );
171
+ cachedSupplier .get ();
172
+ } catch (Exception e ) {
173
+ log .error (() -> "Background refresh failed: " + e .getMessage (), e );
174
+ } finally {
175
+ BACKGROUND_REFRESH_LEASE .release ();
176
+ scheduleRefresh (cachedSupplier );
177
+ }
178
+ });
179
+ } catch (Throwable t ) {
180
+ BACKGROUND_REFRESH_LEASE .release ();
181
+ log .warn (() -> "Failed to submit a background refresh task." , t );
86
182
scheduleRefresh (cachedSupplier );
87
183
}
88
184
}, asyncRefreshFrequency .toMillis (), MILLISECONDS );
@@ -93,7 +189,8 @@ public void prefetch(Runnable valueUpdater) {
93
189
// Only run one async refresh at a time.
94
190
if (currentlyRefreshing .compareAndSet (false , true )) {
95
191
try {
96
- executor .submit (() -> {
192
+ EXECUTOR .submit (() -> {
193
+ Thread .currentThread ().setName (asyncThreadName );
97
194
try {
98
195
valueUpdater .run ();
99
196
} catch (RuntimeException e ) {
@@ -111,6 +208,6 @@ public void prefetch(Runnable valueUpdater) {
111
208
112
209
@ Override
113
210
public void close () {
114
- executor . shutdown () ;
211
+ shutdown = true ;
115
212
}
116
213
}
0 commit comments