Skip to content

Commit 3b18922

Browse files
committed
Add pluggable eviction strategy defaulting to periodic eviction
1 parent dfe5a7a commit 3b18922

2 files changed

Lines changed: 157 additions & 8 deletions

File tree

src/main/java/com/thealgorithms/datastructures/caches/RRCache.java

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
*
3131
* @param <K> the type of keys maintained by this cache
3232
* @param <V> the type of mapped values
33-
*
3433
* See <a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Random_replacement_(RR)">Random Replacement</a>
3534
* @author Kevin Babu (<a href="https://www.github.com/KevinMwita7">GitHub</a>)
3635
*/
@@ -45,8 +44,8 @@ public final class RRCache<K, V> {
4544

4645
private long hits = 0;
4746
private long misses = 0;
48-
4947
private final BiConsumer<K, V> evictionListener;
48+
private final EvictionStrategy<K, V> evictionStrategy;
5049

5150
/**
5251
* Internal structure to store value + expiry timestamp.
@@ -95,6 +94,7 @@ private RRCache(Builder<K, V> builder) {
9594
this.random = builder.random != null ? builder.random : new Random();
9695
this.lock = new ReentrantLock();
9796
this.evictionListener = builder.evictionListener;
97+
this.evictionStrategy = builder.evictionStrategy;
9898
}
9999

100100
/**
@@ -116,6 +116,8 @@ public V get(K key) {
116116

117117
lock.lock();
118118
try {
119+
evictionStrategy.onAccess(this);
120+
119121
CacheEntry<V> entry = cache.get(key);
120122
if (entry == null || entry.isExpired()) {
121123
if (entry != null) {
@@ -196,17 +198,22 @@ public void put(K key, V value, long ttlMillis) {
196198
* entry for expiration. Expired entries are removed from both the key tracking list
197199
* and the cache map. For each eviction, the eviction listener is notified.
198200
*/
199-
private void evictExpired() {
201+
private int evictExpired() {
200202
Iterator<K> it = keys.iterator();
203+
int expiredCount = 0;
204+
201205
while (it.hasNext()) {
202206
K k = it.next();
203207
CacheEntry<V> entry = cache.get(k);
204208
if (entry != null && entry.isExpired()) {
205209
it.remove();
206210
cache.remove(k);
211+
++expiredCount;
207212
notifyEviction(k, entry.value);
208213
}
209214
}
215+
216+
return expiredCount;
210217
}
211218

212219
/**
@@ -277,6 +284,13 @@ public long getMisses() {
277284
public int size() {
278285
lock.lock();
279286
try {
287+
int cachedSize = cache.size();
288+
int evictedCount = evictionStrategy.onAccess(this);
289+
if (evictedCount > 0) {
290+
return cachedSize - evictedCount;
291+
}
292+
293+
// This runs if periodic eviction does not occur
280294
int count = 0;
281295
for (Map.Entry<K, CacheEntry<V>> entry : cache.entrySet()) {
282296
if (!entry.getValue().isExpired()) {
@@ -315,11 +329,78 @@ public String toString() {
315329
}
316330

317331
/**
318-
* A builder for creating instances of {@link RRCache} with custom configuration.
332+
* A strategy interface for controlling when expired entries are evicted from the cache.
319333
*
320-
* <p>This static inner class allows you to configure parameters such as cache capacity,
321-
* default TTL (time-to-live), random eviction behavior, and an optional eviction listener.
322-
* Once configured, use {@link #build()} to create the {@code RRCache} instance.
334+
* <p>Implementations decide whether and when to trigger {@link RRCache#evictExpired()} based
335+
* on cache usage patterns. This allows for flexible eviction behaviour such as periodic cleanup,
336+
* or no automatic cleanup.
337+
*
338+
* @param <K> the type of keys maintained by the cache
339+
* @param <V> the type of cached values
340+
*/
341+
public interface EvictionStrategy<K, V> {
342+
/**
343+
* Called on each cache access (e.g., {@link RRCache#get(Object)}) to optionally trigger eviction.
344+
*
345+
* @param cache the cache instance on which this strategy is applied
346+
* @return the number of expired entries evicted during this access
347+
*/
348+
int onAccess(RRCache<K, V> cache);
349+
}
350+
351+
/**
352+
* An eviction strategy that performs eviction of expired entries on each call.
353+
*
354+
* @param <K> the type of keys
355+
* @param <V> the type of values
356+
*/
357+
public static class NoEvictionStrategy<K, V> implements EvictionStrategy<K, V> {
358+
@Override public int onAccess(RRCache<K, V> cache) {
359+
return cache.evictExpired();
360+
}
361+
}
362+
363+
/**
364+
* An eviction strategy that triggers eviction every fixed number of accesses.
365+
*
366+
* <p>This deterministic strategy ensures cleanup occurs at predictable intervals,
367+
* ideal for moderately active caches where memory usage is a concern.
368+
*
369+
* @param <K> the type of keys
370+
* @param <V> the type of values
371+
*/
372+
public static class PeriodicEvictionStrategy<K, V> implements EvictionStrategy<K, V> {
373+
private final int interval;
374+
private int counter = 0;
375+
376+
/**
377+
* Constructs a periodic eviction strategy.
378+
*
379+
* @param interval the number of accesses between evictions; must be > 0
380+
* @throws IllegalArgumentException if {@code interval} is less than or equal to 0
381+
*/
382+
public PeriodicEvictionStrategy(int interval) {
383+
if (interval <= 0) {
384+
throw new IllegalArgumentException("Interval must be > 0");
385+
}
386+
this.interval = interval;
387+
}
388+
389+
@Override
390+
public int onAccess(RRCache<K, V> cache) {
391+
if (++counter % interval == 0) {
392+
return cache.evictExpired();
393+
}
394+
395+
return 0;
396+
}
397+
}
398+
399+
/**
400+
* A builder for constructing an {@link RRCache} instance with customizable settings.
401+
*
402+
* <p>Allows configuring capacity, default TTL, random eviction behavior, eviction listener,
403+
* and a pluggable eviction strategy. Call {@link #build()} to create the configured cache instance.
323404
*
324405
* @param <K> the type of keys maintained by the cache
325406
* @param <V> the type of values stored in the cache
@@ -329,7 +410,7 @@ public static class Builder<K, V> {
329410
private long defaultTTL = 0;
330411
private Random random;
331412
private BiConsumer<K, V> evictionListener;
332-
413+
private EvictionStrategy<K, V> evictionStrategy = new RRCache.PeriodicEvictionStrategy<>(100);
333414
/**
334415
* Creates a new {@code Builder} with the specified cache capacity.
335416
*
@@ -396,5 +477,20 @@ public Builder<K, V> evictionListener(BiConsumer<K, V> listener) {
396477
public RRCache<K, V> build() {
397478
return new RRCache<>(this);
398479
}
480+
481+
/**
482+
* Sets the eviction strategy used to determine when to clean up expired entries.
483+
*
484+
* @param strategy an {@link EvictionStrategy} implementation; must not be {@code null}
485+
* @return this builder instance
486+
* @throws IllegalArgumentException if {@code strategy} is {@code null}
487+
*/
488+
public Builder<K, V> evictionStrategy(EvictionStrategy<K, V> strategy) {
489+
if (strategy == null) {
490+
throw new IllegalArgumentException("Eviction strategy must not be null");
491+
}
492+
this.evictionStrategy = strategy;
493+
return this;
494+
}
399495
}
400496
}

src/test/java/com/thealgorithms/datastructures/caches/RRCacheTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,57 @@ void testTtlZeroThrowsIllegalArgumentException() {
155155
Executable exec = () -> new RRCache.Builder<String, String>(3).defaultTTL(-1).build();
156156
Assertions.assertThrows(IllegalArgumentException.class, exec);
157157
}
158+
159+
@Test
160+
void testPeriodicEvictionStrategyEvictsAtInterval() throws InterruptedException {
161+
RRCache<String, String> periodicCache = new RRCache.Builder<String, String>(10)
162+
.defaultTTL(50)
163+
.evictionStrategy(new RRCache.PeriodicEvictionStrategy<>(3))
164+
.build();
165+
166+
periodicCache.put("x", "1");
167+
int ev1 = periodicCache.size();
168+
int ev2 = periodicCache.size();
169+
Thread.sleep(50);
170+
int ev3 = periodicCache.size();
171+
172+
Assertions.assertEquals(1, ev1);
173+
Assertions.assertEquals(1, ev2);
174+
Assertions.assertEquals(0, ev3, "Eviction should happen on the 3rd access");
175+
Assertions.assertEquals(0, cache.size());
176+
}
177+
178+
@Test
179+
void testPeriodicEvictionStrategyThrowsExceptionIfIntervalLessThanOrEqual0() {
180+
Executable executable = () -> new RRCache.Builder<String, String>(10)
181+
.defaultTTL(50)
182+
.evictionStrategy(new RRCache.PeriodicEvictionStrategy<>(0))
183+
.build();
184+
185+
Assertions.assertThrows(IllegalArgumentException.class, executable);
186+
}
187+
188+
@Test
189+
void testNoEvictionStrategyEvictsOnEachCall() throws InterruptedException {
190+
RRCache<String, String> noEvictionStrategyCache = new RRCache.Builder<String, String>(10)
191+
.defaultTTL(50)
192+
.evictionStrategy(new RRCache.NoEvictionStrategy<>())
193+
.build();
194+
195+
noEvictionStrategyCache.put("x", "1");
196+
Thread.sleep(50);
197+
int size = noEvictionStrategyCache.size();
198+
199+
Assertions.assertEquals(0, size);
200+
}
201+
202+
@Test
203+
void testBuilderThrowsExceptionIfEvictionStrategyNull() {
204+
Executable executable = () -> new RRCache.Builder<String, String>(10)
205+
.defaultTTL(50)
206+
.evictionStrategy(null)
207+
.build();
208+
209+
Assertions.assertThrows(IllegalArgumentException.class, executable);
210+
}
158211
}

0 commit comments

Comments
 (0)