Skip to content

Commit

Permalink
Make CacheBuilder Duration overloads available in guava-android.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cpovirk authored and Google Java Core Libraries committed Jun 13, 2024
1 parent fdfbed1 commit a5f9bca
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.testing.NullPointerTester;
import java.time.Duration;
import java.util.Map;
import java.util.Random;
import java.util.Set;
Expand Down Expand Up @@ -228,6 +229,18 @@ public void testValueStrengthSetTwice() {
assertThrows(IllegalStateException.class, () -> builder2.weakValues());
}

@GwtIncompatible // Duration
@SuppressWarnings("Java7ApiChecker")
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
public void testLargeDurationsAreOk() {
Duration threeHundredYears = Duration.ofDays(365 * 300);
CacheBuilder<Object, Object> unused =
CacheBuilder.newBuilder()
.expireAfterWrite(threeHundredYears)
.expireAfterAccess(threeHundredYears)
.refreshAfterWrite(threeHundredYears);
}

public void testTimeToLive_negative() {
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
try {
Expand All @@ -237,6 +250,14 @@ public void testTimeToLive_negative() {
}
}

@GwtIncompatible // Duration
@SuppressWarnings("Java7ApiChecker")
public void testTimeToLive_negative_duration() {
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
assertThrows(
IllegalArgumentException.class, () -> builder.expireAfterWrite(Duration.ofSeconds(-1)));
}

@SuppressWarnings("ReturnValueIgnored")
public void testTimeToLive_small() {
CacheBuilder.newBuilder().expireAfterWrite(1, NANOSECONDS).build(identityLoader());
Expand All @@ -254,6 +275,14 @@ public void testTimeToLive_setTwice() {
}
}

@GwtIncompatible // Duration
@SuppressWarnings("Java7ApiChecker")
public void testTimeToLive_setTwice_duration() {
CacheBuilder<Object, Object> builder =
CacheBuilder.newBuilder().expireAfterWrite(Duration.ofHours(1));
assertThrows(IllegalStateException.class, () -> builder.expireAfterWrite(Duration.ofHours(1)));
}

public void testTimeToIdle_negative() {
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
try {
Expand All @@ -263,6 +292,14 @@ public void testTimeToIdle_negative() {
}
}

@GwtIncompatible // Duration
@SuppressWarnings("Java7ApiChecker")
public void testTimeToIdle_negative_duration() {
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
assertThrows(
IllegalArgumentException.class, () -> builder.expireAfterAccess(Duration.ofSeconds(-1)));
}

@SuppressWarnings("ReturnValueIgnored")
public void testTimeToIdle_small() {
CacheBuilder.newBuilder().expireAfterAccess(1, NANOSECONDS).build(identityLoader());
Expand All @@ -280,12 +317,21 @@ public void testTimeToIdle_setTwice() {
}
}

@SuppressWarnings("ReturnValueIgnored")
@GwtIncompatible // Duration
@SuppressWarnings("Java7ApiChecker")
public void testTimeToIdle_setTwice_duration() {
CacheBuilder<Object, Object> builder =
CacheBuilder.newBuilder().expireAfterAccess(Duration.ofHours(1));
assertThrows(IllegalStateException.class, () -> builder.expireAfterAccess(Duration.ofHours(1)));
}

@SuppressWarnings("Java7ApiChecker")
public void testTimeToIdleAndToLive() {
CacheBuilder.newBuilder()
.expireAfterWrite(1, NANOSECONDS)
.expireAfterAccess(1, NANOSECONDS)
.build(identityLoader());
LoadingCache<?, ?> unused =
CacheBuilder.newBuilder()
.expireAfterWrite(1, NANOSECONDS)
.expireAfterAccess(1, NANOSECONDS)
.build(identityLoader());
// well, it didn't blow up.
}

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

@GwtIncompatible // Duration
@SuppressWarnings("Java7ApiChecker")
public void testRefresh_zero_duration() {
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
assertThrows(IllegalArgumentException.class, () -> builder.refreshAfterWrite(Duration.ZERO));
}

@GwtIncompatible // refreshAfterWrite
public void testRefresh_setTwice() {
CacheBuilder<Object, Object> builder =
CacheBuilder.newBuilder().refreshAfterWrite(3600, SECONDS);
assertThrows(IllegalStateException.class, () -> builder.refreshAfterWrite(3600, SECONDS));
}

@GwtIncompatible // Duration
@SuppressWarnings("Java7ApiChecker")
public void testRefresh_setTwice_duration() {
CacheBuilder<Object, Object> builder =
CacheBuilder.newBuilder().refreshAfterWrite(Duration.ofHours(1));
assertThrows(IllegalStateException.class, () -> builder.refreshAfterWrite(Duration.ofHours(1)));
}

public void testTicker_setTwice() {
Ticker testTicker = Ticker.systemTicker();
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder().ticker(testTicker);
Expand Down Expand Up @@ -508,6 +569,7 @@ public void testRemovalNotification_get_basher() throws InterruptedException {
final AtomicInteger computeCount = new AtomicInteger();
final AtomicInteger exceptionCount = new AtomicInteger();
final AtomicInteger computeNullCount = new AtomicInteger();
@SuppressWarnings("CacheLoaderNull") // test of handling of erroneous implementation
CacheLoader<String, String> countingIdentityLoader =
new CacheLoader<String, String>() {
@Override
Expand Down
163 changes: 154 additions & 9 deletions android/guava/src/com/google/common/cache/CacheBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
import com.google.common.cache.AbstractCache.StatsCounter;
import com.google.common.cache.LocalCache.Strength;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.j2objc.annotations.J2ObjCIncompatible;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.ConcurrentModificationException;
import java.util.IdentityHashMap;
import java.util.Map;
Expand Down Expand Up @@ -104,7 +106,7 @@
* <pre>{@code
* LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
* .maximumSize(10000)
* .expireAfterWrite(10, TimeUnit.MINUTES)
* .expireAfterWrite(Duration.ofMinutes(10))
* .removalListener(MY_LISTENER)
* .build(
* new CacheLoader<Key, Graph>() {
Expand Down Expand Up @@ -194,10 +196,10 @@ public final class CacheBuilder<K, V> {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private static final int DEFAULT_CONCURRENCY_LEVEL = 4;

@SuppressWarnings("GoodTime") // should be a java.time.Duration
@SuppressWarnings("GoodTime") // should be a Duration
private static final int DEFAULT_EXPIRATION_NANOS = 0;

@SuppressWarnings("GoodTime") // should be a java.time.Duration
@SuppressWarnings("GoodTime") // should be a Duration
private static final int DEFAULT_REFRESH_NANOS = 0;

static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER =
Expand Down Expand Up @@ -289,13 +291,13 @@ private static final class LoggerHolder {
@CheckForNull Strength keyStrength;
@CheckForNull Strength valueStrength;

@SuppressWarnings("GoodTime") // should be a java.time.Duration
@SuppressWarnings("GoodTime") // should be a Duration
long expireAfterWriteNanos = UNSET_INT;

@SuppressWarnings("GoodTime") // should be a java.time.Duration
@SuppressWarnings("GoodTime") // should be a Duration
long expireAfterAccessNanos = UNSET_INT;

@SuppressWarnings("GoodTime") // should be a java.time.Duration
@SuppressWarnings("GoodTime") // should be a Duration
long refreshNanos = UNSET_INT;

@CheckForNull Equivalence<Object> keyEquivalence;
Expand Down Expand Up @@ -711,12 +713,48 @@ Strength getValueStrength() {
*
* @param duration the length of time after an entry is created that it should be automatically
* removed
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if {@link #expireAfterWrite} was already set
* @throws ArithmeticException for durations greater than +/- approximately 292 years
* @since NEXT (but since 25.0 in the JRE <a
* href="https://github.com/google/guava#guava-google-core-libraries-for-java">flavor</a>)
*/
@J2ObjCIncompatible
@GwtIncompatible // Duration
@SuppressWarnings({
"GoodTime", // Duration decomposition
"Java7ApiChecker",
})
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
@CanIgnoreReturnValue
public CacheBuilder<K, V> expireAfterWrite(Duration duration) {
return expireAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration
* has elapsed after the entry's creation, or the most recent replacement of its value.
*
* <p>When {@code duration} is zero, this method hands off to {@link #maximumSize(long)
* maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be
* useful in testing, or to disable caching temporarily without a code change.
*
* <p>Expired entries may be counted in {@link Cache#size}, but will never be visible to read or
* write operations. Expired entries are cleaned up as part of the routine maintenance described
* in the class javadoc.
*
* <p>If you can represent the duration as a {@link Duration} (which should be preferred when
* feasible), use {@link #expireAfterWrite(Duration)} instead.
*
* @param duration the length of time after an entry is created that it should be automatically
* removed
* @param unit the unit that {@code duration} is expressed in
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if {@link #expireAfterWrite} was already set
*/
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
@SuppressWarnings("GoodTime") // should accept a Duration
@CanIgnoreReturnValue
public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
checkState(
Expand All @@ -733,6 +771,44 @@ long getExpireAfterWriteNanos() {
return (expireAfterWriteNanos == UNSET_INT) ? DEFAULT_EXPIRATION_NANOS : expireAfterWriteNanos;
}

/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration
* has elapsed after the entry's creation, the most recent replacement of its value, or its last
* access. Access time is reset by all cache read and write operations (including {@code
* Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by {@code
* containsKey(Object)}, nor by operations on the collection-views of {@link Cache#asMap}}. So,
* for example, iterating through {@code Cache.asMap().entrySet()} does not reset access time for
* the entries you retrieve.
*
* <p>When {@code duration} is zero, this method hands off to {@link #maximumSize(long)
* maximumSize}{@code (0)}, ignoring any otherwise-specified maximum size or weight. This can be
* useful in testing, or to disable caching temporarily without a code change.
*
* <p>Expired entries may be counted in {@link Cache#size}, but will never be visible to read or
* write operations. Expired entries are cleaned up as part of the routine maintenance described
* in the class javadoc.
*
* @param duration the length of time after an entry is last accessed that it should be
* automatically removed
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if {@link #expireAfterAccess} was already set
* @throws ArithmeticException for durations greater than +/- approximately 292 years
* @since NEXT (but since 25.0 in the JRE <a
* href="https://github.com/google/guava#guava-google-core-libraries-for-java">flavor</a>)
*/
@J2ObjCIncompatible
@GwtIncompatible // Duration
@SuppressWarnings({
"GoodTime", // Duration decomposition
"Java7ApiChecker",
})
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
@CanIgnoreReturnValue
public CacheBuilder<K, V> expireAfterAccess(Duration duration) {
return expireAfterAccess(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration
* has elapsed after the entry's creation, the most recent replacement of its value, or its last
Expand All @@ -750,14 +826,17 @@ long getExpireAfterWriteNanos() {
* write operations. Expired entries are cleaned up as part of the routine maintenance described
* in the class javadoc.
*
* <p>If you can represent the duration as a {@link Duration} (which should be preferred when
* feasible), use {@link #expireAfterAccess(Duration)} instead.
*
* @param duration the length of time after an entry is last accessed that it should be
* automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if {@link #expireAfterAccess} was already set
*/
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
@SuppressWarnings("GoodTime") // should accept a Duration
@CanIgnoreReturnValue
public CacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) {
checkState(
Expand All @@ -776,6 +855,46 @@ long getExpireAfterAccessNanos() {
: expireAfterAccessNanos;
}

/**
* Specifies that active entries are eligible for automatic refresh once a fixed duration has
* elapsed after the entry's creation, or the most recent replacement of its value. The semantics
* of refreshes are specified in {@link LoadingCache#refresh}, and are performed by calling {@link
* CacheLoader#reload}.
*
* <p>As the default implementation of {@link CacheLoader#reload} is synchronous, it is
* recommended that users of this method override {@link CacheLoader#reload} with an asynchronous
* implementation; otherwise refreshes will be performed during unrelated cache read and write
* operations.
*
* <p>Currently automatic refreshes are performed when the first stale request for an entry
* occurs. The request triggering refresh will make a synchronous call to {@link
* CacheLoader#reload}
* to obtain a future of the new value. If the returned future is already complete, it is returned
* immediately. Otherwise, the old value is returned.
*
* <p><b>Note:</b> <i>all exceptions thrown during refresh will be logged and then swallowed</i>.
*
* @param duration the length of time after an entry is created that it should be considered
* stale, and thus eligible for refresh
* @return this {@code CacheBuilder} instance (for chaining)
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if {@link #refreshAfterWrite} was already set
* @throws ArithmeticException for durations greater than +/- approximately 292 years
* @since NEXT (but since 25.0 in the JRE <a
* href="https://github.com/google/guava#guava-google-core-libraries-for-java">flavor</a>)
*/
@J2ObjCIncompatible
@GwtIncompatible // Duration
@SuppressWarnings({
"GoodTime", // Duration decomposition
"Java7ApiChecker",
})
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
@CanIgnoreReturnValue
public CacheBuilder<K, V> refreshAfterWrite(Duration duration) {
return refreshAfterWrite(toNanosSaturated(duration), TimeUnit.NANOSECONDS);
}

/**
* Specifies that active entries are eligible for automatic refresh once a fixed duration has
* elapsed after the entry's creation, or the most recent replacement of its value. The semantics
Expand All @@ -795,6 +914,9 @@ long getExpireAfterAccessNanos() {
*
* <p><b>Note:</b> <i>all exceptions thrown during refresh will be logged and then swallowed</i>.
*
* <p>If you can represent the duration as a {@link Duration} (which should be preferred when
* feasible), use {@link #refreshAfterWrite(Duration)} instead.
*
* @param duration the length of time after an entry is created that it should be considered
* stale, and thus eligible for refresh
* @param unit the unit that {@code duration} is expressed in
Expand All @@ -804,7 +926,7 @@ long getExpireAfterAccessNanos() {
* @since 11.0
*/
@GwtIncompatible // To be supported (synchronously).
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
@SuppressWarnings("GoodTime") // should accept a Duration
@CanIgnoreReturnValue
public CacheBuilder<K, V> refreshAfterWrite(long duration, TimeUnit unit) {
checkNotNull(unit);
Expand Down Expand Up @@ -1002,4 +1124,27 @@ public String toString() {
}
return s.toString();
}

/**
* Returns the number of nanoseconds of the given duration without throwing or overflowing.
*
* <p>Instead of throwing {@link ArithmeticException}, this method silently saturates to either
* {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}. This behavior can be useful when decomposing
* a duration in order to call a legacy API which requires a {@code long, TimeUnit} pair.
*/
@GwtIncompatible // Duration
@SuppressWarnings({
"GoodTime", // Duration decomposition
"Java7ApiChecker",
})
@IgnoreJRERequirement // No more dangerous than wherever the caller got the Duration from
private static long toNanosSaturated(Duration duration) {
// Using a try/catch seems lazy, but the catch block will rarely get invoked (except for
// durations longer than approximately +/- 292 years).
try {
return duration.toNanos();
} catch (ArithmeticException tooBig) {
return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE;
}
}
}
Loading

0 comments on commit a5f9bca

Please sign in to comment.