Skip to content

Commit a5f9bca

Browse files
cpovirkGoogle Java Core Libraries
authored and
Google Java Core Libraries
committed
Make CacheBuilder Duration overloads available in guava-android.
Also, related minor changes: - Import `java.time.Duration` instead of using it fully qualified. (Somehow, existing unqualified usages of `Duration` in _Javadoc_ references were working even without the import.) Compare cl/641315337. - "Upstream" a fix to a Javadoc reference to `maximumSize` from the Android flavor to the mainline. Fixes #7232 Progress on #6567 RELNOTES=`cache`: Added `CacheBuilder` `Duration` overloads to `guava-android`. PiperOrigin-RevId: 642993916
1 parent fdfbed1 commit a5f9bca

File tree

8 files changed

+341
-76
lines changed

8 files changed

+341
-76
lines changed

android/guava-tests/test/com/google/common/cache/CacheBuilderTest.java

+67-5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.google.common.collect.Maps;
3737
import com.google.common.collect.Sets;
3838
import com.google.common.testing.NullPointerTester;
39+
import java.time.Duration;
3940
import java.util.Map;
4041
import java.util.Random;
4142
import java.util.Set;
@@ -228,6 +229,18 @@ public void testValueStrengthSetTwice() {
228229
assertThrows(IllegalStateException.class, () -> builder2.weakValues());
229230
}
230231

232+
@GwtIncompatible // Duration
233+
@SuppressWarnings("Java7ApiChecker")
234+
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
235+
public void testLargeDurationsAreOk() {
236+
Duration threeHundredYears = Duration.ofDays(365 * 300);
237+
CacheBuilder<Object, Object> unused =
238+
CacheBuilder.newBuilder()
239+
.expireAfterWrite(threeHundredYears)
240+
.expireAfterAccess(threeHundredYears)
241+
.refreshAfterWrite(threeHundredYears);
242+
}
243+
231244
public void testTimeToLive_negative() {
232245
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
233246
try {
@@ -237,6 +250,14 @@ public void testTimeToLive_negative() {
237250
}
238251
}
239252

253+
@GwtIncompatible // Duration
254+
@SuppressWarnings("Java7ApiChecker")
255+
public void testTimeToLive_negative_duration() {
256+
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
257+
assertThrows(
258+
IllegalArgumentException.class, () -> builder.expireAfterWrite(Duration.ofSeconds(-1)));
259+
}
260+
240261
@SuppressWarnings("ReturnValueIgnored")
241262
public void testTimeToLive_small() {
242263
CacheBuilder.newBuilder().expireAfterWrite(1, NANOSECONDS).build(identityLoader());
@@ -254,6 +275,14 @@ public void testTimeToLive_setTwice() {
254275
}
255276
}
256277

278+
@GwtIncompatible // Duration
279+
@SuppressWarnings("Java7ApiChecker")
280+
public void testTimeToLive_setTwice_duration() {
281+
CacheBuilder<Object, Object> builder =
282+
CacheBuilder.newBuilder().expireAfterWrite(Duration.ofHours(1));
283+
assertThrows(IllegalStateException.class, () -> builder.expireAfterWrite(Duration.ofHours(1)));
284+
}
285+
257286
public void testTimeToIdle_negative() {
258287
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
259288
try {
@@ -263,6 +292,14 @@ public void testTimeToIdle_negative() {
263292
}
264293
}
265294

295+
@GwtIncompatible // Duration
296+
@SuppressWarnings("Java7ApiChecker")
297+
public void testTimeToIdle_negative_duration() {
298+
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
299+
assertThrows(
300+
IllegalArgumentException.class, () -> builder.expireAfterAccess(Duration.ofSeconds(-1)));
301+
}
302+
266303
@SuppressWarnings("ReturnValueIgnored")
267304
public void testTimeToIdle_small() {
268305
CacheBuilder.newBuilder().expireAfterAccess(1, NANOSECONDS).build(identityLoader());
@@ -280,12 +317,21 @@ public void testTimeToIdle_setTwice() {
280317
}
281318
}
282319

283-
@SuppressWarnings("ReturnValueIgnored")
320+
@GwtIncompatible // Duration
321+
@SuppressWarnings("Java7ApiChecker")
322+
public void testTimeToIdle_setTwice_duration() {
323+
CacheBuilder<Object, Object> builder =
324+
CacheBuilder.newBuilder().expireAfterAccess(Duration.ofHours(1));
325+
assertThrows(IllegalStateException.class, () -> builder.expireAfterAccess(Duration.ofHours(1)));
326+
}
327+
328+
@SuppressWarnings("Java7ApiChecker")
284329
public void testTimeToIdleAndToLive() {
285-
CacheBuilder.newBuilder()
286-
.expireAfterWrite(1, NANOSECONDS)
287-
.expireAfterAccess(1, NANOSECONDS)
288-
.build(identityLoader());
330+
LoadingCache<?, ?> unused =
331+
CacheBuilder.newBuilder()
332+
.expireAfterWrite(1, NANOSECONDS)
333+
.expireAfterAccess(1, NANOSECONDS)
334+
.build(identityLoader());
289335
// well, it didn't blow up.
290336
}
291337

@@ -295,13 +341,28 @@ public void testRefresh_zero() {
295341
assertThrows(IllegalArgumentException.class, () -> builder.refreshAfterWrite(0, SECONDS));
296342
}
297343

344+
@GwtIncompatible // Duration
345+
@SuppressWarnings("Java7ApiChecker")
346+
public void testRefresh_zero_duration() {
347+
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
348+
assertThrows(IllegalArgumentException.class, () -> builder.refreshAfterWrite(Duration.ZERO));
349+
}
350+
298351
@GwtIncompatible // refreshAfterWrite
299352
public void testRefresh_setTwice() {
300353
CacheBuilder<Object, Object> builder =
301354
CacheBuilder.newBuilder().refreshAfterWrite(3600, SECONDS);
302355
assertThrows(IllegalStateException.class, () -> builder.refreshAfterWrite(3600, SECONDS));
303356
}
304357

358+
@GwtIncompatible // Duration
359+
@SuppressWarnings("Java7ApiChecker")
360+
public void testRefresh_setTwice_duration() {
361+
CacheBuilder<Object, Object> builder =
362+
CacheBuilder.newBuilder().refreshAfterWrite(Duration.ofHours(1));
363+
assertThrows(IllegalStateException.class, () -> builder.refreshAfterWrite(Duration.ofHours(1)));
364+
}
365+
305366
public void testTicker_setTwice() {
306367
Ticker testTicker = Ticker.systemTicker();
307368
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder().ticker(testTicker);
@@ -508,6 +569,7 @@ public void testRemovalNotification_get_basher() throws InterruptedException {
508569
final AtomicInteger computeCount = new AtomicInteger();
509570
final AtomicInteger exceptionCount = new AtomicInteger();
510571
final AtomicInteger computeNullCount = new AtomicInteger();
572+
@SuppressWarnings("CacheLoaderNull") // test of handling of erroneous implementation
511573
CacheLoader<String, String> countingIdentityLoader =
512574
new CacheLoader<String, String>() {
513575
@Override

android/guava/src/com/google/common/cache/CacheBuilder.java

+154-9
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
import com.google.common.cache.AbstractCache.StatsCounter;
3131
import com.google.common.cache.LocalCache.Strength;
3232
import com.google.errorprone.annotations.CanIgnoreReturnValue;
33+
import com.google.j2objc.annotations.J2ObjCIncompatible;
3334
import java.lang.ref.SoftReference;
3435
import java.lang.ref.WeakReference;
36+
import java.time.Duration;
3537
import java.util.ConcurrentModificationException;
3638
import java.util.IdentityHashMap;
3739
import java.util.Map;
@@ -104,7 +106,7 @@
104106
* <pre>{@code
105107
* LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
106108
* .maximumSize(10000)
107-
* .expireAfterWrite(10, TimeUnit.MINUTES)
109+
* .expireAfterWrite(Duration.ofMinutes(10))
108110
* .removalListener(MY_LISTENER)
109111
* .build(
110112
* new CacheLoader<Key, Graph>() {
@@ -194,10 +196,10 @@ public final class CacheBuilder<K, V> {
194196
private static final int DEFAULT_INITIAL_CAPACITY = 16;
195197
private static final int DEFAULT_CONCURRENCY_LEVEL = 4;
196198

197-
@SuppressWarnings("GoodTime") // should be a java.time.Duration
199+
@SuppressWarnings("GoodTime") // should be a Duration
198200
private static final int DEFAULT_EXPIRATION_NANOS = 0;
199201

200-
@SuppressWarnings("GoodTime") // should be a java.time.Duration
202+
@SuppressWarnings("GoodTime") // should be a Duration
201203
private static final int DEFAULT_REFRESH_NANOS = 0;
202204

203205
static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER =
@@ -289,13 +291,13 @@ private static final class LoggerHolder {
289291
@CheckForNull Strength keyStrength;
290292
@CheckForNull Strength valueStrength;
291293

292-
@SuppressWarnings("GoodTime") // should be a java.time.Duration
294+
@SuppressWarnings("GoodTime") // should be a Duration
293295
long expireAfterWriteNanos = UNSET_INT;
294296

295-
@SuppressWarnings("GoodTime") // should be a java.time.Duration
297+
@SuppressWarnings("GoodTime") // should be a Duration
296298
long expireAfterAccessNanos = UNSET_INT;
297299

298-
@SuppressWarnings("GoodTime") // should be a java.time.Duration
300+
@SuppressWarnings("GoodTime") // should be a Duration
299301
long refreshNanos = UNSET_INT;
300302

301303
@CheckForNull Equivalence<Object> keyEquivalence;
@@ -711,12 +713,48 @@ Strength getValueStrength() {
711713
*
712714
* @param duration the length of time after an entry is created that it should be automatically
713715
* removed
716+
* @return this {@code CacheBuilder} instance (for chaining)
717+
* @throws IllegalArgumentException if {@code duration} is negative
718+
* @throws IllegalStateException if {@link #expireAfterWrite} was already set
719+
* @throws ArithmeticException for durations greater than +/- approximately 292 years
720+
* @since NEXT (but since 25.0 in the JRE <a
721+
* href="https://github.com/google/guava#guava-google-core-libraries-for-java">flavor</a>)
722+
*/
723+
@J2ObjCIncompatible
724+
@GwtIncompatible // Duration
725+
@SuppressWarnings({
726+
"GoodTime", // Duration decomposition
727+
"Java7ApiChecker",
728+
})
729+
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
730+
@CanIgnoreReturnValue
731+
public CacheBuilder<K, V> expireAfterWrite(Duration duration) {
732+
return expireAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
733+
}
734+
735+
/**
736+
* Specifies that each entry should be automatically removed from the cache once a fixed duration
737+
* has elapsed after the entry's creation, or the most recent replacement of its value.
738+
*
739+
* <p>When {@code duration} is zero, this method hands off to {@link #maximumSize(long)
740+
* maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be
741+
* useful in testing, or to disable caching temporarily without a code change.
742+
*
743+
* <p>Expired entries may be counted in {@link Cache#size}, but will never be visible to read or
744+
* write operations. Expired entries are cleaned up as part of the routine maintenance described
745+
* in the class javadoc.
746+
*
747+
* <p>If you can represent the duration as a {@link Duration} (which should be preferred when
748+
* feasible), use {@link #expireAfterWrite(Duration)} instead.
749+
*
750+
* @param duration the length of time after an entry is created that it should be automatically
751+
* removed
714752
* @param unit the unit that {@code duration} is expressed in
715753
* @return this {@code CacheBuilder} instance (for chaining)
716754
* @throws IllegalArgumentException if {@code duration} is negative
717755
* @throws IllegalStateException if {@link #expireAfterWrite} was already set
718756
*/
719-
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
757+
@SuppressWarnings("GoodTime") // should accept a Duration
720758
@CanIgnoreReturnValue
721759
public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
722760
checkState(
@@ -733,6 +771,44 @@ long getExpireAfterWriteNanos() {
733771
return (expireAfterWriteNanos == UNSET_INT) ? DEFAULT_EXPIRATION_NANOS : expireAfterWriteNanos;
734772
}
735773

774+
/**
775+
* Specifies that each entry should be automatically removed from the cache once a fixed duration
776+
* has elapsed after the entry's creation, the most recent replacement of its value, or its last
777+
* access. Access time is reset by all cache read and write operations (including {@code
778+
* Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by {@code
779+
* containsKey(Object)}, nor by operations on the collection-views of {@link Cache#asMap}}. So,
780+
* for example, iterating through {@code Cache.asMap().entrySet()} does not reset access time for
781+
* the entries you retrieve.
782+
*
783+
* <p>When {@code duration} is zero, this method hands off to {@link #maximumSize(long)
784+
* maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be
785+
* useful in testing, or to disable caching temporarily without a code change.
786+
*
787+
* <p>Expired entries may be counted in {@link Cache#size}, but will never be visible to read or
788+
* write operations. Expired entries are cleaned up as part of the routine maintenance described
789+
* in the class javadoc.
790+
*
791+
* @param duration the length of time after an entry is last accessed that it should be
792+
* automatically removed
793+
* @return this {@code CacheBuilder} instance (for chaining)
794+
* @throws IllegalArgumentException if {@code duration} is negative
795+
* @throws IllegalStateException if {@link #expireAfterAccess} was already set
796+
* @throws ArithmeticException for durations greater than +/- approximately 292 years
797+
* @since NEXT (but since 25.0 in the JRE <a
798+
* href="https://github.com/google/guava#guava-google-core-libraries-for-java">flavor</a>)
799+
*/
800+
@J2ObjCIncompatible
801+
@GwtIncompatible // Duration
802+
@SuppressWarnings({
803+
"GoodTime", // Duration decomposition
804+
"Java7ApiChecker",
805+
})
806+
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
807+
@CanIgnoreReturnValue
808+
public CacheBuilder<K, V> expireAfterAccess(Duration duration) {
809+
return expireAfterAccess(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
810+
}
811+
736812
/**
737813
* Specifies that each entry should be automatically removed from the cache once a fixed duration
738814
* has elapsed after the entry's creation, the most recent replacement of its value, or its last
@@ -750,14 +826,17 @@ long getExpireAfterWriteNanos() {
750826
* write operations. Expired entries are cleaned up as part of the routine maintenance described
751827
* in the class javadoc.
752828
*
829+
* <p>If you can represent the duration as a {@link Duration} (which should be preferred when
830+
* feasible), use {@link #expireAfterAccess(Duration)} instead.
831+
*
753832
* @param duration the length of time after an entry is last accessed that it should be
754833
* automatically removed
755834
* @param unit the unit that {@code duration} is expressed in
756835
* @return this {@code CacheBuilder} instance (for chaining)
757836
* @throws IllegalArgumentException if {@code duration} is negative
758837
* @throws IllegalStateException if {@link #expireAfterAccess} was already set
759838
*/
760-
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
839+
@SuppressWarnings("GoodTime") // should accept a Duration
761840
@CanIgnoreReturnValue
762841
public CacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) {
763842
checkState(
@@ -776,6 +855,46 @@ long getExpireAfterAccessNanos() {
776855
: expireAfterAccessNanos;
777856
}
778857

858+
/**
859+
* Specifies that active entries are eligible for automatic refresh once a fixed duration has
860+
* elapsed after the entry's creation, or the most recent replacement of its value. The semantics
861+
* of refreshes are specified in {@link LoadingCache#refresh}, and are performed by calling {@link
862+
* CacheLoader#reload}.
863+
*
864+
* <p>As the default implementation of {@link CacheLoader#reload} is synchronous, it is
865+
* recommended that users of this method override {@link CacheLoader#reload} with an asynchronous
866+
* implementation; otherwise refreshes will be performed during unrelated cache read and write
867+
* operations.
868+
*
869+
* <p>Currently automatic refreshes are performed when the first stale request for an entry
870+
* occurs. The request triggering refresh will make a synchronous call to {@link
871+
* CacheLoader#reload}
872+
* to obtain a future of the new value. If the returned future is already complete, it is returned
873+
* immediately. Otherwise, the old value is returned.
874+
*
875+
* <p><b>Note:</b> <i>all exceptions thrown during refresh will be logged and then swallowed</i>.
876+
*
877+
* @param duration the length of time after an entry is created that it should be considered
878+
* stale, and thus eligible for refresh
879+
* @return this {@code CacheBuilder} instance (for chaining)
880+
* @throws IllegalArgumentException if {@code duration} is negative
881+
* @throws IllegalStateException if {@link #refreshAfterWrite} was already set
882+
* @throws ArithmeticException for durations greater than +/- approximately 292 years
883+
* @since NEXT (but since 25.0 in the JRE <a
884+
* href="https://github.com/google/guava#guava-google-core-libraries-for-java">flavor</a>)
885+
*/
886+
@J2ObjCIncompatible
887+
@GwtIncompatible // Duration
888+
@SuppressWarnings({
889+
"GoodTime", // Duration decomposition
890+
"Java7ApiChecker",
891+
})
892+
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
893+
@CanIgnoreReturnValue
894+
public CacheBuilder<K, V> refreshAfterWrite(Duration duration) {
895+
return refreshAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
896+
}
897+
779898
/**
780899
* Specifies that active entries are eligible for automatic refresh once a fixed duration has
781900
* elapsed after the entry's creation, or the most recent replacement of its value. The semantics
@@ -795,6 +914,9 @@ long getExpireAfterAccessNanos() {
795914
*
796915
* <p><b>Note:</b> <i>all exceptions thrown during refresh will be logged and then swallowed</i>.
797916
*
917+
* <p>If you can represent the duration as a {@link Duration} (which should be preferred when
918+
* feasible), use {@link #refreshAfterWrite(Duration)} instead.
919+
*
798920
* @param duration the length of time after an entry is created that it should be considered
799921
* stale, and thus eligible for refresh
800922
* @param unit the unit that {@code duration} is expressed in
@@ -804,7 +926,7 @@ long getExpireAfterAccessNanos() {
804926
* @since 11.0
805927
*/
806928
@GwtIncompatible // To be supported (synchronously).
807-
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
929+
@SuppressWarnings("GoodTime") // should accept a Duration
808930
@CanIgnoreReturnValue
809931
public CacheBuilder<K, V> refreshAfterWrite(long duration, TimeUnit unit) {
810932
checkNotNull(unit);
@@ -1002,4 +1124,27 @@ public String toString() {
10021124
}
10031125
return s.toString();
10041126
}
1127+
1128+
/**
1129+
* Returns the number of nanoseconds of the given duration without throwing or overflowing.
1130+
*
1131+
* <p>Instead of throwing {@link ArithmeticException}, this method silently saturates to either
1132+
* {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}. This behavior can be useful when decomposing
1133+
* a duration in order to call a legacy API which requires a {@code long, TimeUnit} pair.
1134+
*/
1135+
@GwtIncompatible // Duration
1136+
@SuppressWarnings({
1137+
"GoodTime", // Duration decomposition
1138+
"Java7ApiChecker",
1139+
})
1140+
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
1141+
private static long toNanosSaturated(Duration duration) {
1142+
// Using a try/catch seems lazy, but the catch block will rarely get invoked (except for
1143+
// durations longer than approximately +/- 292 years).
1144+
try {
1145+
return duration.toNanos();
1146+
} catch (ArithmeticException tooBig) {
1147+
return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE;
1148+
}
1149+
}
10051150
}

0 commit comments

Comments
 (0)