Skip to content

Add TTL support for CSC #4115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/main/java/redis/clients/jedis/csc/AbstractCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ public abstract class AbstractCache implements Cache {
private Cacheable cacheable;
private final Map<ByteBuffer, Set<CacheKey<?>>> redisKeysToCacheKeys = new ConcurrentHashMap<>();
private final int maximumSize;
private ReentrantLock lock = new ReentrantLock();
private final long ttl;
ReentrantLock lock = new ReentrantLock();
private volatile CacheStats stats = new CacheStats();

protected AbstractCache(int maximumSize) {
this(maximumSize, DefaultCacheable.INSTANCE);
protected AbstractCache(int maximumSize, long ttl) {
this(maximumSize, ttl, DefaultCacheable.INSTANCE);
}

protected AbstractCache(int maximumSize, Cacheable cacheable) {
protected AbstractCache(int maximumSize, long ttl, Cacheable cacheable) {
this.maximumSize = maximumSize;
this.ttl = ttl;
this.cacheable = cacheable;
}

Expand All @@ -42,6 +44,11 @@ public int getMaxSize() {
return maximumSize;
}

@Override
public long getTtl() {
return ttl;
};

@Override
public abstract int getSize();

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/redis/clients/jedis/csc/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public interface Cache {
*/
int getSize();

/**
* @return The ttl of the cache
*/
long getTtl();

/**
* @return All the entries within the cache
*/
Expand Down Expand Up @@ -110,4 +115,9 @@ public interface Cache {
* @return The compatibility of cache against different Redis versions
*/
boolean compatibilityMode();

/**
* Releases any system resources associated with it
*/
void close();
}
16 changes: 16 additions & 0 deletions src/main/java/redis/clients/jedis/csc/CacheConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public class CacheConfig {

private int maxSize;
private long ttl;
private Cacheable cacheable;
private EvictionPolicy evictionPolicy;
private Class cacheClass;
Expand All @@ -11,6 +12,10 @@ public int getMaxSize() {
return maxSize;
}

public long getTtl() {
return ttl;
}

public Cacheable getCacheable() {
return cacheable;
}
Expand All @@ -28,7 +33,12 @@ public static Builder builder() {

public static class Builder {
private final int DEFAULT_MAX_SIZE = 10000;
/**
* 30000ms
*/
private final long DEFAULT_TTL = 30000;
private int maxSize = DEFAULT_MAX_SIZE;
private long ttl = DEFAULT_TTL;
private Cacheable cacheable = DefaultCacheable.INSTANCE;
private EvictionPolicy evictionPolicy;
private Class cacheClass;
Expand All @@ -38,6 +48,11 @@ public Builder maxSize(int maxSize) {
return this;
}

public Builder ttl(long ttl) {
this.ttl = ttl;
return this;
}

public Builder evictionPolicy(EvictionPolicy policy) {
this.evictionPolicy = policy;
return this;
Expand All @@ -56,6 +71,7 @@ public Builder cacheClass(Class cacheClass) {
public CacheConfig build() {
CacheConfig cacheConfig = new CacheConfig();
cacheConfig.maxSize = this.maxSize;
cacheConfig.ttl = this.ttl;
cacheConfig.cacheable = this.cacheable;
cacheConfig.evictionPolicy = this.evictionPolicy;
cacheConfig.cacheClass = this.cacheClass;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/redis/clients/jedis/csc/CacheConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public <T> T executeCommand(final CommandObject<T> commandObject) {
// CACHE MISS !!
cache.getStats().miss();
T value = super.executeCommand(commandObject);
cacheEntry = new CacheEntry<>(cacheKey, value, this);
cacheEntry = new CacheEntry<>(cacheKey, value, this, cache.getTtl());
cache.set(cacheKey, cacheEntry);
// this line actually provides a deep copy of cached object instance
value = cacheEntry.getValue();
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/redis/clients/jedis/csc/CacheEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ public class CacheEntry<T> {
private final CacheKey<T> cacheKey;
private final WeakReference<CacheConnection> connection;
private final byte[] bytes;
private long expireTime;

public CacheEntry(CacheKey<T> cacheKey, T value, CacheConnection connection) {
public CacheEntry(CacheKey<T> cacheKey, T value, CacheConnection connection, long ttl) {
this.cacheKey = cacheKey;
this.connection = new WeakReference<>(connection);
this.bytes = toBytes(value);
this.expireTime = System.currentTimeMillis() + ttl;
}

public CacheKey<T> getCacheKey() {
Expand Down Expand Up @@ -53,4 +55,8 @@ private T toObject(byte[] data) {
throw new JedisCacheException("Failed to deserialize object", e);
}
}

public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
2 changes: 1 addition & 1 deletion src/main/java/redis/clients/jedis/csc/CacheFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static Cache getCache(CacheConfig config) {
if (config.getCacheable() == null) {
throw new JedisCacheException("Cacheable is required to create the default cache!");
}
return new DefaultCache(config.getMaxSize(), config.getCacheable(), getEvictionPolicy(config));
return new DefaultCache(config.getMaxSize(), config.getTtl(), config.getCacheable(), getEvictionPolicy(config));
}
return instantiateCustomCache(config);
}
Expand Down
60 changes: 49 additions & 11 deletions src/main/java/redis/clients/jedis/csc/DefaultCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,47 @@

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class DefaultCache extends AbstractCache {

protected final Map<CacheKey, CacheEntry> cache;
private final EvictionPolicy evictionPolicy;
private final ScheduledExecutorService scheduler;

protected DefaultCache(int maximumSize) {
this(maximumSize, new HashMap<CacheKey, CacheEntry>());
protected DefaultCache(int maximumSize, long ttl) {
this(maximumSize, ttl, new HashMap<CacheKey, CacheEntry>());
}

protected DefaultCache(int maximumSize, Map<CacheKey, CacheEntry> map) {
this(maximumSize, map, DefaultCacheable.INSTANCE, new LRUEviction(maximumSize));
protected DefaultCache(int maximumSize, long ttl, Map<CacheKey, CacheEntry> map) {
this(maximumSize, ttl, map, DefaultCacheable.INSTANCE, new LRUEviction(maximumSize));
}

protected DefaultCache(int maximumSize, Cacheable cacheable) {
this(maximumSize, new HashMap<CacheKey, CacheEntry>(), cacheable, new LRUEviction(maximumSize));
protected DefaultCache(int maximumSize, long ttl, Cacheable cacheable) {
this(maximumSize, ttl, new HashMap<CacheKey, CacheEntry>(), cacheable, new LRUEviction(maximumSize));
}

protected DefaultCache(int maximumSize, Cacheable cacheable, EvictionPolicy evictionPolicy) {
this(maximumSize, new HashMap<CacheKey, CacheEntry>(), cacheable, evictionPolicy);
protected DefaultCache(int maximumSize, long ttl, Cacheable cacheable, EvictionPolicy evictionPolicy) {
this(maximumSize, ttl, new HashMap<CacheKey, CacheEntry>(), cacheable, evictionPolicy);
}

protected DefaultCache(int maximumSize, Map<CacheKey, CacheEntry> map, Cacheable cacheable, EvictionPolicy evictionPolicy) {
super(maximumSize, cacheable);
protected DefaultCache(int maximumSize, long ttl, Map<CacheKey, CacheEntry> map, Cacheable cacheable, EvictionPolicy evictionPolicy) {
super(maximumSize, ttl, cacheable);
this.cache = map;
this.evictionPolicy = evictionPolicy;
this.evictionPolicy.setCache(this);
this.scheduler = Executors.newSingleThreadScheduledExecutor();
// Periodically clear expired cache every 2 seconds
this.scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
cleanupExpiredEntries();
}
}, ttl, 2000, TimeUnit.MILLISECONDS);
}

@Override
Expand All @@ -47,9 +60,18 @@ public EvictionPolicy getEvictionPolicy() {
return this.evictionPolicy;
}

@Override
public void close() {
this.scheduler.shutdown();
}

@Override
public CacheEntry getFromStore(CacheKey key) {
return cache.get(key);
CacheEntry entry = cache.get(key);
if (entry != null && !entry.isExpired()) {
return entry;
}
return null;
}

@Override
Expand All @@ -72,4 +94,20 @@ protected boolean containsKeyInStore(CacheKey cacheKey) {
return cache.containsKey(cacheKey);
}

private void cleanupExpiredEntries() {
this.lock.lock();
try {
Iterator<Map.Entry<CacheKey, CacheEntry>> iterator = cache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<CacheKey, CacheEntry> entry = iterator.next();
if (entry.getValue().isExpired()) {
iterator.remove();
evictionPolicy.reset(entry.getKey());
}
}
} finally {
lock.unlock();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void lruEvictionTest() {
}

Map<CacheKey, CacheEntry> map = new LinkedHashMap<>(count);
Cache cache = new DefaultCache(count, map);
Cache cache = new DefaultCache(count, 5000, map);
try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), cache)) {

// Retrieve the 100 keys in the same order
Expand Down Expand Up @@ -565,4 +565,27 @@ public void testCacheFactory() throws InterruptedException {
assertEquals(1, stats.getMissCount());
}
}

@Test
public void testCacheEntryExpiredWithUnifiedJedis() throws InterruptedException {
Cache cache = new TestCache();
UnifiedJedis client = new UnifiedJedis(hnp, clientConfig.get(), cache);

try {
// "foo" is cached
client.set("foo", "bar");
client.get("foo"); // read from the server
Assert.assertEquals("bar", client.get("foo")); // cache hit
Assert.assertEquals(1, cache.getSize());

Thread.sleep(10000); // sleep 10000ms

Assert.assertEquals(0, cache.getSize()); // The cache "foo bar" has expired

client.get("foo"); // cache miss, read from the server
Assert.assertEquals(1, cache.getSize());
} finally {
client.close();
}
}
}
8 changes: 4 additions & 4 deletions src/test/java/redis/clients/jedis/csc/TestCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ public TestCache() {
}

public TestCache(Map<CacheKey, CacheEntry> map) {
super(10000, map);
super(10000, 5000, map);
}

public TestCache(Map<CacheKey, CacheEntry> map, Cacheable cacheable) {
super(10000, map, cacheable, new LRUEviction(10000));
super(10000, 5000, map, cacheable, new LRUEviction(10000));
}

public TestCache(int maximumSize, EvictionPolicy evictionPolicy ) {
super(maximumSize, new HashMap<CacheKey, CacheEntry>(), DefaultCacheable.INSTANCE, evictionPolicy);
super(maximumSize, 5000, new HashMap<CacheKey, CacheEntry>(), DefaultCacheable.INSTANCE, evictionPolicy);
}

public TestCache(int maximumSize, EvictionPolicy evictionPolicy, Cacheable cacheable ) {
super(maximumSize, new HashMap<CacheKey, CacheEntry>(), cacheable, evictionPolicy);
super(maximumSize, 5000, new HashMap<CacheKey, CacheEntry>(), cacheable, evictionPolicy);
}

}