/* * Copyright © 2015 Cask Data, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package co.cask.cdap.data2.dataset2.cache; import co.cask.cdap.api.dataset.DatasetManagementException; import co.cask.cdap.api.dataset.DatasetProperties; import co.cask.cdap.data.dataset.SystemDatasetInstantiator; import co.cask.cdap.data2.dataset2.DatasetFramework; import co.cask.cdap.data2.dataset2.DatasetFrameworkTestUtil; import co.cask.cdap.data2.dataset2.DynamicDatasetCache; import co.cask.cdap.data2.metadata.lineage.AccessType; import co.cask.cdap.data2.transaction.Transactions; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.id.NamespaceId; import co.cask.tephra.TransactionAware; import co.cask.tephra.TransactionContext; import co.cask.tephra.TransactionFailureException; import co.cask.tephra.TransactionSystemClient; import co.cask.tephra.inmemory.InMemoryTxSystemClient; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; public abstract class DynamicDatasetCacheTest { @ClassRule public static DatasetFrameworkTestUtil dsFrameworkUtil = new DatasetFrameworkTestUtil(); private static final Map<String, String> ARGUMENTS = ImmutableMap.of("key", "k", "value", "v"); private static final Map<String, String> NO_ARGUMENTS = ImmutableMap.of(); private static final Map<String, String> A_ARGUMENTS = NO_ARGUMENTS; private static final Map<String, String> B_ARGUMENTS = ImmutableMap.of("value", "b"); private static final Map<String, String> C_ARGUMENTS = ImmutableMap.of("key", "c", "value", "c"); private static final Map<String, String> X_ARGUMENTS = ImmutableMap.of("value", "x"); protected static final Id.Namespace NAMESPACE = DatasetFrameworkTestUtil.NAMESPACE_ID; protected static final NamespaceId NAMESPACE_ID = new NamespaceId(NAMESPACE.getId()); protected static DatasetFramework dsFramework; protected static TransactionSystemClient txClient; protected DynamicDatasetCache cache; @BeforeClass public static void init() throws DatasetManagementException, IOException { dsFramework = dsFrameworkUtil.getFramework(); dsFramework.addModule(Id.DatasetModule.from(NAMESPACE, "testDataset"), new TestDatasetModule()); txClient = new InMemoryTxSystemClient(dsFrameworkUtil.getTxManager()); dsFrameworkUtil.createInstance("testDataset", Id.DatasetInstance.from(NAMESPACE, "a"), DatasetProperties.EMPTY); dsFrameworkUtil.createInstance("testDataset", Id.DatasetInstance.from(NAMESPACE, "b"), DatasetProperties.EMPTY); dsFrameworkUtil.createInstance("testDataset", Id.DatasetInstance.from(NAMESPACE, "c"), DatasetProperties.EMPTY); } @AfterClass public static void tearDown() throws IOException, DatasetManagementException { dsFrameworkUtil.deleteInstance(Id.DatasetInstance.from(NAMESPACE, "a")); dsFrameworkUtil.deleteInstance(Id.DatasetInstance.from(NAMESPACE, "b")); dsFrameworkUtil.deleteModule(Id.DatasetModule.from(NAMESPACE, "testDataset")); } @Before public void initCache() { SystemDatasetInstantiator instantiator = new SystemDatasetInstantiator(dsFramework, getClass().getClassLoader(), null); cache = createCache(instantiator, ARGUMENTS, ImmutableMap.of("b", B_ARGUMENTS)); } protected abstract DynamicDatasetCache createCache(SystemDatasetInstantiator instantiator, Map<String, String> arguments, Map<String, Map<String, String>> staticDatasets); @After public void closeCache() { if (cache != null) { cache.close(); } } /** * @param datasetMap if not null, this test method will save the instances of a and b into the map. */ protected void testDatasetCache(@Nullable Map<String, TestDataset> datasetMap) throws IOException, DatasetManagementException, TransactionFailureException { // test that getting the same dataset are always the same object TestDataset a = cache.getDataset("a"); TestDataset a1 = cache.getDataset("a"); TestDataset a2 = cache.getDataset("a", A_ARGUMENTS); Assert.assertSame(a, a1); Assert.assertSame(a, a2); TestDataset b = cache.getDataset("b", B_ARGUMENTS); TestDataset b1 = cache.getDataset("b", B_ARGUMENTS, AccessType.READ); TestDataset b2 = cache.getDataset("b", B_ARGUMENTS, AccessType.WRITE); Assert.assertSame(b, b1); // assert that b1 and b2 are the same, even though their accessType is different Assert.assertSame(b1, b2); // validate that arguments for a are the global runtime args of the cache Assert.assertEquals(2, a.getArguments().size()); Assert.assertEquals("k", a.getKey()); Assert.assertEquals("v", a.getValue()); // validate that arguments for b did override the global runtime args of the cache Assert.assertEquals("k", b.getKey()); Assert.assertEquals("b", b.getValue()); // verify that before a tx context is created, tx-awares is empty List<TestDataset> txAwares = getTxAwares(); Assert.assertTrue(txAwares.isEmpty()); // verify that static datasets are in the tx-awares after start TransactionContext txContext = cache.newTransactionContext(); txAwares = getTxAwares(); Assert.assertEquals(2, txAwares.size()); Assert.assertSame(a, txAwares.get(0)); Assert.assertSame(b, txAwares.get(1)); // verify that the tx-aware is actually part of the tx context and that they are in the same tx txContext.start(); Assert.assertNotNull(a.getCurrentTransaction()); Assert.assertNotEquals(0L, a.getCurrentTransaction().getWritePointer()); Assert.assertEquals(a.getCurrentTransaction(), b.getCurrentTransaction()); // get another dataset, validate that it participates in the same tx TestDataset c = cache.getDataset("c", C_ARGUMENTS); Assert.assertEquals(a.getCurrentTransaction(), c.getCurrentTransaction()); // validate runtime arguments of c // validate that arguments for b did override the global runtime args of the cache Assert.assertEquals("c", c.getKey()); Assert.assertEquals("c", c.getValue()); // validate that c was added to the tx-awares txAwares = getTxAwares(); Assert.assertEquals(3, txAwares.size()); Assert.assertSame(a, txAwares.get(0)); Assert.assertSame(b, txAwares.get(1)); Assert.assertSame(c, txAwares.get(2)); // discard b and c, validate that they are not closed yet cache.discardDataset(b); cache.discardDataset(c); Assert.assertFalse(b.isClosed()); Assert.assertFalse(c.isClosed()); // validate that static dataset b is still in the tx-awares, whereas c was dropped txAwares = getTxAwares(); Assert.assertEquals(2, txAwares.size()); Assert.assertSame(a, txAwares.get(0)); Assert.assertSame(b, txAwares.get(1)); // get b and c again, validate that they are the same TestDataset b3 = cache.getDataset("b", B_ARGUMENTS); TestDataset c1 = cache.getDataset("c", C_ARGUMENTS); Assert.assertSame(b3, b); Assert.assertSame(c1, c); // validate that c is back in the tx-awares txAwares = getTxAwares(); Assert.assertEquals(3, txAwares.size()); Assert.assertSame(a, txAwares.get(0)); Assert.assertSame(b, txAwares.get(1)); Assert.assertSame(c, txAwares.get(2)); // discard b and c, validate that they are not closed yet cache.discardDataset(b); cache.discardDataset(c); Assert.assertFalse(b.isClosed()); Assert.assertFalse(c.isClosed()); // validate that static dataset b is still in the tx-awares, whereas c was dropped txAwares = getTxAwares(); Assert.assertEquals(2, txAwares.size()); Assert.assertSame(a, txAwares.get(0)); Assert.assertSame(b, txAwares.get(1)); // validate that all datasets participate in abort of tx txContext.abort(); Assert.assertNull(a.getCurrentTransaction()); Assert.assertNull(b.getCurrentTransaction()); Assert.assertNull(c.getCurrentTransaction()); // validate that c disappears from the txAwares after tx aborted, and that it was closed txAwares = getTxAwares(); Assert.assertEquals(2, txAwares.size()); Assert.assertSame(a, txAwares.get(0)); Assert.assertSame(b, txAwares.get(1)); Assert.assertTrue(c.isClosed()); // but also that b (a static dataset) remains and was not closed Assert.assertFalse(b.isClosed()); // validate the tx context does not include c in the next transaction txContext.start(); Assert.assertNotNull(a.getCurrentTransaction()); Assert.assertEquals(a.getCurrentTransaction(), b.getCurrentTransaction()); Assert.assertNull(c.getCurrentTransaction()); // validate that discarding a dataset that is not in the tx does not cause errors cache.discardDataset(c); // get a new instance of c, validate that it is not the same as before, that is, c was really discarded c1 = cache.getDataset("c", C_ARGUMENTS); Assert.assertNotSame(c, c1); // discard c and finish the tx cache.discardDataset(c1); txContext.finish(); // verify that after discarding the tx context, tx-awares is empty cache.dismissTransactionContext(); Assert.assertTrue(getTxAwares().isEmpty()); // verify that after getting an new tx-context, we still have the same datasets txContext = cache.newTransactionContext(); txContext.start(); Assert.assertNotNull(txContext.getCurrentTransaction()); Assert.assertEquals(txContext.getCurrentTransaction(), a.getCurrentTransaction()); Assert.assertEquals(txContext.getCurrentTransaction(), b.getCurrentTransaction()); txContext.abort(); if (datasetMap != null) { datasetMap.put("a", a); datasetMap.put("b", b); } } @Test public void testThatDatasetsStayInTransaction() throws TransactionFailureException { final AtomicReference<Object> ref = new AtomicReference<>(); Transactions.execute(cache.newTransactionContext(), "foo", new Runnable() { @Override public void run() { try { // this writes the value "x" to row "key" TestDataset ds = cache.getDataset("a", X_ARGUMENTS); ds.write(); // this would close and discard ds, but the transaction is going on, so we should get the // identical instance of the dataset again cache.discardDataset(ds); TestDataset ds2 = cache.getDataset("a", X_ARGUMENTS); Assert.assertSame(ds, ds2); ref.set(ds); } catch (Exception e) { throw Throwables.propagate(e); } try { // get the same dataset again. It should be the same object TestDataset ds = cache.getDataset("a", X_ARGUMENTS); Assert.assertSame(ref.get(), ds); cache.discardDataset(ds); } catch (Exception e) { throw Throwables.propagate(e); } } }); // now validate that the dataset did participate in the commit of the tx Transactions.execute(cache.newTransactionContext(), "foo", new Runnable() { @Override public void run() { try { TestDataset ds = cache.getDataset("a", X_ARGUMENTS); Assert.assertEquals("x", ds.read()); // validate that we now have a different instance because the old one was discarded Assert.assertNotSame(ref.get(), ds); } catch (Exception e) { throw Throwables.propagate(e); } // validate that only the new instance of the dataset remained active Assert.assertEquals(1, Iterables.size(cache.getTransactionAwares()) - Iterables.size(cache.getStaticTransactionAwares())); } }); } private List<TestDataset> getTxAwares() { SortedSet<TestDataset> set = new TreeSet<>(); for (TransactionAware txAware : cache.getTransactionAwares()) { TestDataset dataset = (TestDataset) txAware; set.add(dataset); } return ImmutableList.copyOf(set); } }