Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add implementation for Suppliers#memoizeWithRefresh #3777

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions guava-tests/test/com/google/common/base/SuppliersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ public String toString() {
}
}

static class CountingDelayedSupplier extends CountingSupplier {
private final long delayTime;

CountingDelayedSupplier(long delayTime) {
this.delayTime = delayTime;
}

@Override
public Integer get() {
try {
Thread.sleep(delayTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
calls++;
return calls * 10;
}

@Override
public String toString() {
return "CountingDelayedSupplier";
}
}

static class ThrowingSupplier implements Supplier<Integer> {
@Override
public Integer get() {
Expand Down Expand Up @@ -223,6 +247,67 @@ public void testMemoizeWithExpiration() throws InterruptedException {
checkExpiration(countingSupplier, memoizedSupplier);
}

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

int refreshInterval = 8;
Supplier<Integer> memoizedSupplier =
Suppliers.memoizeWithRefresh(countingSupplier, refreshInterval, TimeUnit.MILLISECONDS);

// the underlying supplier executes when the supplier is initialised
assertEquals(1, countingSupplier.calls);

assertEquals(10, (int) memoizedSupplier.get());
// now it has
assertEquals(1, countingSupplier.calls);

assertEquals(10, (int) memoizedSupplier.get());
// it still should only have executed once due to memoization
assertEquals(1, countingSupplier.calls);

// wait for enough time to make the supplier request a refresh
Thread.sleep(refreshInterval + 1);

// request the value to kick off the async refresh task
// this could return 10 or 20 depending on speed of the async task
memoizedSupplier.get();

// ensure the task has enough time to run
Thread.sleep(1);
assertEquals(2, countingSupplier.calls);
assertEquals(20, (int) memoizedSupplier.get());
// it still should only have executed twice due to memoization
assertEquals(2, countingSupplier.calls);
}

@GwtIncompatible // Thread.sleep
public void testMemoizeWithRefreshAsync() throws InterruptedException {
int processingDelay = 2;
int refreshInterval = 8;

CountingDelayedSupplier countingDelayedSupplier = new CountingDelayedSupplier(processingDelay);
Supplier<Integer> memoizedSupplier =
Suppliers.memoizeWithRefresh(countingDelayedSupplier, refreshInterval, TimeUnit.MILLISECONDS);

// the underlying supplier executes when the supplier is initialised
assertEquals(1, countingDelayedSupplier.calls);

assertEquals(10, (int) memoizedSupplier.get());

// wait for enough time to make the supplier request a refresh
Thread.sleep(refreshInterval + 1);

// request the value to kick off the async refresh task
// since the supplier has a processing time we can verify the old value is still returned
assertEquals(10, (int) memoizedSupplier.get());

// ensure the task has enough time to run
Thread.sleep(processingDelay + 1);
assertEquals(2, countingDelayedSupplier.calls);
assertEquals(20, (int) memoizedSupplier.get());
}

@GwtIncompatible // Thread.sleep, SerializationTester
public void testMemoizeWithExpirationSerialized() throws InterruptedException {
SerializableCountingSupplier countingSupplier = new SerializableCountingSupplier();
Expand Down
59 changes: 59 additions & 0 deletions guava/src/com/google/common/base/Suppliers.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.VisibleForTesting;
import java.io.Serializable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.checkerframework.checker.nullness.qual.Nullable;

/**
Expand Down Expand Up @@ -263,6 +268,60 @@ public String toString() {
private static final long serialVersionUID = 0;
}

@SuppressWarnings("GoodTime") // should accept a java.time.Duration
public static <T> Supplier<T> memoizeWithRefresh(
Supplier<T> delegate, long duration, TimeUnit unit) {
return new RefreshingMemoizingSupplier<T>(delegate, duration, unit);
}

@VisibleForTesting
static class RefreshingMemoizingSupplier<T> implements Supplier<T>, Serializable {
final Supplier<T> delegate;
final long durationNanos;
transient volatile @Nullable T value;
final AtomicLong expirationNanos;
final Lock lock = new ReentrantLock();

RefreshingMemoizingSupplier(Supplier<T> delegate, long duration, TimeUnit unit) {
this.delegate = checkNotNull(delegate);
this.durationNanos = unit.toNanos(duration);
this.expirationNanos = new AtomicLong(Platform.systemNanoTime() + durationNanos);
// initialise the value on creation
this.value = delegate.get();
checkArgument(duration > 0, "duration (%s %s) must be > 0", duration, unit);
}

@Override
public T get() {
long now = Platform.systemNanoTime();
if (now >= expirationNanos.get()
&& lock.tryLock()) {
CompletableFuture.supplyAsync(delegate)
.exceptionally(ex -> {
lock.unlock();
return value;
})
.thenAcceptAsync(d -> {
expirationNanos.set(Platform.systemNanoTime() + durationNanos);
if (java.util.Objects.equals(value, d)) {
lock.unlock();
return;
}
value = d;
lock.unlock();
});
}
return value;
}

@Override
public String toString() {
return "Suppliers.memoizeWithRefresh(" + delegate + ", " + durationNanos + ", NANOS)";
}

private static final long serialVersionUID = 0;
}

/** Returns a supplier that always supplies {@code instance}. */
public static <T> Supplier<T> ofInstance(@Nullable T instance) {
return new SupplierOfInstance<T>(instance);
Expand Down