package org.javersion.store.jdbc; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableMap; import com.querydsl.sql.SQLQueryFactory; import org.apache.commons.lang3.mutable.MutableBoolean; import org.javersion.core.Revision; import org.javersion.object.ObjectVersion; import org.javersion.object.ObjectVersionGraph; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.assertThat; import static org.javersion.core.Revision.NODE; import static org.javersion.path.PropertyPath.ROOT; import static org.javersion.store.jdbc.GraphOptions.keepHeadsAndNewest; import static org.javersion.store.sql.QDocumentVersion.documentVersion; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = PersistenceTestConfiguration.class) public class GuavaGraphCacheTest { @Resource DocumentVersionStoreJdbc<String, String, JDocumentVersion<String>> documentStore; @Resource SQLQueryFactory queryFactory; @Resource TransactionTemplate transactionTemplate; @Test public void load_and_refresh() { GuavaGraphCache<String, String> cache = newRefreshingCache(); String docId = randomUUID().toString(); ObjectVersionGraph<String> versionGraph = cache.load(docId); assertThat(versionGraph.isEmpty()).isTrue(); ObjectVersion<String> version = ObjectVersion.<String>builder() .changeset(ImmutableMap.of(ROOT.property("property"), "value")) .build(); documentStore.append(docId, ObjectVersionGraph.init(version).getTip()); documentStore.publish(); versionGraph = cache.load(docId); assertThat(versionGraph.isEmpty()).isFalse(); assertThat(versionGraph.getTip().getVersion()).isEqualTo(version); version = ObjectVersion.<String>builder() .changeset(ImmutableMap.of(ROOT.property("property"), "value2")) .build(); documentStore.append(docId, ObjectVersionGraph.init(version).getTip()); documentStore.publish(); versionGraph = cache.load(docId); assertThat(versionGraph.getTip().getVersion()).isEqualTo(version); cache = newRefreshingCache(); versionGraph = cache.load(docId); assertThat(versionGraph.getTip().getVersion()).isEqualTo(version); assertThat(cache.load(docId)).isSameAs(versionGraph); } @Test public void manual_refresh() { String docId = randomUUID().toString(); GuavaGraphCache<String, String> cache = newNonRefreshingCache(); ObjectVersionGraph<String> versionGraph = cache.load(docId); assertThat(versionGraph.isEmpty()).isTrue(); ObjectVersion<String> version = ObjectVersion.<String>builder() .changeset(ImmutableMap.of(ROOT.property("property"), "value")) .build(); documentStore.append(docId, ObjectVersionGraph.init(version).getTip()); documentStore.publish(); versionGraph = cache.load(docId); assertThat(versionGraph.isEmpty()).isTrue(); cache.refresh(docId); versionGraph = cache.load(docId); assertThat(versionGraph.isEmpty()).isFalse(); assertThat(versionGraph.getTip().getVersion()).isEqualTo(version); } @Test public void clear_cache() throws InterruptedException { String docId = randomUUID().toString(); GuavaGraphCache<String, String> cache = newNonRefreshingCache(); ObjectVersion<String> version = ObjectVersion.<String>builder().build(); // empty version documentStore.append(docId, ObjectVersionGraph.init(version).getTip()); documentStore.publish(); assertThat(cache.load(docId).isEmpty()).isEqualTo(false); queryFactory.delete(documentVersion).where(documentVersion.revision.eq(version.revision)).execute(); // Does not hit database assertThat(cache.load(docId).isEmpty()).isEqualTo(false); cache.evictAll(); assertThat(cache.load(docId).isEmpty()).isEqualTo(true); } @Test public void return_empty_if_fetch_fails() throws InterruptedException { String docId = randomUUID().toString(); GuavaGraphCache<String, String> cache = newRefreshingCache(); ObjectVersion<String> version = ObjectVersion.<String>builder().build(); // empty version documentStore.append(docId, ObjectVersionGraph.init(version).getTip()); publish(cache, docId); assertThat(cache.load(docId).isEmpty()).isEqualTo(false); queryFactory.delete(documentVersion).where(documentVersion.revision.eq(version.revision)).execute(); Thread.sleep(10); assertThat(cache.load(docId).isEmpty()).isEqualTo(true); } @Test public void auto_refresh_only_cached_graphs() { final MutableBoolean cacheRefreshed = new MutableBoolean(false); DocumentVersionStoreJdbc<String, String, JDocumentVersion<String>> proxyStore = new DocumentVersionStoreJdbc<String, String, JDocumentVersion<String>>(documentStore.options) { @Override protected FetchResults<String, String> doFetch(String docId, boolean optimized) { cacheRefreshed.setTrue(); throw new RuntimeException("Should not refresh!"); } }; String docId = randomUUID().toString(); GuavaGraphCache<String, String> cache = new GuavaGraphCache<String, String>(proxyStore, // Non-refreshing cache CacheBuilder.<String, ObjectVersionGraph<String>>newBuilder() .maximumSize(8)); ObjectVersion<String> version = ObjectVersion.<String>builder() .changeset(ImmutableMap.of(ROOT.property("property"), "value")) .build(); proxyStore.append(docId, ObjectVersionGraph.init(version).getTip()); // This should not refresh cache as docId is not cached! cache.refresh(docId); assertThat(cacheRefreshed.getValue()).isFalse(); } /** * Cache compaction: Keep heads + 1 newest * * v1 * | * v2 * / \ * v3 | * | v4 * v5 | * | | * v6 | * \/ * v7 */ @Test public void compact_keep_heads_and_one_newest() { ObjectVersionGraph<String> graph = ObjectVersionGraph.init(); GuavaGraphCache<String, String> cache = newRefreshingCache(1, keepHeadsAndNewest(1, 2)); final String docId = randomUUID().toString(); final Revision v1 = new Revision(1, NODE), v2 = new Revision(2, NODE), v3 = new Revision(3, NODE), v4 = new Revision(4, NODE), v5 = new Revision(5, NODE), v6 = new Revision(6, NODE), v7 = new Revision(7, NODE); graph = graph.commit(ObjectVersion.<String>builder(v1).build()); documentStore.append(docId, graph.getTip()); graph = graph.commit(ObjectVersion.<String>builder(v2).parents(v1).build()); documentStore.append(docId, graph.getTip()); assertCacheContains(cache, docId, v1, v2); // v1 is dropped graph = graph.commit(ObjectVersion.<String>builder(v3).parents(v2).build()); documentStore.append(docId, graph.getTip()); assertCacheContains(cache, docId, v2, v3); // heads v5 and v4 + one newer (v3) and LCA (v2) are kept graph = graph.commit(ObjectVersion.<String>builder(v4).parents(v2).build()); documentStore.append(docId, graph.getTip()); graph = graph.commit(ObjectVersion.<String>builder(v5).parents(v3).build()); documentStore.append(docId, graph.getTip()); assertCacheContains(cache, docId, v2, v3, v4, v5); // v3 is dropped graph = graph.commit(ObjectVersion.<String>builder(v6).parents(v5).build()); documentStore.append(docId, graph.getTip()); assertCacheContains(cache, docId, v2, v4, v5, v6); // all other than tip (v7) and second newest are dropped graph = graph.commit(ObjectVersion.<String>builder(v7).parents(v6, v4).build()); documentStore.append(docId, graph.getTip()); // Optimizing storage doesn't effect cache... documentStore.publish(); transactionTemplate.execute(status -> { documentStore.updateBatch(docId) .optimize(documentStore.getOptimizedGraph(docId), v -> v.revision.equals(v7)) .execute(); return null; }); assertCacheContains(cache, docId, v6, v7); // ...until reload cache.evict(docId); assertCacheContains(cache, docId, v7); } @Test(expected = IllegalArgumentException.class) public void keep_predicate_function_is_required() { new GraphOptions<String, String>(g -> true, null); } @Test(expected = IllegalArgumentException.class) public void when_predicate_is_required() { new GraphOptions<String, String>(null, (g) -> v -> true); } private void assertCacheContains(GuavaGraphCache<String, String> cache, String docId, Revision... revisions) { publish(cache, docId); ObjectVersionGraph<String> graph = cache.load(docId); assertThat(graph.size()).isEqualTo(revisions.length); for (Revision revision : revisions) { assertThat(graph.contains(revision)).isTrue().overridingErrorMessage("%s not found", revision); } } private void publish(GuavaGraphCache<String, String> cache, String docId) { documentStore.publish(); cache.refresh(docId); } private GuavaGraphCache<String, String> newRefreshingCache() { return newRefreshingCache(1, new GraphOptions<>()); } private GuavaGraphCache<String, String> newRefreshingCache(long refreshAfterNanos, GraphOptions<String, String> graphOptions) { return new GuavaGraphCache<>(documentStore, CacheBuilder.<String, ObjectVersionGraph<String>>newBuilder() .maximumSize(8) .refreshAfterWrite(refreshAfterNanos, TimeUnit.NANOSECONDS), graphOptions ); } private GuavaGraphCache<String, String> newNonRefreshingCache() { return new GuavaGraphCache<>(documentStore, // Non-refreshing cache CacheBuilder.<String, ObjectVersionGraph<String>>newBuilder() .maximumSize(8)); } }