Skip to content

Instantly share code, notes, and snippets.

@tingstad
Last active May 12, 2020 11:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tingstad/d21326254fd6e7f67d5fafa70b3a2dfc to your computer and use it in GitHub Desktop.
Save tingstad/d21326254fd6e7f67d5fafa70b3a2dfc to your computer and use it in GitHub Desktop.
Cached Supplier - similar to Google Guava's Suppliers.memoizeWithExpiration
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import static java.util.Objects.requireNonNull;
public class CachedSupplier<T> implements Supplier<T> {
private final Supplier<T> delegate;
private final long expiration;
private final Supplier<Long> currentTimeSupplier;
private T value;
private volatile long time;
public CachedSupplier(Supplier<T> delegate, long expiration, TimeUnit timeUnit) {
this(delegate, expiration, timeUnit, System::currentTimeMillis);
}
public CachedSupplier(Supplier<T> delegate, long expiration, TimeUnit timeUnit, Supplier<Long> currentTimeMillisSupplier) {
if (expiration <= 0) throw new IllegalArgumentException("expiration must be > 0");
this.delegate = requireNonNull(delegate);
this.expiration = timeUnit.toMillis(expiration);
this.currentTimeSupplier = requireNonNull(currentTimeMillisSupplier);
this.time = 0;
}
@Override
public T get() {
long currentTime = currentTimeSupplier.get();
boolean expired = currentTime - time > expiration;
if (expired) {
synchronized (this) {
if (time < currentTime) {
value = delegate.get();
time = currentTime;
}
}
}
return value;
}
}
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class CachedSupplierTest {
@Test
void shouldCallDelegate() {
Supplier<Long> delegate = () -> 7L;
long value = new CachedSupplier<>(delegate, 1, TimeUnit.HOURS, () -> Long.MAX_VALUE).get();
assertEquals(7L, value);
}
@Test
void twoRapidCallsMakesOneBackendRequest() {
AtomicInteger i = new AtomicInteger(0);
Supplier<Integer> delegate = i::getAndIncrement;
CachedSupplier cachedSupplier = new CachedSupplier(delegate, 1, TimeUnit.SECONDS, () -> Long.MAX_VALUE);
cachedSupplier.get();
cachedSupplier.get();
assertEquals(1, i.get());
}
@Test
void shouldCallDelegateAfterExpiration() {
Supplier<Integer> delegate = new AtomicInteger(0)::incrementAndGet;
AtomicLong timeInMillis = new AtomicLong(1001);
CachedSupplier cachedSupplier = new CachedSupplier(delegate, 1, TimeUnit.SECONDS, timeInMillis::get);
assertEquals(1, cachedSupplier.get());
assertEquals(1, cachedSupplier.get());
timeInMillis.set(2002);
assertEquals(2, cachedSupplier.get());
assertEquals(2, cachedSupplier.get());
assertEquals(2, cachedSupplier.get());
timeInMillis.set(9000);
assertEquals(3, cachedSupplier.get());
assertEquals(3, cachedSupplier.get());
timeInMillis.set(9999);
assertEquals(3, cachedSupplier.get());
assertEquals(3, cachedSupplier.get());
}
@Test
void zeroOrNegativeExpirationIsNotAllowed() {
Supplier<Long> delegate = () -> 1L;
assertThrows(IllegalArgumentException.class, () -> new CachedSupplier(delegate, 0, TimeUnit.SECONDS),
"Expiration must be positive");
assertThrows(IllegalArgumentException.class, () -> new CachedSupplier(delegate, -10, TimeUnit.SECONDS),
"Expiration must be positive");
assertThrows(NullPointerException.class, () -> new CachedSupplier(null, 1, TimeUnit.SECONDS),
"Supplier argument must not be null");
assertThrows(NullPointerException.class, () -> new CachedSupplier(delegate, 1, null),
"TimeUnit argument must not be null");
assertThrows(NullPointerException.class, () -> new CachedSupplier(delegate, 1, TimeUnit.SECONDS, null),
"TimeSupplier argument must not be null");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment