#1427 - QueryCache should be cleared, if one of a dependent bean is updated

Initial work, does not include propagation across cluster.
This commit is contained in:
rob bygrave
2018-06-15 17:36:23 +12:00
parent 59537227b6
commit 96cc8d7605
51 changed files with 952 additions and 283 deletions
@@ -3,6 +3,8 @@ package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import io.ebean.config.ServerConfig;
import io.ebeaninternal.server.transaction.TableModState;
import org.tests.model.basic.Contact;
import org.tests.model.basic.Customer;
import org.junit.Test;
@@ -17,10 +19,17 @@ public class DefaultCacheHolderTest {
private final ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
private final ServerCacheOptions defaultOptions = new ServerCacheOptions();
@Test
public void getCache_normal() throws Exception {
private CacheManagerOptions options() {
return new CacheManagerOptions(null, new ServerConfig(), true)
.with(defaultOptions, defaultOptions)
.with(cacheFactory, new TableModState());
}
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, null);
@Test
public void getCache_normal() {
DefaultCacheHolder holder = new DefaultCacheHolder(options());
DefaultServerCache cache = cache(holder, Customer.class, "customer");
assertThat(cache.getName()).isEqualTo("customer_B");
@@ -40,7 +49,9 @@ public class DefaultCacheHolderTest {
@Test
public void getCache_multiTenant() throws Exception {
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, tenantId::get);
CacheManagerOptions builder = options().with(tenantId::get);
DefaultCacheHolder holder = new DefaultCacheHolder(builder);
tenantId.set("ten_1");
DefaultServerCache cache = cache(holder, Customer.class, "customer");
@@ -48,26 +59,26 @@ public class DefaultCacheHolderTest {
cache.put("1", "value-for-tenant1");
cache.put("2", "an other value-for-tenant1");
assertThat(cache.size()).isEqualTo(2);
tenantId.set("ten_2");
cache.put("1", "value-for-tenant2");
cache.put("2", "an other value-for-tenant2");
assertThat(cache.size()).isEqualTo(4);
assertThat(cache.get("1")).isEqualTo("value-for-tenant2");
assertThat(cache.get("2")).isEqualTo("an other value-for-tenant2");
tenantId.set("ten_1");
assertThat(cache.get("1")).isEqualTo("value-for-tenant1");
assertThat(cache.get("2")).isEqualTo("an other value-for-tenant1");
Exception exInThread[] = new Exception[1];
Exception exInThread[] = new Exception[1];
Thread t = new Thread() {
@Override
public void run() {
@@ -77,29 +88,29 @@ public class DefaultCacheHolderTest {
cache.put("1", "value-for-tenant2");
cache.put("2", "an other value-for-tenant2");
tenantId.set(null);
cache.clear();
} catch (Exception e) {
exInThread[0] = e;
}
};
};
// do some async work
t.start();
t.join();
if (exInThread[0] != null) {
if (exInThread[0] != null) {
throw exInThread[0];
}
assertThat(cache.size()).isEqualTo(0);
}
@Test
public void clearAll() throws Exception {
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, null);
public void clearAll() {
DefaultCacheHolder holder = new DefaultCacheHolder(options());
DefaultServerCache cache = cache(holder, Customer.class, "customer");
cache.put("foo", "foo");
assertThat(cache.size()).isEqualTo(1);
@@ -109,8 +120,11 @@ public class DefaultCacheHolderTest {
}
@Test
public void clearAll_multiTenant() throws Exception {
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, tenantId::get);
public void clearAll_multiTenant() {
CacheManagerOptions options = options().with(tenantId::get);
DefaultCacheHolder holder = new DefaultCacheHolder(options);
DefaultServerCache cache = cache(holder, Customer.class, "customer");
cache.put("foo", "foo");
assertThat(cache.size()).isEqualTo(1);
@@ -118,5 +132,5 @@ public class DefaultCacheHolderTest {
holder.clearAll();
assertThat(cache.size()).isEqualTo(0);
}
}
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import io.ebean.config.ServerConfig;
import org.tests.model.basic.Article;
import org.tests.model.basic.Order;
import org.tests.model.basic.Product;
@@ -19,7 +20,10 @@ public class DefaultCacheHolder_getCacheOptions_Test {
defaultOptions.setMaxSize(10000);
defaultOptions.setMaxSecsToLive(120);
this.cacheHolder = new DefaultCacheHolder(null, defaultOptions, defaultOptions, null);
CacheManagerOptions builder = new CacheManagerOptions(null, new ServerConfig(), true)
.with(defaultOptions, defaultOptions);
this.cacheHolder = new DefaultCacheHolder(builder);
}
@Test
@@ -0,0 +1,52 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import org.junit.Test;
import static org.junit.Assert.*;
public class DefaultServerCacheConfigTest {
private DefaultServerCacheConfig create(int maxSize, int maxIdleSecs, int maxSecsToLive, int trimFreq) {
ServerCacheOptions options = new ServerCacheOptions();
options.setMaxSize(maxSize);
options.setMaxIdleSecs(maxIdleSecs);
options.setMaxSecsToLive(maxSecsToLive);
options.setTrimFrequency(trimFreq);
return new DefaultServerCacheConfig(new ServerCacheConfig(null, null, options, null, null));
}
@Test
public void trimFreq_halfIdle() {
assertEquals(create(10000,10,20, 0).determineTrimFrequency(), 4);
}
@Test
public void trimFreq_halfIdle_withRounding() {
assertEquals(create(10000,11,20, 0).determineTrimFrequency(), 4);
}
@Test
public void trimFreq_halfTTL() {
assertEquals(create(10000,0,20, 0).determineTrimFrequency(), 9);
}
@Test
public void trimFreq_halfTTL_withRounding() {
assertEquals(create(10000,0,21, 0).determineTrimFrequency(), 9);
}
@Test
public void trimFreq_explicit() {
assertEquals(create(10000,10,20, 42).determineTrimFrequency(), 42);
}
}
@@ -1,6 +1,8 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -15,11 +17,13 @@ public class DefaultServerCacheTest {
cacheOptions.setMaxSecsToLive(600);
cacheOptions.setTrimFrequency(60);
return new DefaultServerCache("foo", null, cacheOptions);
ServerCacheConfig con = new ServerCacheConfig(ServerCacheType.BEAN, "foo", cacheOptions, null, null);
DefaultServerCacheConfig config = new DefaultServerCacheConfig(con);
return new DefaultServerCache(config);
}
@Test
public void testGetHitRatio() throws Exception {
public void testGetHitRatio() {
DefaultServerCache cache = createCache();
assertEquals(0, cache.getHitRatio());
@@ -34,7 +38,7 @@ public class DefaultServerCacheTest {
}
@Test
public void testSize() throws Exception {
public void testSize() {
DefaultServerCache cache = createCache();
assertEquals(0, cache.size());
@@ -50,39 +54,5 @@ public class DefaultServerCacheTest {
assertEquals(0, cache.size());
}
@Test
public void trimFreq_halfIdle() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 10, 20, 0);
assertEquals(cache.trimFrequency, 4);
}
@Test
public void trimFreq_halfIdle_withRounding() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 11, 20, 0);
assertEquals(cache.trimFrequency, 4);
}
@Test
public void trimFreq_halfTTL() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 0, 20, 0);
assertEquals(cache.trimFrequency, 9);
}
@Test
public void trimFreq_halfTTL_withRounding() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 0, 21, 0);
assertEquals(cache.trimFrequency, 9);
}
@Test
public void trimFreq_explicit() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 10, 20, 42);
assertEquals(cache.trimFrequency, 42);
}
}
@@ -1,7 +1,9 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.cache.ServerCacheType;
import org.junit.Ignore;
import org.junit.Test;
@@ -19,7 +21,8 @@ public class DefaultServerCache_RunEvictionTest {
cacheOptions.setMaxSecsToLive(2);
cacheOptions.setTrimFrequency(1);
return new DefaultServerCache("foo", null, cacheOptions);
ServerCacheConfig con = new ServerCacheConfig(ServerCacheType.BEAN, "foo", cacheOptions, null, null);
return new DefaultServerCache(new DefaultServerCacheConfig(con));
}
private final DefaultServerCache cache;
@@ -0,0 +1,44 @@
package io.ebeaninternal.server.transaction;
import org.junit.Test;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.*;
public class TableModStateTest {
private TableModState tableModState = new TableModState();
@Test
public void isValid() {
long now = System.currentTimeMillis();
tableModState.touch(setOf("one", "two", "three"), now);
// empty
assertTrue(tableModState.isValid(Collections.emptySet(), 12L));
// no entry
assertTrue(tableModState.isValid(setOf("noEntry"), 12L));
// later timestamp
assertTrue(tableModState.isValid(setOf("one"), now + 1));
assertTrue(tableModState.isValid(setOf("one", "two", "noEntry"), now + 1));
// invalid
assertFalse(tableModState.isValid(setOf("one"), now - 1));
assertFalse(tableModState.isValid(setOf("one", "two"), now - 1));
assertFalse(tableModState.isValid(setOf("three", "two"), now - 1));
}
private Set<String> setOf(String... tables) {
Set<String> touched = new HashSet<>();
Collections.addAll(touched, tables);
return touched;
}
}
@@ -0,0 +1,59 @@
package org.tests.cache;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.cache.ServerCache;
import org.junit.Test;
import org.tests.model.basic.Address;
import org.tests.model.basic.Customer;
import org.tests.model.basic.ResetBasicData;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestQueryCacheTableDependency extends BaseTestCase {
@Test
public void testFindCountOnDependent() {
ResetBasicData.reset();
ServerCache customerCache = Ebean.getServerCacheManager().getQueryCache(Customer.class);
customerCache.clear();
List<Address> addrs = Ebean.find(Address.class)
.where().eq("line2", "St Lukes")
.findList();
int custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lukes")
.findCount();
assertThat(custs).isEqualTo(3);
custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lukes")
.findCount();
assertThat(custs).isEqualTo(3);
Address a1 = addrs.get(0);
a1.setLine2("St Lucky");
Ebean.save(a1);
custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lukes")
.findCount();
assertThat(custs).isEqualTo(2); // cache says 3
custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lucky")
.findCount();
assertThat(custs).isEqualTo(1);
}
}
@@ -1,5 +1,6 @@
package org.tests.model.basic;
import io.ebean.annotation.InvalidateQueryCache;
import org.tests.model.basic.metaannotation.SizeMedium;
import javax.persistence.Column;
@@ -14,6 +15,11 @@ import java.sql.Timestamp;
/**
* Address entity bean.
*/
// Address is not L2 cached directly but it is joined to queries that are cached
// What InvalidateQueryCache means is that we propagate a table modification event
// when address is changed ... and cached queries that join to address will be
// invalidated accordingly.
@InvalidateQueryCache
@Entity
@Table(name = "o_address")
public class Address {