SeasonalCacheHook.java
package io.github.baokhang83.mnemo.warden.examples.cache;
import io.github.baokhang83.mnemo.cache.SeasonalCache;
import io.github.baokhang83.mnemo.cache.SeasonalCacheState;
import io.github.baokhang83.mnemo.warden.cache.CacheHook;
import io.github.baokhang83.mnemo.warden.cache.CacheStats;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Supplier;
/**
* A working {@link CacheHook} for the real <a href="https://github.com/baokhang83/mnemo-cache">
* mnemo-cache</a> library (W-504) — not a fake, and no changes to mnemo-cache itself: it's
* built only against {@link SeasonalCache}'s existing public surface.
*
* <p>mnemo-cache exposes no per-key hotness signal (Caffeine's W-TinyLFU owns that judgment
* internally) and no {@code invalidateAll()}, so this adapter treats the whole cache it wraps as
* "evictable" — matching {@link CacheHook#flushEvictable()}'s own contract: whatever
* <em>this cache</em> considers safely evictable, not a per-key split within it. An app that
* wants a true hot working set simply keeps that state in a cache it never registers a {@link
* CacheHook} for — see {@code QueryCacheApp}'s {@code sessionCache}.
*
* <p>Every key ever inserted is tracked locally, since {@link SeasonalCache} has no way to
* enumerate or clear its own contents; {@link #flushEvictable()} invalidates each tracked key in
* turn. Hit/miss counts are tracked here too, purely for {@link #stats()} —
* {@link SeasonalCacheState} doesn't carry a hit rate.
*/
public final class SeasonalCacheHook implements CacheHook {
private final SeasonalCache<String, String> cache;
private final Map<String, Supplier<String>> warmSet;
private final Set<String> trackedKeys = ConcurrentHashMap.newKeySet();
private final LongAdder hits = new LongAdder();
private final LongAdder misses = new LongAdder();
public SeasonalCacheHook(SeasonalCache<String, String> cache, Map<String, Supplier<String>> warmSet) {
this.cache = cache;
this.warmSet = Map.copyOf(warmSet);
}
/** Get-or-load, exactly as an app would call it day-to-day. */
public String get(String key, Supplier<String> loader) {
String value = cache.get(key, k -> loader.get());
trackedKeys.add(key);
return value;
}
public String getIfPresent(String key) {
String value = cache.getIfPresent(key);
if (value != null) {
hits.increment();
} else {
misses.increment();
}
return value;
}
public void put(String key, String value) {
cache.put(key, value);
trackedKeys.add(key);
}
/** Invalidates every key this adapter has ever inserted — the whole cache is evictable. */
@Override
public void flushEvictable() {
for (String key : trackedKeys) {
cache.invalidate(key);
}
trackedKeys.clear();
}
/** Re-loads the fixed warm set an app owner configured this adapter with. */
@Override
public void preWarm() {
warmSet.forEach(
(key, loader) -> {
cache.get(key, k -> loader.get());
trackedKeys.add(key);
});
}
@Override
public CacheStats stats() {
SeasonalCacheState state = cache.state();
long total = hits.sum() + misses.sum();
double hitRate = total == 0 ? 0.0 : (double) hits.sum() / total;
return new CacheStats(state.size(), state.size(), hitRate);
}
}