Implement cache entry expiration for CachedWeatherForecaster.

Co-Authored-By: os222
This commit is contained in:
Gleb Koval 2024-11-19 09:14:59 +00:00
parent 30cd615adb
commit 5f731c1ab2
Signed by: cyclane
GPG Key ID: 15E168A8B332382C
3 changed files with 54 additions and 10 deletions

View File

@ -1,5 +1,6 @@
package ic.doc; package ic.doc;
import java.time.Instant;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.HashMap; import java.util.HashMap;
import java.util.Queue; import java.util.Queue;
@ -8,33 +9,43 @@ import java.util.Queue;
public class CachedWeatherForecaster implements WeatherForecaster { public class CachedWeatherForecaster implements WeatherForecaster {
private final WeatherForecaster forecaster; private final WeatherForecaster forecaster;
private final Integer maxCacheSize; 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<>(); private final Queue<CacheKey> evictionQueue = new ArrayDeque<>();
CachedWeatherForecaster(WeatherForecaster forecaster, Integer maxCacheSize) { CachedWeatherForecaster(WeatherForecaster forecaster, Integer maxCacheSize,
Integer maxCacheAgeSecs) {
this.forecaster = forecaster; this.forecaster = forecaster;
this.maxCacheSize = maxCacheSize; this.maxCacheSize = maxCacheSize;
this.maxCacheAgeSecs = maxCacheAgeSecs;
} }
@Override @Override
public WeatherForecast forecastFor(WeatherRegion region, Weekday day) { public WeatherForecast forecastFor(WeatherRegion region, Weekday day) {
Instant now = Instant.now();
CacheKey key = new CacheKey(region, day); CacheKey key = new CacheKey(region, day);
WeatherForecast forecast = cache.get(key); CacheEntry entry = cache.get(key);
if (forecast == null) { if (entry == null || entry.isExpired(now, maxCacheAgeSecs)) {
forecast = forecaster.forecastFor(region, day); WeatherForecast forecast = forecaster.forecastFor(region, day);
putCache(key, forecast); putCache(key, new CacheEntry(now, forecast));
return 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) { if (maxCacheSize != null && cache.size() >= maxCacheSize) {
cache.remove(evictionQueue.poll()); cache.remove(evictionQueue.poll());
} }
cache.put(key, forecast); cache.put(key, entry);
evictionQueue.add(key); evictionQueue.add(key);
} }
private record CacheKey(WeatherRegion region, Weekday day) {} 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);
}
}
} }

View File

@ -3,6 +3,7 @@ package ic.doc;
public class CachedWeatherForecasterBuilder { public class CachedWeatherForecasterBuilder {
private WeatherForecaster forecaster; private WeatherForecaster forecaster;
private Integer maxCacheSize = null; private Integer maxCacheSize = null;
private Integer maxCacheAgeSecs = null;
private CachedWeatherForecasterBuilder() {} private CachedWeatherForecasterBuilder() {}
@ -20,7 +21,12 @@ public class CachedWeatherForecasterBuilder {
return this; return this;
} }
public CachedWeatherForecasterBuilder withMaxCacheAgeSecs(int maxCacheAgeSecs) {
this.maxCacheAgeSecs = maxCacheAgeSecs;
return this;
}
public CachedWeatherForecaster build() { public CachedWeatherForecaster build() {
return new CachedWeatherForecaster(forecaster, maxCacheSize); return new CachedWeatherForecaster(forecaster, maxCacheSize, maxCacheAgeSecs);
} }
} }

View File

@ -85,4 +85,31 @@ public class CachedWeatherForecasterTest {
actForecast = cachedForecaster.forecastFor(WeatherRegion.LONDON, Weekday.MONDAY); actForecast = cachedForecaster.forecastFor(WeatherRegion.LONDON, Weekday.MONDAY);
assertEquals(expForecast, actForecast); 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);
}
} }