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