Skip to content

Commit

Permalink
Merge pull request #2638 from ebean-orm/feature/TenantAwareCache
Browse files Browse the repository at this point in the history
Refactor L2 cache tenant awareness - add TenantAwareCache
  • Loading branch information
rbygrave authored Apr 7, 2022
2 parents daf5220 + f5a779e commit e41f9a0
Show file tree
Hide file tree
Showing 14 changed files with 314 additions and 108 deletions.
7 changes: 7 additions & 0 deletions ebean-api/src/main/java/io/ebean/cache/ServerCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,11 @@ default ServerCacheStatistics statistics(boolean reset) {
default void visit(MetricVisitor visitor) {
// do nothing by default
}

/**
* Unwrap the underlying ServerCache.
*/
default <T> T unwrap(Class<T> cls) {
return (T) this;
}
}
9 changes: 9 additions & 0 deletions ebean-api/src/main/java/io/ebean/cache/ServerCacheConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class ServerCacheConfig {
private final ServerCacheOptions cacheOptions;
private final CurrentTenantProvider tenantProvider;
private final QueryCacheEntryValidate queryCacheEntryValidate;
private final TenantAwareKey tenantAwareKey;

public ServerCacheConfig(ServerCacheType type, String cacheKey, String shortName, ServerCacheOptions cacheOptions, CurrentTenantProvider tenantProvider, QueryCacheEntryValidate queryCacheEntryValidate) {
this.type = type;
Expand All @@ -21,6 +22,14 @@ public ServerCacheConfig(ServerCacheType type, String cacheKey, String shortName
this.cacheOptions = cacheOptions;
this.tenantProvider = tenantProvider;
this.queryCacheEntryValidate = queryCacheEntryValidate;
this.tenantAwareKey = (tenantProvider == null) ? null : new TenantAwareKey(tenantProvider);
}

/**
* Return the ServerCache taking into account if multi-tenant is used.
*/
public ServerCache tenantAware(ServerCache cache) {
return tenantAwareKey == null ? cache : new TenantAwareCache(cache, tenantAwareKey);
}

/**
Expand Down
104 changes: 104 additions & 0 deletions ebean-api/src/main/java/io/ebean/cache/TenantAwareCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.ebean.cache;

import io.ebean.meta.MetricVisitor;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* A ServerCache proxy that is tenant aware.
*/
public final class TenantAwareCache implements ServerCache {

private final ServerCache delegate;
private final TenantAwareKey tenantAwareKey;

/**
* Create given the TenantAwareKey and delegate cache to proxy to.
*
* @param delegate The cache to proxy to
* @param tenantAwareKey Provides tenant aware keys to use in the cache
*/
public TenantAwareCache(ServerCache delegate, TenantAwareKey tenantAwareKey) {
this.delegate = delegate;
this.tenantAwareKey = tenantAwareKey;
}

/**
* Return the underlying ServerCache that is being delegated to.
*/
@Override
public <T> T unwrap(Class<T> cls) {
return (T)delegate;
}

@Override
public void visit(MetricVisitor visitor) {
delegate.visit(visitor);
}

private Object key(Object key) {
return tenantAwareKey.key(key);
}

@Override
public Object get(Object id) {
return delegate.get(key(id));
}

@Override
public void put(Object id, Object value) {
delegate.put(key(id), value);
}

@Override
public void remove(Object id) {
delegate.remove(key(id));
}

@Override
public void clear() {
delegate.clear();
}

@Override
public int size() {
return delegate.size();
}

@Override
public int getHitRatio() {
return delegate.getHitRatio();
}

@Override
public ServerCacheStatistics getStatistics(boolean reset) {
return delegate.getStatistics(reset);
}

@Override
public Map<Object, Object> getAll(Set<Object> keys) {
Map<Object, Object> keyMapping = new HashMap<>(keys.size());
keys.forEach(k -> keyMapping.put(key(k), k));
Map<Object, Object> tmp = delegate.getAll(keyMapping.keySet());
Map<Object, Object> ret = new HashMap<>(keys.size());
// unwrap tenant info here
tmp.forEach((k,v)-> ret.put(((TenantAwareKey.CacheKey) k).key, v));
return ret;
}

@Override
public void putAll(Map<Object, Object> keyValues) {
Map<Object, Object> tmp = new HashMap<>();
keyValues.forEach((k, v) -> tmp.put(key(k), v));
delegate.putAll(tmp);
}

@Override
public void removeAll(Set<Object> keys) {
delegate.removeAll(keys.stream().map(this::key).collect(Collectors.toSet()));
}

}
122 changes: 122 additions & 0 deletions ebean-api/src/test/java/io/ebean/cache/TenantAwareCacheTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.ebean.cache;

import io.ebean.cache.TenantAwareKey.CacheKey;
import io.ebean.config.CurrentTenantProvider;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static org.assertj.core.api.Assertions.assertThat;

class TenantAwareCacheTest {

ServerCache serverCache;
TenantAwareCache cache;

TenantAwareCacheTest() {
TenantAwareKey key = new TenantAwareKey(new TenantProv());
this.serverCache = new Cache();
this.cache = new TenantAwareCache(serverCache, key);
}

@Test
void put_get_remove() {
cache.put("A", "a");
Object val = cache.get("A");
assertThat(val).isEqualTo("a");

CacheKey cacheKey = new CacheKey("A", 42);
Object val2 = serverCache.get(cacheKey);
assertThat(val2).isEqualTo("a");

cache.put("B", "bb");
assertThat(cache.size()).isEqualTo(2);
cache.remove("A");
assertThat(cache.get("A")).isNull();
assertThat(cache.size()).isEqualTo(1);

cache.clear();
assertThat(cache.size()).isEqualTo(0);
assertThat(cache.get("B")).isNull();
}

@Test
void putAll_getAll_removeAll() {
Map<Object,Object> map = new HashMap<>();
map.put("A", "a");
map.put("B", "b");
map.put("C", "c");

cache.putAll(map);
assertThat(cache.size()).isEqualTo(3);
assertThat(cache.get("A")).isEqualTo("a");
assertThat(serverCache.get(new CacheKey("A", 42))).isEqualTo("a");


Map<Object, Object> result = cache.getAll(Set.of("A", "B", "C", "D"));
assertThat(result).hasSize(3);
assertThat(result).containsOnlyKeys("A", "B", "C");
assertThat(result.values()).containsOnly("a", "b", "c");

cache.removeAll(Set.of("A", "C", "D"));
assertThat(cache.size()).isEqualTo(1);

assertThat(cache.get("B")).isEqualTo("b");
assertThat(serverCache.get(new CacheKey("B", 42))).isEqualTo("b");

cache.remove("B");
assertThat(cache.size()).isEqualTo(0);
}


static class TenantProv implements CurrentTenantProvider {

@Override
public Object currentId() {
return 42;
}
}

static class Cache implements ServerCache {

Map<Object, Object> map = new ConcurrentHashMap<>();

@Override
public Object get(Object id) {
return map.get(id);
}

@Override
public void put(Object id, Object value) {
map.put(id, value);
}

@Override
public void remove(Object id) {
map.remove(id);
}

@Override
public void clear() {
map.clear();
}

@Override
public int size() {
return map.size();
}

@Override
public int getHitRatio() {
return 0;
}

@Override
public ServerCacheStatistics getStatistics(boolean reset) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,12 @@ public class DefaultServerCache implements ServerCache {
private final int trimFrequency;
private final int maxIdleSecs;
private final int maxSecsToLive;
private final TenantAwareKey tenantAwareKey;

public DefaultServerCache(DefaultServerCacheConfig config) {
this.name = config.getName();
this.shortName = config.getShortName();
this.map = config.getMap();
this.maxSize = config.getMaxSize();
this.tenantAwareKey = new TenantAwareKey(config.getTenantProvider());
this.maxIdleSecs = config.getMaxIdleSecs();
this.maxSecsToLive = config.getMaxSecsToLive();
this.trimFrequency = config.determineTrimFrequency();
Expand Down Expand Up @@ -152,19 +150,12 @@ public void clear() {
map.clear();
}

/**
* Return the tenant aware key.
*/
protected Object key(Object id) {
return tenantAwareKey.key(id);
}

/**
* Return a value from the cache.
*/
@Override
public Object get(Object id) {
CacheEntry entry = getCacheEntry(id);
public Object get(Object key) {
CacheEntry entry = getCacheEntry(key);
if (entry == null) {
missCount.increment();
return null;
Expand All @@ -184,8 +175,8 @@ protected Object unwrapEntry(CacheEntry entry) {
/**
* Get the cache entry - override for query cache to validate dependent tables.
*/
protected CacheEntry getCacheEntry(Object id) {
final SoftReference<CacheEntry> ref = map.get(key(id));
protected CacheEntry getCacheEntry(Object key) {
final SoftReference<CacheEntry> ref = map.get(key);
return ref != null ? ref.get() : null;
}

Expand All @@ -198,8 +189,7 @@ public void putAll(Map<Object, Object> keyValues) {
* Put a value into the cache.
*/
@Override
public void put(Object id, Object value) {
Object key = key(id);
public void put(Object key, Object value) {
map.put(key, new SoftReference<>(new CacheEntry(key, value)));
putCount.increment();
}
Expand All @@ -208,8 +198,8 @@ public void put(Object id, Object value) {
* Remove an entry from the cache.
*/
@Override
public void remove(Object id) {
SoftReference<CacheEntry> entry = map.remove(key(id));
public void remove(Object key) {
SoftReference<CacheEntry> entry = map.remove(key);
if (entry != null && entry.get() != null) {
removeCount.increment();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import io.ebean.cache.QueryCacheEntryValidate;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.config.CurrentTenantProvider;
import io.ebeaninternal.server.cache.DefaultServerCache.CacheEntry;

import java.lang.ref.SoftReference;
Expand Down Expand Up @@ -34,10 +33,6 @@ public DefaultServerCacheConfig(ServerCacheConfig config, Map<Object, SoftRefere
this.maxSize = options.getMaxSize();
}

public CurrentTenantProvider getTenantProvider() {
return config.getTenantProvider();
}

public QueryCacheEntryValidate getQueryCacheEntryValidate() {
return config.getQueryCacheEntryValidate();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ServerCache createCache(ServerCacheConfig config) {
if (executor != null) {
cache.periodicTrim(executor);
}
return cache;
return config.tenantAware(cache);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ protected Object unwrapEntry(CacheEntry entry) {
}

@Override
protected CacheEntry getCacheEntry(Object id) {
Object key = key(id);
protected CacheEntry getCacheEntry(Object key) {
final SoftReference<CacheEntry> ref = map.get(key);
CacheEntry entry = ref != null ? ref.get() : null;
if (entry == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public long get(boolean reset) {

@Override
public void visit(MetricVisitor visitor) {

long val = visitor.reset() ? count.sumThenReset() : count.sum();
if (val > 0) {
visitor.visitCount(new DCountMetricStats(name, val));
Expand Down
Loading

0 comments on commit e41f9a0

Please sign in to comment.