diff --git a/src/main/java/ic/doc/CachedWeatherForecaster.java b/src/main/java/ic/doc/CachedWeatherForecaster.java index 83d9a70..817362b 100644 --- a/src/main/java/ic/doc/CachedWeatherForecaster.java +++ b/src/main/java/ic/doc/CachedWeatherForecaster.java @@ -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 cache = new HashMap<>(); + private final Integer maxCacheAgeSecs; + private final HashMap cache = new HashMap<>(); private final Queue 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); + } + } } \ No newline at end of file diff --git a/src/main/java/ic/doc/CachedWeatherForecasterBuilder.java b/src/main/java/ic/doc/CachedWeatherForecasterBuilder.java index be12711..1bb314e 100644 --- a/src/main/java/ic/doc/CachedWeatherForecasterBuilder.java +++ b/src/main/java/ic/doc/CachedWeatherForecasterBuilder.java @@ -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); } } diff --git a/src/test/java/ic/doc/CachedWeatherForecasterTest.java b/src/test/java/ic/doc/CachedWeatherForecasterTest.java index 9a7cbdc..9ea245e 100644 --- a/src/test/java/ic/doc/CachedWeatherForecasterTest.java +++ b/src/test/java/ic/doc/CachedWeatherForecasterTest.java @@ -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); + } } \ No newline at end of file