package org.javersion.store.jdbc; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import org.javersion.core.Revision; import org.javersion.core.VersionNode; import org.javersion.object.ObjectVersion; import org.javersion.object.ObjectVersionGraph; import org.javersion.path.PropertyPath; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Resource; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Predicate; import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableMap; import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.assertThat; import static org.javersion.path.PropertyPath.ROOT; import static org.javersion.path.PropertyPath.parse; import static org.javersion.store.jdbc.ExecutorType.ASYNC; import static org.javersion.store.jdbc.ExecutorType.SYNC; import static org.javersion.store.jdbc.GraphOptions.keepHeadsAndNewest; import static org.javersion.store.jdbc.GuavaGraphCache.guavaCacheBuilder; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = PersistenceTestConfiguration.class) public abstract class AbstractVersionStoreTest { @Resource TransactionTemplate transactionTemplate; private final long timeSeq = Revision.newUniqueTime(); protected final Revision rev1 = new Revision(1, timeSeq), rev2 = new Revision(2, timeSeq), rev3 = new Revision(3, timeSeq), rev4 = new Revision(4, timeSeq), rev5 = new Revision(5, timeSeq), rev6 = new Revision(6, timeSeq); public static Map<PropertyPath, Object> mapOf(Object... entries) { Map<PropertyPath, Object> map = Maps.newHashMap(); for (int i=0; i+1 < entries.length; i+=2) { map.put(parse(entries[i].toString()), entries[i+1]); } return unmodifiableMap(map); } @Test public void allow_squashed_parent() { AbstractVersionStoreJdbc<String, String, ?, ?, ?> store = getStore(); final String docId = randomUUID().toString(); final String doc2Id = randomUUID().toString(); ObjectVersion<String> v1 = ObjectVersion.<String>builder(rev1).changeset(mapOf("property", "value1")).build(), v2 = ObjectVersion.<String>builder(rev2).changeset(mapOf("property", null)).parents(rev1).build(), v3 = ObjectVersion.<String>builder(rev3).changeset(mapOf("property", "value3")).parents(rev1).build(), v4 = ObjectVersion.<String>builder(rev4).build(); final ObjectVersionGraph<String> originalGraph = ObjectVersionGraph.init(v1, v2, v3); addVersions(docId, store, originalGraph.getVersionNode(rev1), originalGraph.getVersionNode(rev2)); // rev1 is optimized away optimize(docId, v -> v.revision.equals(rev2), store); assertThat(store.getOptimizedGraph(docId).contains(rev1)).isFalse(); // Load one (loadOptimized) addVersions(docId, store, originalGraph.getVersionNode(rev3)); ObjectVersionGraph<String> loadedGraph = store.getOptimizedGraph(docId); // Optimization is reset assertThat(loadedGraph.getVersionNode(rev1).getVersion()).isEqualTo(v1); assertThat(loadedGraph.getVersionNode(rev2).getVersion()).isEqualTo(v2); assertThat(loadedGraph.getVersionNode(rev3).getVersion()).isEqualTo(v3); // Batch load addVersions(doc2Id, store, ObjectVersionGraph.init(v4).getTip()); GraphResults<String, String> results = store.getGraphs(asList(docId, doc2Id)); assertThat(results.getVersionGraph(docId).getVersionNode(rev1).getVersion()).isEqualTo(v1); assertThat(results.getVersionGraph(docId).getVersionNode(rev2).getVersion()).isEqualTo(v2); assertThat(results.getVersionGraph(docId).getVersionNode(rev3).getVersion()).isEqualTo(v3); assertThat(results.getVersionGraph(doc2Id).getVersionNode(rev4).getVersion()).isEqualTo(v4); } @Test public void optimize_progressively() { AbstractVersionStoreJdbc<String, String, ?, ?, ?> store = getStore(); final String docId = randomUUID().toString(); ObjectVersionGraph<String> originalGraph = graphForOptimization(); addVersions(docId, store, ImmutableList.copyOf(originalGraph.getVersionNodes()).reverse()); store.publish(); optimize(docId, v -> !v.revision.equals(rev1), store); // Non-optimized load returns still full graph assertThat(store.getFullGraph(docId).size()).isEqualTo(6); ObjectVersionGraph<String> versionGraph = store.getOptimizedGraph(docId); assertThat(versionGraph.size()).isEqualTo(5); VersionNode<PropertyPath, Object, String> versionNode = versionGraph.getVersionNode(rev2); assertThat(versionNode.getParentRevisions()).isEqualTo(ImmutableSet.of()); assertThat(versionNode.getChangeset()).isEqualTo(mapOf( "property1", "value1" // redundant tombstone ("property2", null) is removed )); // Keep rev5, rev6 and their LCA rev3 optimize(docId, v -> v.revision.equals(rev5) || v.revision.equals(rev6), store); // Non-optimized load returns still full graph assertThat(store.getFullGraph(docId).size()).isEqualTo(6); versionGraph = store.getOptimizedGraph(docId); assertThat(versionGraph.size()).isEqualTo(3); versionNode = versionGraph.getVersionNode(rev3); assertThat(versionNode.getParentRevisions()).isEqualTo(ImmutableSet.of()); assertThat(versionNode.getChangeset()).isEqualTo(mapOf( "property1", "value1" )); verifyRedundantRelations(); } @Test public void reset() { final String docId = randomUUID().toString(); final ObjectVersionGraph<String> originalGraph = graphForOptimization(); AbstractVersionStoreJdbc<String, String, ?, ?, ?> store = getStore(); addVersions(docId, store, originalGraph.getVersionNode(rev1), originalGraph.getVersionNode(rev2), originalGraph.getVersionNode(rev3), originalGraph.getVersionNode(rev4), originalGraph.getVersionNode(rev5), originalGraph.getVersionNode(rev6)); optimize(docId, v -> v.revision.equals(rev6), store); ObjectVersionGraph<String> graph = store.getOptimizedGraph(docId); assertThat(graph.contains(rev6)).isTrue(); assertThat(graph.size()).isEqualTo(1); store.reset(docId); graph = store.getOptimizedGraph(docId); assertThat(graph.getVersionNode(rev1).getVersion()).isEqualTo(originalGraph.getVersionNode(rev1).getVersion()); assertThat(graph.getVersionNode(rev2).getVersion()).isEqualTo(originalGraph.getVersionNode(rev2).getVersion()); assertThat(graph.getVersionNode(rev3).getVersion()).isEqualTo(originalGraph.getVersionNode(rev3).getVersion()); assertThat(graph.getVersionNode(rev4).getVersion()).isEqualTo(originalGraph.getVersionNode(rev4).getVersion()); assertThat(graph.getVersionNode(rev5).getVersion()).isEqualTo(originalGraph.getVersionNode(rev5).getVersion()); assertThat(graph.getVersionNode(rev6).getVersion()).isEqualTo(originalGraph.getVersionNode(rev6).getVersion()); } @Test public void synchronous_publishing() { final String docId = randomUUID().toString(); VersionStore<String, String> store = newStore(getStore().options.toBuilder().publisherType(SYNC).build()); ObjectVersionGraph<String> graph = ObjectVersionGraph.init(ObjectVersion.<String>builder(rev1).build()); addVersions(docId, store, graph.getVersionNode(rev1)); // getGraphs(Collection) returns published documents GraphResults<String, String> results = store.getGraphs(asList(docId)); graph = results.getVersionGraph(docId); assertThat(graph.getVersionNode(rev1).getRevision()).isEqualTo(rev1); } @Test public void asynchronous_publishing() throws InterruptedException { final String docId = randomUUID().toString(); AbstractVersionStoreJdbc<String, String, ?, ?, ?> originalStore = getStore(); StoreOptions<String, String, ?> options = originalStore.options.toBuilder().publisherType(ASYNC).build(); /* * Override doPublish to block publish until it is verified that inserted version is not returned */ CountDownLatch beforePublish = new CountDownLatch(1); CountDownLatch afterPublish = new CountDownLatch(1); MethodInterceptor interceptor = (Object o, Method method, Object[] args, MethodProxy methodProxy) -> { if (method.getName().equals("publish")) { beforePublish.await(); try { return methodProxy.invokeSuper(o, args); } finally { afterPublish.countDown(); } } return methodProxy.invokeSuper(o, args); }; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(originalStore.getClass()); enhancer.setCallback(interceptor); VersionStore<String, String> store = (VersionStore<String, String>) enhancer.create(new Class[] { options.getClass() }, new Object[] { options }); final ObjectVersionGraph<String> originalGraph = ObjectVersionGraph.init(ObjectVersion.<String>builder(rev1).build()); transactionTemplate.execute(status -> { UpdateBatch<String, String> batch = store.updateBatch(docId); batch.addVersion(docId, originalGraph.getVersionNode(rev1)); batch.execute(); return null; }); // getGraphs(Collection) should not return version before it's published GraphResults<String, String> results = store.getGraphs(asList(docId)); assertThat(results.isEmpty()).isTrue(); // Publish and verify that published version is found beforePublish.countDown(); afterPublish.await(); results = store.getGraphs(asList(docId)); ObjectVersionGraph<String> graph = results.getVersionGraph(docId); assertThat(graph.getVersionNode(rev1).getRevision()).isEqualTo(rev1); } @Test public void automatic_optimization() { final AtomicInteger optimizationRuns = new AtomicInteger(0); final Executor optimizer = runnable -> { optimizationRuns.incrementAndGet(); runnable.run(); }; final GraphOptions<String, String> graphOptions = keepHeadsAndNewest(0, 2); final VersionStore<String, String> store = newStore(getStore().options.toBuilder().optimizer(optimizer).graphOptions(graphOptions).build()); final String docId = randomUUID().toString(); final ObjectVersionGraph<String> originalGraph = graphForOptimization(); addVersions(docId, store, originalGraph.getVersionNode(rev1), originalGraph.getVersionNode(rev2), originalGraph.getVersionNode(rev3)); // First time loads full graph and runs optimization in background assertThat(store.getOptimizedGraph(docId).size()).isEqualTo(3); assertThat(optimizationRuns.get()).isEqualTo(1); // Second time returns newly optimized graph and doesn't rerun optimization assertThat(store.getOptimizedGraph(docId).size()).isEqualTo(1); assertThat(optimizationRuns.get()).isEqualTo(1); addVersions(docId, store, originalGraph.getVersionNode(rev4), originalGraph.getVersionNode(rev6)); // Return updated previous optimization directly and trigger optimization assertThat(store.getOptimizedGraph(docId).size()).isEqualTo(3); assertThat(optimizationRuns.get()).isEqualTo(2); // Return newly optimized assertThat(store.getOptimizedGraph(docId).size()).isEqualTo(1); assertThat(optimizationRuns.get()).isEqualTo(2); // Adding a version referring to squashed parent, returns the full graph and reruns optimization in background addVersions(docId, store, originalGraph.getVersionNode(rev5)); assertThat(store.getOptimizedGraph(docId).size()).isEqualTo(6); assertThat(optimizationRuns.get()).isEqualTo(3); assertThat(store.getOptimizedGraph(docId).size()).isEqualTo(3); assertThat(optimizationRuns.get()).isEqualTo(3); } @Test public void allow_cglib_proxy() { final String docId = "docId"; final Revision revision = new Revision(); Function<ObjectVersionGraph<String>, Predicate<VersionNode<PropertyPath, Object, String>>> keep = g -> n -> false; Map<String, List<Object>> interceptedCalls = new HashMap<>(); MethodInterceptor interceptor = (Object o, Method method, Object[] args, MethodProxy methodProxy) -> { interceptedCalls.put(method.getName(), ImmutableList.copyOf(args)); return null; }; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(getStore().getClass()); enhancer.setCallback(interceptor); @SuppressWarnings("unchecked") VersionStore<String, String> store = (VersionStore<String, String>) enhancer.create(); store.publish(); assertThat(interceptedCalls.get("publish")).isEqualTo(asList()); store.reset(docId); assertThat(interceptedCalls.get("reset")).isEqualTo(asList(docId)); store.getOptimizedGraph(docId); assertThat(interceptedCalls.get("getOptimizedGraph")).isEqualTo(asList(docId)); store.getFullGraph(docId); assertThat(interceptedCalls.get("getFullGraph")).isEqualTo(asList(docId)); store.getGraphs(asList(docId)); assertThat(interceptedCalls.get("getGraphs")).isEqualTo(asList(asList(docId))); store.fetchUpdates(docId, revision); assertThat(interceptedCalls.get("fetchUpdates")).isEqualTo(asList(docId, revision)); store.prune(docId, keep); assertThat(interceptedCalls.get("prune")).isEqualTo(asList(docId, keep)); store.optimize(docId, keep); assertThat(interceptedCalls.get("optimize")).isEqualTo(asList(docId, keep)); store.updateBatch(docId); assertThat(interceptedCalls.get("updateBatch")).isEqualTo(asList(docId)); store.updateBatch(asList(docId)); assertThat(interceptedCalls.get("updateBatch")).isEqualTo(asList(asList(docId))); } @Test public void auto_refresh_published_values() { String docId = randomUUID().toString(); // Non-refreshing cache StoreOptions options = getStore().options.toBuilder() .cacheBuilder(guavaCacheBuilder(CacheBuilder.<Object, Object>newBuilder().maximumSize(8))).build(); AbstractVersionStoreJdbc<String, String, ?, ?, ?> store = newStore(options); final ObjectVersionGraph<String> orginalGraph = ObjectVersionGraph.init( ObjectVersion.<String>builder(rev1) .changeset(ImmutableMap.of(ROOT.property("property"), "value1")) .build(), ObjectVersion.<String>builder(rev2) .parents(rev1) .changeset(ImmutableMap.of(ROOT.property("property"), "value2")) .build(), ObjectVersion.<String>builder(rev3) .parents(rev1) .changeset(ImmutableMap.of(ROOT.property("property"), "value3")) .build() ); assertThat(store.getGraph(docId).isEmpty()).isTrue(); transactionTemplate.execute(status -> { store.updateBatch(docId).addVersion(docId, orginalGraph.getVersionNode(rev1)).execute(); store.updateBatch(docId).addVersion(docId, orginalGraph.getVersionNode(rev2)).execute(); return null; }); assertThat(store.getGraph(docId).isEmpty()).isTrue(); store.publish(); assertThat(store.getGraph(docId).size()).isEqualTo(2); // Cached graph is returned after store optimization... store.optimize(docId, graph -> node -> node.revision.equals(rev2)); assertThat(store.getGraph(docId).size()).isEqualTo(2); // ...until document is evicted store.cache.evict(docId); assertThat(store.getGraph(docId).size()).isEqualTo(1); // Fallback to full graph assertThat(store.getGraph(docId, asList(rev1)).size()).isEqualTo(2); // Reset and optimize transactionTemplate.execute(status -> { store.updateBatch(docId).addVersion(docId, orginalGraph.getVersionNode(rev3)).execute(); return null; }); store.optimize(docId, graph -> node -> node.revision.equals(rev3)); } protected void optimize(String docId, Predicate<VersionNode<PropertyPath, Object, String>> keep, AbstractVersionStoreJdbc<String, String, ?, ?, ?> store) { store.optimize(docId, g -> keep); } protected void addVersions(String docId, VersionStore<String, String> store, VersionNode<PropertyPath, Object, String>... versions) { addVersions(docId, store, asList(versions)); } protected void addVersions(String docId, VersionStore<String, String> store, List<VersionNode<PropertyPath, Object, String>> versions) { transactionTemplate.execute(status -> { UpdateBatch<String, String> batch = store.updateBatch(docId); versions.forEach(v -> batch.addVersion(docId, v)); batch.execute(); return null; }); store.publish(); } /** * v1 * | * v2 * | * v3 * / \ * v4 v5 * | * v6 */ protected ObjectVersionGraph<String> graphForOptimization() { ObjectVersion<String> v1 = ObjectVersion.<String>builder(rev1) .changeset(mapOf( // This should ve moved to v3 "property1", "value1", "property2", "value1")) .build(); ObjectVersion<String> v2 = ObjectVersion.<String>builder(rev2) // Toombstones should be removed .changeset(mapOf("property2", null)) .parents(rev1) .build(); ObjectVersion<String> v3 = ObjectVersion.<String>builder(rev3) .parents(rev2) .build(); // This intermediate version should be removed ObjectVersion<String> v4 = ObjectVersion.<String>builder(rev4) .changeset(mapOf( // These should be left as is "property1", "value2", "property2", "value1")) .parents(rev3) .build(); ObjectVersion<String> v5 = ObjectVersion.<String>builder(rev5) // This should be in conflict with v4 .changeset(mapOf("property2", "value2")) .parents(rev3) .build(); ObjectVersion<String> v6 = ObjectVersion.<String>builder(rev6) // This should be replaced with v3 .parents(rev4) .build(); return ObjectVersionGraph.init(v1, v2, v3, v4, v5, v6); } protected abstract void verifyRedundantRelations(); protected abstract AbstractVersionStoreJdbc<String, String, ?, ?, ?> getStore(); protected abstract AbstractVersionStoreJdbc<String, String, ?, ?, ?> newStore(StoreOptions options); }