60
60
import java .util .concurrent .ForkJoinTask ;
61
61
import java .util .concurrent .ThreadLocalRandom ;
62
62
import java .util .concurrent .TimeUnit ;
63
+ import java .util .concurrent .atomic .AtomicReference ;
63
64
import java .util .concurrent .locks .ReentrantLock ;
64
65
import java .util .function .BiFunction ;
65
66
import java .util .function .Consumer ;
@@ -221,10 +222,10 @@ abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef<K, V>
221
222
final Executor executor ;
222
223
final boolean isAsync ;
223
224
224
- // The collection views
225
- @ Nullable transient Set < K > keySet ;
226
- @ Nullable transient Collection < V > values ;
227
- @ Nullable transient Set < Entry < K , V >> entrySet ;
225
+ @ Nullable Set < K > keySet ;
226
+ @ Nullable Collection < V > values ;
227
+ @ Nullable Set < Entry < K , V >> entrySet ;
228
+ AtomicReference < ConcurrentMap < Object , CompletableFuture <?>>> refreshes ;
228
229
229
230
/** Creates an instance based on the builder's configuration. */
230
231
protected BoundedLocalCache (Caffeine <K , V > builder ,
@@ -233,6 +234,7 @@ protected BoundedLocalCache(Caffeine<K, V> builder,
233
234
this .cacheLoader = cacheLoader ;
234
235
executor = builder .getExecutor ();
235
236
evictionLock = new ReentrantLock ();
237
+ refreshes = new AtomicReference <>();
236
238
weigher = builder .getWeigher (isAsync );
237
239
writer = builder .getCacheWriter (isAsync );
238
240
drainBuffersTask = new PerformCleanupTask (this );
@@ -289,11 +291,29 @@ public final Executor executor() {
289
291
return executor ;
290
292
}
291
293
294
+ @ Override
295
+ @ SuppressWarnings ("NullAway" )
296
+ public ConcurrentMap <Object , CompletableFuture <?>> refreshes () {
297
+ var pending = refreshes .get ();
298
+ if (pending == null ) {
299
+ pending = new ConcurrentHashMap <>();
300
+ if (!refreshes .compareAndSet (null , pending )) {
301
+ pending = refreshes .get ();
302
+ }
303
+ }
304
+ return pending ;
305
+ }
306
+
292
307
/** Returns whether this cache notifies a writer when an entry is modified. */
293
308
protected boolean hasWriter () {
294
309
return (writer != CacheWriter .disabledWriter ());
295
310
}
296
311
312
+ @ Override
313
+ public Object referenceKey (K key ) {
314
+ return nodeFactory .newLookupKey (key );
315
+ }
316
+
297
317
/* --------------- Stats Support --------------- */
298
318
299
319
@ Override
@@ -900,8 +920,9 @@ boolean evictEntry(Node<K, V> node, RemovalCause cause, long now) {
900
920
boolean [] removed = new boolean [1 ];
901
921
boolean [] resurrect = new boolean [1 ];
902
922
RemovalCause [] actualCause = new RemovalCause [1 ];
923
+ Object keyReference = node .getKeyReference ();
903
924
904
- data .computeIfPresent (node . getKeyReference () , (k , n ) -> {
925
+ data .computeIfPresent (keyReference , (k , n ) -> {
905
926
if (n != node ) {
906
927
return n ;
907
928
}
@@ -964,6 +985,12 @@ boolean evictEntry(Node<K, V> node, RemovalCause cause, long now) {
964
985
965
986
if (removed [0 ]) {
966
987
statsCounter ().recordEviction (node .getWeight (), actualCause [0 ]);
988
+
989
+ var pending = refreshes .get ();
990
+ if (pending != null ) {
991
+ pending .remove (keyReference );
992
+ }
993
+
967
994
if (hasRemovalListener ()) {
968
995
// Notify the listener only if the entry was evicted. This must be performed as the last
969
996
// step during eviction to safe guard against the executor rejecting the notification task.
@@ -1171,51 +1198,60 @@ void refreshIfNeeded(Node<K, V> node, long now) {
1171
1198
if (!refreshAfterWrite ()) {
1172
1199
return ;
1173
1200
}
1201
+
1174
1202
K key ;
1175
1203
V oldValue ;
1176
- long oldWriteTime = node .getWriteTime ();
1177
- long refreshWriteTime = ( now + ASYNC_EXPIRY );
1178
- if (((now - oldWriteTime ) > refreshAfterWriteNanos ())
1204
+ long writeTime = node .getWriteTime ();
1205
+ Object keyReference = node . getKeyReference ( );
1206
+ if (((now - writeTime ) > refreshAfterWriteNanos ()) && ( keyReference != null )
1179
1207
&& ((key = node .getKey ()) != null ) && ((oldValue = node .getValue ()) != null )
1180
- && node .casWriteTime (oldWriteTime , refreshWriteTime )) {
1181
- try {
1182
- CompletableFuture <V > refreshFuture ;
1183
- long startTime = statsTicker ().read ();
1208
+ && !refreshes ().containsKey (keyReference )) {
1209
+ long [] startTime = new long [1 ];
1210
+ @ SuppressWarnings ({"unchecked" , "rawtypes" })
1211
+ CompletableFuture <V >[] refreshFuture = new CompletableFuture [1 ];
1212
+ refreshes ().computeIfAbsent (keyReference , k -> {
1213
+ startTime [0 ] = statsTicker ().read ();
1184
1214
if (isAsync ) {
1185
1215
@ SuppressWarnings ("unchecked" )
1186
1216
CompletableFuture <V > future = (CompletableFuture <V >) oldValue ;
1187
1217
if (Async .isReady (future )) {
1188
1218
@ SuppressWarnings ("NullAway" )
1189
- CompletableFuture <V > refresh = future .thenCompose (value ->
1190
- cacheLoader .asyncReload (key , value , executor ));
1191
- refreshFuture = refresh ;
1219
+ var refresh = cacheLoader .asyncReload (key , future .join (), executor );
1220
+ refreshFuture [0 ] = refresh ;
1192
1221
} else {
1193
1222
// no-op if load is pending
1194
- node .casWriteTime (refreshWriteTime , oldWriteTime );
1195
- return ;
1223
+ return future ;
1196
1224
}
1197
1225
} else {
1198
1226
@ SuppressWarnings ("NullAway" )
1199
- CompletableFuture < V > refresh = cacheLoader .asyncReload (key , oldValue , executor );
1200
- refreshFuture = refresh ;
1227
+ var refresh = cacheLoader .asyncReload (key , oldValue , executor );
1228
+ refreshFuture [ 0 ] = refresh ;
1201
1229
}
1202
- refreshFuture .whenComplete ((newValue , error ) -> {
1203
- long loadTime = statsTicker ().read () - startTime ;
1230
+ return refreshFuture [0 ];
1231
+ });
1232
+
1233
+ if (refreshFuture [0 ] != null ) {
1234
+ refreshFuture [0 ].whenComplete ((newValue , error ) -> {
1235
+ long loadTime = statsTicker ().read () - startTime [0 ];
1204
1236
if (error != null ) {
1205
1237
logger .log (Level .WARNING , "Exception thrown during refresh" , error );
1206
- node . casWriteTime ( refreshWriteTime , oldWriteTime );
1238
+ refreshes (). remove ( keyReference , refreshFuture [ 0 ] );
1207
1239
statsCounter ().recordLoadFailure (loadTime );
1208
1240
return ;
1209
1241
}
1210
1242
1211
1243
@ SuppressWarnings ("unchecked" )
1212
- V value = (isAsync && (newValue != null )) ? (V ) refreshFuture : newValue ;
1244
+ V value = (isAsync && (newValue != null )) ? (V ) refreshFuture [ 0 ] : newValue ;
1213
1245
1214
1246
boolean [] discard = new boolean [1 ];
1215
1247
compute (key , (k , currentValue ) -> {
1216
1248
if (currentValue == null ) {
1217
- return value ;
1218
- } else if ((currentValue == oldValue ) && (node .getWriteTime () == refreshWriteTime )) {
1249
+ if (value == null ) {
1250
+ return null ;
1251
+ } else if (refreshes ().get (key ) == refreshFuture [0 ]) {
1252
+ return value ;
1253
+ }
1254
+ } else if ((currentValue == oldValue ) && (node .getWriteTime () == writeTime )) {
1219
1255
return value ;
1220
1256
}
1221
1257
discard [0 ] = true ;
@@ -1230,10 +1266,9 @@ void refreshIfNeeded(Node<K, V> node, long now) {
1230
1266
} else {
1231
1267
statsCounter ().recordLoadSuccess (loadTime );
1232
1268
}
1269
+
1270
+ refreshes ().remove (keyReference , refreshFuture [0 ]);
1233
1271
});
1234
- } catch (Throwable t ) {
1235
- node .casWriteTime (refreshWriteTime , oldWriteTime );
1236
- logger .log (Level .ERROR , "Exception thrown when submitting refresh task" , t );
1237
1272
}
1238
1273
}
1239
1274
}
@@ -1781,8 +1816,12 @@ public void clear() {
1781
1816
}
1782
1817
1783
1818
// Discard all entries
1784
- for (Node <K , V > node : data .values ()) {
1785
- removeNode (node , now );
1819
+ var pending = refreshes .get ();
1820
+ for (var entry : data .entrySet ()) {
1821
+ removeNode (entry .getValue (), now );
1822
+ if (pending != null ) {
1823
+ pending .remove (entry .getKey ());
1824
+ }
1786
1825
}
1787
1826
1788
1827
// Discard all pending reads
@@ -2098,8 +2137,9 @@ public Map<K, V> getAllPresent(Iterable<?> keys) {
2098
2137
@ SuppressWarnings ("unchecked" )
2099
2138
V [] oldValue = (V []) new Object [1 ];
2100
2139
RemovalCause [] cause = new RemovalCause [1 ];
2140
+ Object lookupKey = nodeFactory .newLookupKey (key );
2101
2141
2102
- data .computeIfPresent (nodeFactory . newLookupKey ( key ) , (k , n ) -> {
2142
+ data .computeIfPresent (lookupKey , (k , n ) -> {
2103
2143
synchronized (n ) {
2104
2144
oldValue [0 ] = n .getValue ();
2105
2145
if (oldValue [0 ] == null ) {
@@ -2117,6 +2157,11 @@ public Map<K, V> getAllPresent(Iterable<?> keys) {
2117
2157
});
2118
2158
2119
2159
if (cause [0 ] != null ) {
2160
+ var pending = refreshes .get ();
2161
+ if (pending != null ) {
2162
+ pending .remove (lookupKey );
2163
+ }
2164
+
2120
2165
afterWrite (new RemovalTask (node [0 ]));
2121
2166
if (hasRemovalListener ()) {
2122
2167
notifyRemoval (castKey , oldValue [0 ], cause [0 ]);
@@ -2139,8 +2184,9 @@ public boolean remove(Object key, Object value) {
2139
2184
@ SuppressWarnings ("unchecked" )
2140
2185
V [] oldValue = (V []) new Object [1 ];
2141
2186
RemovalCause [] cause = new RemovalCause [1 ];
2187
+ Object lookupKey = nodeFactory .newLookupKey (key );
2142
2188
2143
- data .computeIfPresent (nodeFactory . newLookupKey ( key ) , (kR , node ) -> {
2189
+ data .computeIfPresent (lookupKey , (kR , node ) -> {
2144
2190
synchronized (node ) {
2145
2191
oldKey [0 ] = node .getKey ();
2146
2192
oldValue [0 ] = node .getValue ();
@@ -2162,7 +2208,13 @@ public boolean remove(Object key, Object value) {
2162
2208
2163
2209
if (removed [0 ] == null ) {
2164
2210
return false ;
2165
- } else if (hasRemovalListener ()) {
2211
+ }
2212
+
2213
+ var pending = refreshes .get ();
2214
+ if (pending != null ) {
2215
+ pending .remove (lookupKey );
2216
+ }
2217
+ if (hasRemovalListener ()) {
2166
2218
notifyRemoval (oldKey [0 ], oldValue [0 ], cause [0 ]);
2167
2219
}
2168
2220
afterWrite (new RemovalTask (removed [0 ]));
@@ -2581,15 +2633,14 @@ public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2581
2633
if (expiresAfterWrite () || (weightedDifference != 0 )) {
2582
2634
afterWrite (new UpdateTask (node , weightedDifference ));
2583
2635
} else {
2584
- if (cause [0 ] == null ) {
2585
- if (!isComputingAsync (node )) {
2586
- tryExpireAfterRead (node , key , newValue [0 ], expiry (), now [0 ]);
2587
- setAccessTime (node , now [0 ]);
2588
- }
2589
- } else if (cause [0 ] == RemovalCause .COLLECTED ) {
2590
- scheduleDrainBuffers ();
2636
+ if ((cause [0 ] == null ) && !isComputingAsync (node )) {
2637
+ tryExpireAfterRead (node , key , newValue [0 ], expiry (), now [0 ]);
2638
+ setAccessTime (node , now [0 ]);
2591
2639
}
2592
2640
afterRead (node , now [0 ], /* recordHit */ false );
2641
+ if ((cause [0 ] != null ) && cause [0 ].wasEvicted ()) {
2642
+ scheduleDrainBuffers ();
2643
+ }
2593
2644
}
2594
2645
}
2595
2646
0 commit comments