Skip to content

Commit

Permalink
Add Duration overload for Suppliers.memoizeWithExpiration.
Browse files Browse the repository at this point in the history
This CL contains part but not all of #3691

Fixes #3169

We'd considered this previously in cl/197933201 but gotten stuck on the question of whether to accept and/or return the `java.util.function` type. My claim is that we want an overload that is near-identical to the old one for ease of migration. Additionally, if we want to support `java.util.function.Supplier` in the future, then we're going to have to add an overload of the existing `memoize` method, and which point it would probably look _more_ weird if we had 2 overloads for `memoize` (and maybe `synchronizedSupplier`) but only a single `memoizeWithExpiration` method.

RELNOTES=`base`: Added a `Duration` overload for `Suppliers.memoizeWithExpiration`.
PiperOrigin-RevId: 597280125
  • Loading branch information
mfboulos authored and Google Java Core Libraries committed Jan 10, 2024
1 parent ca0ad2a commit 76e46ec
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 24 deletions.
59 changes: 56 additions & 3 deletions android/guava-tests/test/com/google/common/base/SuppliersTest.java
Expand Up @@ -27,6 +27,7 @@
import com.google.common.testing.ClassSanityTester;
import com.google.common.testing.EqualsTester;
import java.io.Serializable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -216,7 +217,7 @@ public List<Integer> apply(List<Integer> list) {

@J2ktIncompatible
@GwtIncompatible // Thread.sleep
public void testMemoizeWithExpiration() throws InterruptedException {
public void testMemoizeWithExpiration_longTimeUnit() throws InterruptedException {
CountingSupplier countingSupplier = new CountingSupplier();

Supplier<Integer> memoizedSupplier =
Expand All @@ -225,6 +226,50 @@ public void testMemoizeWithExpiration() throws InterruptedException {
checkExpiration(countingSupplier, memoizedSupplier);
}

@J2ktIncompatible
@GwtIncompatible // Thread.sleep
@SuppressWarnings("Java7ApiChecker") // test of Java 8+ API
public void testMemoizeWithExpiration_duration() throws InterruptedException {
CountingSupplier countingSupplier = new CountingSupplier();

Supplier<Integer> memoizedSupplier =
Suppliers.memoizeWithExpiration(countingSupplier, Duration.ofMillis(75));

checkExpiration(countingSupplier, memoizedSupplier);
}

public void testMemoizeWithExpiration_longTimeUnitNegative() throws InterruptedException {
try {
Supplier<String> unused = Suppliers.memoizeWithExpiration(() -> "", 0, TimeUnit.MILLISECONDS);
fail();
} catch (IllegalArgumentException expected) {
}

try {
Supplier<String> unused =
Suppliers.memoizeWithExpiration(() -> "", -1, TimeUnit.MILLISECONDS);
fail();
} catch (IllegalArgumentException expected) {
}
}

@SuppressWarnings("Java7ApiChecker") // test of Java 8+ API
@J2ktIncompatible // Duration
@GwtIncompatible // Duration
public void testMemoizeWithExpiration_durationNegative() throws InterruptedException {
try {
Supplier<String> unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ZERO);
fail();
} catch (IllegalArgumentException expected) {
}

try {
Supplier<String> unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ofMillis(-1));
fail();
} catch (IllegalArgumentException expected) {
}
}

@J2ktIncompatible
@GwtIncompatible // Thread.sleep, SerializationTester
public void testMemoizeWithExpirationSerialized() throws InterruptedException {
Expand Down Expand Up @@ -451,15 +496,23 @@ public void testSerialization() {

@J2ktIncompatible
@GwtIncompatible // reflection
@SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API
public void testSuppliersNullChecks() throws Exception {
new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testNulls();
new ClassSanityTester()
.setDefault(Duration.class, Duration.ofSeconds(1))
.forAllPublicStaticMethods(Suppliers.class)
.testNulls();
}

@J2ktIncompatible
@GwtIncompatible // reflection
@AndroidIncompatible // TODO(cpovirk): ClassNotFoundException: com.google.common.base.Function
@SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API
public void testSuppliersSerializable() throws Exception {
new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testSerializable();
new ClassSanityTester()
.setDefault(Duration.class, Duration.ofSeconds(1))
.forAllPublicStaticMethods(Suppliers.class)
.testSerializable();
}

public void testOfInstance_equals() {
Expand Down
30 changes: 30 additions & 0 deletions android/guava/src/com/google/common/base/IgnoreJRERequirement.java
@@ -0,0 +1,30 @@
/*
* Copyright 2019 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

package com.google.common.base;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Target;

/**
* Disables Animal Sniffer's checking of compatibility with older versions of Java/Android.
*
* <p>Each package's copy of this annotation needs to be listed in our {@code pom.xml}.
*/
@Target({METHOD, CONSTRUCTOR, TYPE})
@ElementTypesAreNonnullByDefault
@interface IgnoreJRERequirement {}
53 changes: 53 additions & 0 deletions android/guava/src/com/google/common/base/Internal.java
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2019 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

package com.google.common.base;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import java.time.Duration;

/** This class is for {@code com.google.common.base} use only! */
@J2ktIncompatible
@GwtIncompatible // java.time.Duration
@ElementTypesAreNonnullByDefault
final class Internal {

/**
* 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.
*/
@SuppressWarnings({
// We use this method only for cases in which we need to decompose to primitives.
"GoodTime-ApiWithNumericTimeUnit",
"GoodTime-DecomposeToPrimitive",
// We use this method only from within APIs that require a Duration.
"Java7ApiChecker",
})
@IgnoreJRERequirement
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;
}
}

private Internal() {}
}
53 changes: 45 additions & 8 deletions android/guava/src/com/google/common/base/Suppliers.java
Expand Up @@ -14,13 +14,18 @@

package com.google.common.base;

import static com.google.common.base.Internal.toNanosSaturated;
import static com.google.common.base.NullnessCasts.uncheckedCastNullableTToT;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import com.google.common.annotations.VisibleForTesting;
import java.io.Serializable;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.Nullable;
Expand All @@ -34,7 +39,7 @@
* @author Harry Heymann
* @since 2.0
*/
@GwtCompatible
@GwtCompatible(emulated = true)
@ElementTypesAreNonnullByDefault
public final class Suppliers {
private Suppliers() {}
Expand Down Expand Up @@ -221,10 +226,44 @@ public String toString() {
* @throws IllegalArgumentException if {@code duration} is not positive
* @since 2.0
*/
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
@SuppressWarnings("GoodTime") // Prefer the Duration overload
public static <T extends @Nullable Object> Supplier<T> memoizeWithExpiration(
Supplier<T> delegate, long duration, TimeUnit unit) {
return new ExpiringMemoizingSupplier<>(delegate, duration, unit);
checkNotNull(delegate);
checkArgument(duration > 0, "duration (%s %s) must be > 0", duration, unit);
return new ExpiringMemoizingSupplier<>(delegate, unit.toNanos(duration));
}

/**
* Returns a supplier that caches the instance supplied by the delegate and removes the cached
* value after the specified time has passed. Subsequent calls to {@code get()} return the cached
* value if the expiration time has not passed. After the expiration time, a new value is
* retrieved, cached, and returned. See: <a
* href="http://en.wikipedia.org/wiki/Memoization">memoization</a>
*
* <p>The returned supplier is thread-safe. The supplier's serialized form does not contain the
* cached value, which will be recalculated when {@code get()} is called on the reserialized
* instance. The actual memoization does not happen when the underlying delegate throws an
* exception.
*
* <p>When the underlying delegate throws an exception then this memoizing supplier will keep
* delegating calls until it returns valid data.
*
* @param duration the length of time after a value is created that it should stop being returned
* by subsequent {@code get()} calls
* @throws IllegalArgumentException if {@code duration} is not positive
* @since NEXT
*/
@Beta // only until we're confident that Java 8 APIs are safe for our Android users
@J2ktIncompatible
@GwtIncompatible // java.time.Duration
@SuppressWarnings("Java7ApiChecker") // no more dangerous that wherever the user got the Duration
@IgnoreJRERequirement
public static <T extends @Nullable Object> Supplier<T> memoizeWithExpiration(
Supplier<T> delegate, Duration duration) {
checkNotNull(delegate);
checkArgument(duration.compareTo(Duration.ZERO) > 0, "duration (%s) must be > 0", duration);
return new ExpiringMemoizingSupplier<T>(delegate, toNanosSaturated(duration));
}

@VisibleForTesting
Expand All @@ -237,15 +276,13 @@ static class ExpiringMemoizingSupplier<T extends @Nullable Object>
// The special value 0 means "not yet initialized".
transient volatile long expirationNanos;

ExpiringMemoizingSupplier(Supplier<T> delegate, long duration, TimeUnit unit) {
this.delegate = checkNotNull(delegate);
this.durationNanos = unit.toNanos(duration);
checkArgument(duration > 0, "duration (%s %s) must be > 0", duration, unit);
ExpiringMemoizingSupplier(Supplier<T> delegate, long durationNanos) {
this.delegate = delegate;
this.durationNanos = durationNanos;
}

@Override
@ParametricNullness
@SuppressWarnings("GoodTime") // reading system time without TimeSource
public T get() {
// Another variant of Double Checked Locking.
//
Expand Down
2 changes: 1 addition & 1 deletion android/pom.xml
Expand Up @@ -177,7 +177,7 @@
<artifactId>animal-sniffer-maven-plugin</artifactId>
<version>1.23</version>
<configuration>
<annotations>com.google.common.collect.IgnoreJRERequirement,com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement</annotations>
<annotations>com.google.common.base.IgnoreJRERequirement,com.google.common.collect.IgnoreJRERequirement,com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement</annotations>
<checkTestClasses>true</checkTestClasses>
<signature>
<groupId>com.toasttab.android</groupId>
Expand Down
59 changes: 56 additions & 3 deletions guava-tests/test/com/google/common/base/SuppliersTest.java
Expand Up @@ -27,6 +27,7 @@
import com.google.common.testing.ClassSanityTester;
import com.google.common.testing.EqualsTester;
import java.io.Serializable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -216,7 +217,7 @@ public List<Integer> apply(List<Integer> list) {

@J2ktIncompatible
@GwtIncompatible // Thread.sleep
public void testMemoizeWithExpiration() throws InterruptedException {
public void testMemoizeWithExpiration_longTimeUnit() throws InterruptedException {
CountingSupplier countingSupplier = new CountingSupplier();

Supplier<Integer> memoizedSupplier =
Expand All @@ -225,6 +226,50 @@ public void testMemoizeWithExpiration() throws InterruptedException {
checkExpiration(countingSupplier, memoizedSupplier);
}

@J2ktIncompatible
@GwtIncompatible // Thread.sleep
@SuppressWarnings("Java7ApiChecker") // test of Java 8+ API
public void testMemoizeWithExpiration_duration() throws InterruptedException {
CountingSupplier countingSupplier = new CountingSupplier();

Supplier<Integer> memoizedSupplier =
Suppliers.memoizeWithExpiration(countingSupplier, Duration.ofMillis(75));

checkExpiration(countingSupplier, memoizedSupplier);
}

public void testMemoizeWithExpiration_longTimeUnitNegative() throws InterruptedException {
try {
Supplier<String> unused = Suppliers.memoizeWithExpiration(() -> "", 0, TimeUnit.MILLISECONDS);
fail();
} catch (IllegalArgumentException expected) {
}

try {
Supplier<String> unused =
Suppliers.memoizeWithExpiration(() -> "", -1, TimeUnit.MILLISECONDS);
fail();
} catch (IllegalArgumentException expected) {
}
}

@SuppressWarnings("Java7ApiChecker") // test of Java 8+ API
@J2ktIncompatible // Duration
@GwtIncompatible // Duration
public void testMemoizeWithExpiration_durationNegative() throws InterruptedException {
try {
Supplier<String> unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ZERO);
fail();
} catch (IllegalArgumentException expected) {
}

try {
Supplier<String> unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ofMillis(-1));
fail();
} catch (IllegalArgumentException expected) {
}
}

@J2ktIncompatible
@GwtIncompatible // Thread.sleep, SerializationTester
public void testMemoizeWithExpirationSerialized() throws InterruptedException {
Expand Down Expand Up @@ -451,15 +496,23 @@ public void testSerialization() {

@J2ktIncompatible
@GwtIncompatible // reflection
@SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API
public void testSuppliersNullChecks() throws Exception {
new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testNulls();
new ClassSanityTester()
.setDefault(Duration.class, Duration.ofSeconds(1))
.forAllPublicStaticMethods(Suppliers.class)
.testNulls();
}

@J2ktIncompatible
@GwtIncompatible // reflection
@AndroidIncompatible // TODO(cpovirk): ClassNotFoundException: com.google.common.base.Function
@SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API
public void testSuppliersSerializable() throws Exception {
new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testSerializable();
new ClassSanityTester()
.setDefault(Duration.class, Duration.ofSeconds(1))
.forAllPublicStaticMethods(Suppliers.class)
.testSerializable();
}

public void testOfInstance_equals() {
Expand Down

0 comments on commit 76e46ec

Please sign in to comment.