Implement cache entry expiration for CachedWeatherForecaster.
Co-Authored-By: os222
This commit is contained in:
parent
30cd615adb
commit
5f731c1ab2
|
@ -1,5 +1,6 @@
|
|||
package ic.doc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.Queue;
|
||||
|
@ -8,33 +9,43 @@ import java.util.Queue;
|
|||
public class CachedWeatherForecaster implements WeatherForecaster {
|
||||
private final WeatherForecaster forecaster;
|
||||
private final Integer maxCacheSize;
|
||||
private final HashMap<CacheKey, WeatherForecast> cache = new HashMap<>();
|
||||
private final Integer maxCacheAgeSecs;
|
||||
private final HashMap<CacheKey, CacheEntry> cache = new HashMap<>();
|
||||
private final Queue<CacheKey> evictionQueue = new ArrayDeque<>();
|
||||
|
||||
CachedWeatherForecaster(WeatherForecaster forecaster, Integer maxCacheSize) {
|
||||
CachedWeatherForecaster(WeatherForecaster forecaster, Integer maxCacheSize,
|
||||
Integer maxCacheAgeSecs) {
|
||||
this.forecaster = forecaster;
|
||||
this.maxCacheSize = maxCacheSize;
|
||||
this.maxCacheAgeSecs = maxCacheAgeSecs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeatherForecast forecastFor(WeatherRegion region, Weekday day) {
|
||||
Instant now = Instant.now();
|
||||
CacheKey key = new CacheKey(region, day);
|
||||
WeatherForecast forecast = cache.get(key);
|
||||
if (forecast == null) {
|
||||
forecast = forecaster.forecastFor(region, day);
|
||||
putCache(key, forecast);
|
||||
CacheEntry entry = cache.get(key);
|
||||
if (entry == null || entry.isExpired(now, maxCacheAgeSecs)) {
|
||||
WeatherForecast forecast = forecaster.forecastFor(region, day);
|
||||
putCache(key, new CacheEntry(now, forecast));
|
||||
return forecast;
|
||||
}
|
||||
return forecast;
|
||||
return entry.forecast;
|
||||
}
|
||||
|
||||
private void putCache(CacheKey key, WeatherForecast forecast) {
|
||||
private void putCache(CacheKey key, CacheEntry entry) {
|
||||
if (maxCacheSize != null && cache.size() >= maxCacheSize) {
|
||||
cache.remove(evictionQueue.poll());
|
||||
}
|
||||
cache.put(key, forecast);
|
||||
cache.put(key, entry);
|
||||
evictionQueue.add(key);
|
||||
}
|
||||
|
||||
private record CacheKey(WeatherRegion region, Weekday day) {}
|
||||
|
||||
private record CacheEntry(Instant timestamp, WeatherForecast forecast) {
|
||||
boolean isExpired(Instant now, Integer maxCacheAgeSecs) {
|
||||
return maxCacheAgeSecs != null && timestamp.plusSeconds(maxCacheAgeSecs).isBefore(now);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package ic.doc;
|
|||
public class CachedWeatherForecasterBuilder {
|
||||
private WeatherForecaster forecaster;
|
||||
private Integer maxCacheSize = null;
|
||||
private Integer maxCacheAgeSecs = null;
|
||||
|
||||
private CachedWeatherForecasterBuilder() {}
|
||||
|
||||
|
@ -20,7 +21,12 @@ public class CachedWeatherForecasterBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public CachedWeatherForecasterBuilder withMaxCacheAgeSecs(int maxCacheAgeSecs) {
|
||||
this.maxCacheAgeSecs = maxCacheAgeSecs;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CachedWeatherForecaster build() {
|
||||
return new CachedWeatherForecaster(forecaster, maxCacheSize);
|
||||
return new CachedWeatherForecaster(forecaster, maxCacheSize, maxCacheAgeSecs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,4 +85,31 @@ public class CachedWeatherForecasterTest {
|
|||
actForecast = cachedForecaster.forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
|
||||
assertEquals(expForecast, actForecast);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forecastForCacheExpires() {
|
||||
WeatherForecaster cachedForecaster = cachedWeatherForecaster.withMaxCacheAgeSecs(1).build();
|
||||
WeatherForecast forecast1 = new WeatherForecast("Sunny", 20);
|
||||
WeatherForecast forecast2 = new WeatherForecast("Rainy", 10);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(forecaster).forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
|
||||
will(returnValue(forecast1));
|
||||
oneOf(forecaster).forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
|
||||
will(returnValue(forecast2));
|
||||
}});
|
||||
// Cache put and immediate hit
|
||||
cachedForecaster.forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
|
||||
WeatherForecast actForecast =
|
||||
cachedForecaster.forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
|
||||
assertEquals(forecast1, actForecast);
|
||||
// Cache expires, miss, then hit
|
||||
try {
|
||||
Thread.sleep(1100);
|
||||
} catch (InterruptedException e) {
|
||||
fail("Interrupted while sleeping: " + e);
|
||||
}
|
||||
cachedForecaster.forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
|
||||
actForecast = cachedForecaster.forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
|
||||
assertEquals(forecast2, actForecast);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue