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;
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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}