co.cask.tephra.persist.AbstractTransactionStateStorageTest.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.tephra.persist.AbstractTransactionStateStorageTest.java

Source

/*
 * Copyright  2012-2014 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.tephra.persist;

import co.cask.tephra.ChangeId;
import co.cask.tephra.Transaction;
import co.cask.tephra.TransactionManager;
import co.cask.tephra.TransactionType;
import co.cask.tephra.TxConstants;
import co.cask.tephra.metrics.TxMetricsCollector;
import co.cask.tephra.util.TransactionEditUtil;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.hadoop.conf.Configuration;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

/**
 * Commons tests to run against the {@link TransactionStateStorage} implementations.
 */
public abstract class AbstractTransactionStateStorageTest {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractTransactionStateStorageTest.class);
    private static Random random = new Random();

    protected abstract Configuration getConfiguration(String testName) throws IOException;

    protected abstract AbstractTransactionStateStorage getStorage(Configuration conf);

    @Test
    public void testSnapshotPersistence() throws Exception {
        Configuration conf = getConfiguration("testSnapshotPersistence");

        TransactionSnapshot snapshot = createRandomSnapshot();
        TransactionStateStorage storage = getStorage(conf);
        try {
            storage.startAndWait();
            storage.writeSnapshot(snapshot);

            TransactionSnapshot readSnapshot = storage.getLatestSnapshot();
            assertNotNull(readSnapshot);
            assertEquals(snapshot, readSnapshot);
        } finally {
            storage.stopAndWait();
        }
    }

    @Test
    public void testLogWriteAndRead() throws Exception {
        Configuration conf = getConfiguration("testLogWriteAndRead");

        // create some random entries
        List<TransactionEdit> edits = TransactionEditUtil.createRandomEdits(100);
        TransactionStateStorage storage = getStorage(conf);
        try {
            long now = System.currentTimeMillis();
            storage.startAndWait();
            TransactionLog log = storage.createLog(now);
            for (TransactionEdit edit : edits) {
                log.append(edit);
            }
            log.close();

            Collection<TransactionLog> logsToRead = storage.getLogsSince(now);
            // should only be our one log
            assertNotNull(logsToRead);
            assertEquals(1, logsToRead.size());
            TransactionLogReader logReader = logsToRead.iterator().next().getReader();
            assertNotNull(logReader);

            List<TransactionEdit> readEdits = Lists.newArrayListWithExpectedSize(edits.size());
            TransactionEdit nextEdit;
            while ((nextEdit = logReader.next()) != null) {
                readEdits.add(nextEdit);
            }
            logReader.close();
            assertEquals(edits.size(), readEdits.size());
            for (int i = 0; i < edits.size(); i++) {
                LOG.info("Checking edit " + i);
                assertEquals(edits.get(i), readEdits.get(i));
            }
        } finally {
            storage.stopAndWait();
        }
    }

    @Test
    public void testTransactionManagerPersistence() throws Exception {
        Configuration conf = getConfiguration("testTransactionManagerPersistence");
        conf.setInt(TxConstants.Manager.CFG_TX_CLEANUP_INTERVAL, 0); // no cleanup thread
        // start snapshot thread, but with long enough interval so we only get snapshots on shutdown
        conf.setInt(TxConstants.Manager.CFG_TX_SNAPSHOT_INTERVAL, 600);

        TransactionStateStorage storage = null;
        TransactionStateStorage storage2 = null;
        TransactionStateStorage storage3 = null;
        try {
            storage = getStorage(conf);
            TransactionManager txManager = new TransactionManager(conf, storage, new TxMetricsCollector());
            txManager.startAndWait();

            // TODO: replace with new persistence tests
            final byte[] a = { 'a' };
            final byte[] b = { 'b' };
            // start a tx1, add a change A and commit
            Transaction tx1 = txManager.startShort();
            Assert.assertTrue(txManager.canCommit(tx1, Collections.singleton(a)));
            Assert.assertTrue(txManager.commit(tx1));
            // start a tx2 and add a change B
            Transaction tx2 = txManager.startShort();
            Assert.assertTrue(txManager.canCommit(tx2, Collections.singleton(b)));
            // start a tx3
            Transaction tx3 = txManager.startShort();
            // restart
            txManager.stopAndWait();
            TransactionSnapshot origState = txManager.getCurrentState();
            LOG.info("Orig state: " + origState);

            Thread.sleep(100);
            // starts a new tx manager
            storage2 = getStorage(conf);
            txManager = new TransactionManager(conf, storage2, new TxMetricsCollector());
            txManager.startAndWait();

            // check that the reloaded state matches the old
            TransactionSnapshot newState = txManager.getCurrentState();
            LOG.info("New state: " + newState);
            assertEquals(origState, newState);

            // commit tx2
            Assert.assertTrue(txManager.commit(tx2));
            // start another transaction, must be greater than tx3
            Transaction tx4 = txManager.startShort();
            Assert.assertTrue(tx4.getTransactionId() > tx3.getTransactionId());
            // tx1 must be visble from tx2, but tx3 and tx4 must not
            Assert.assertTrue(tx2.isVisible(tx1.getTransactionId()));
            Assert.assertFalse(tx2.isVisible(tx3.getTransactionId()));
            Assert.assertFalse(tx2.isVisible(tx4.getTransactionId()));
            // add same change for tx3
            Assert.assertFalse(txManager.canCommit(tx3, Collections.singleton(b)));
            // check visibility with new xaction
            Transaction tx5 = txManager.startShort();
            Assert.assertTrue(tx5.isVisible(tx1.getTransactionId()));
            Assert.assertTrue(tx5.isVisible(tx2.getTransactionId()));
            Assert.assertFalse(tx5.isVisible(tx3.getTransactionId()));
            Assert.assertFalse(tx5.isVisible(tx4.getTransactionId()));
            // can commit tx3?
            txManager.abort(tx3);
            txManager.abort(tx4);
            txManager.abort(tx5);
            // start new tx and verify its exclude list is empty
            Transaction tx6 = txManager.startShort();
            Assert.assertFalse(tx6.hasExcludes());
            txManager.abort(tx6);

            // now start 5 x claim size transactions
            Transaction tx = txManager.startShort();
            for (int i = 1; i < 50; i++) {
                tx = txManager.startShort();
            }
            origState = txManager.getCurrentState();

            Thread.sleep(100);
            // simulate crash by starting a new tx manager without a stopAndWait
            storage3 = getStorage(conf);
            txManager = new TransactionManager(conf, storage3, new TxMetricsCollector());
            txManager.startAndWait();

            // verify state again matches (this time should include WAL replay)
            newState = txManager.getCurrentState();
            assertEquals(origState, newState);

            // get a new transaction and verify it is greater
            Transaction txAfter = txManager.startShort();
            Assert.assertTrue(txAfter.getTransactionId() > tx.getTransactionId());
        } finally {
            if (storage != null) {
                storage.stopAndWait();
            }
            if (storage2 != null) {
                storage2.stopAndWait();
            }
            if (storage3 != null) {
                storage3.stopAndWait();
            }
        }
    }

    /**
     * Tests whether the committed set is advanced properly on WAL replay.
     */
    @Test
    public void testCommittedSetClearing() throws Exception {
        Configuration conf = getConfiguration("testCommittedSetClearing");
        conf.setInt(TxConstants.Manager.CFG_TX_CLEANUP_INTERVAL, 0); // no cleanup thread
        conf.setInt(TxConstants.Manager.CFG_TX_SNAPSHOT_INTERVAL, 0); // no periodic snapshots

        TransactionStateStorage storage1 = null;
        TransactionStateStorage storage2 = null;
        try {
            storage1 = getStorage(conf);
            TransactionManager txManager = new TransactionManager(conf, storage1, new TxMetricsCollector());
            txManager.startAndWait();

            // TODO: replace with new persistence tests
            final byte[] a = { 'a' };
            final byte[] b = { 'b' };
            // start a tx1, add a change A and commit
            Transaction tx1 = txManager.startShort();
            Assert.assertTrue(txManager.canCommit(tx1, Collections.singleton(a)));
            Assert.assertTrue(txManager.commit(tx1));
            // start a tx2 and add a change B
            Transaction tx2 = txManager.startShort();
            Assert.assertTrue(txManager.canCommit(tx2, Collections.singleton(b)));
            // start a tx3
            Transaction tx3 = txManager.startShort();
            TransactionSnapshot origState = txManager.getCurrentState();
            LOG.info("Orig state: " + origState);

            // simulate a failure by starting a new tx manager without stopping first
            storage2 = getStorage(conf);
            txManager = new TransactionManager(conf, storage2, new TxMetricsCollector());
            txManager.startAndWait();

            // check that the reloaded state matches the old
            TransactionSnapshot newState = txManager.getCurrentState();
            LOG.info("New state: " + newState);
            assertEquals(origState, newState);

        } finally {
            if (storage1 != null) {
                storage1.stopAndWait();
            }
            if (storage2 != null) {
                storage2.stopAndWait();
            }
        }
    }

    /**
     * Tests removal of old snapshots and old transaction logs.
     */
    @Test
    public void testOldFileRemoval() throws Exception {
        Configuration conf = getConfiguration("testOldFileRemoval");
        TransactionStateStorage storage = null;
        try {
            storage = getStorage(conf);
            storage.startAndWait();
            long now = System.currentTimeMillis();
            long writePointer = 1;
            Collection<Long> invalid = Lists.newArrayList();
            NavigableMap<Long, TransactionManager.InProgressTx> inprogress = Maps.newTreeMap();
            Map<Long, Set<ChangeId>> committing = Maps.newHashMap();
            Map<Long, Set<ChangeId>> committed = Maps.newHashMap();
            TransactionSnapshot snapshot = new TransactionSnapshot(now, 0, writePointer++, invalid, inprogress,
                    committing, committed);
            TransactionEdit dummyEdit = TransactionEdit.createStarted(1, 0, Long.MAX_VALUE, TransactionType.SHORT);

            // write snapshot 1
            storage.writeSnapshot(snapshot);
            TransactionLog log = storage.createLog(now);
            log.append(dummyEdit);
            log.close();

            snapshot = new TransactionSnapshot(now + 1, 0, writePointer++, invalid, inprogress, committing,
                    committed);
            // write snapshot 2
            storage.writeSnapshot(snapshot);
            log = storage.createLog(now + 1);
            log.append(dummyEdit);
            log.close();

            snapshot = new TransactionSnapshot(now + 2, 0, writePointer++, invalid, inprogress, committing,
                    committed);
            // write snapshot 3
            storage.writeSnapshot(snapshot);
            log = storage.createLog(now + 2);
            log.append(dummyEdit);
            log.close();

            snapshot = new TransactionSnapshot(now + 3, 0, writePointer++, invalid, inprogress, committing,
                    committed);
            // write snapshot 4
            storage.writeSnapshot(snapshot);
            log = storage.createLog(now + 3);
            log.append(dummyEdit);
            log.close();

            snapshot = new TransactionSnapshot(now + 4, 0, writePointer++, invalid, inprogress, committing,
                    committed);
            // write snapshot 5
            storage.writeSnapshot(snapshot);
            log = storage.createLog(now + 4);
            log.append(dummyEdit);
            log.close();

            snapshot = new TransactionSnapshot(now + 5, 0, writePointer++, invalid, inprogress, committing,
                    committed);
            // write snapshot 6
            storage.writeSnapshot(snapshot);
            log = storage.createLog(now + 5);
            log.append(dummyEdit);
            log.close();

            List<String> allSnapshots = storage.listSnapshots();
            LOG.info("All snapshots: " + allSnapshots);
            assertEquals(6, allSnapshots.size());
            List<String> allLogs = storage.listLogs();
            LOG.info("All logs: " + allLogs);
            assertEquals(6, allLogs.size());

            long oldestKept = storage.deleteOldSnapshots(3);
            assertEquals(now + 3, oldestKept);
            allSnapshots = storage.listSnapshots();
            LOG.info("All snapshots: " + allSnapshots);
            assertEquals(3, allSnapshots.size());

            storage.deleteLogsOlderThan(oldestKept);
            allLogs = storage.listLogs();
            LOG.info("All logs: " + allLogs);
            assertEquals(3, allLogs.size());
        } finally {
            if (storage != null) {
                storage.stopAndWait();
            }
        }
    }

    @Test
    public void testLongTxnEditReplay() throws Exception {
        Configuration conf = getConfiguration("testLongTxnEditReplay");
        TransactionStateStorage storage = null;
        try {
            storage = getStorage(conf);
            storage.startAndWait();

            // Create long running txns. Abort one of them, invalidate another, invalidate and abort the last.
            long time1 = System.currentTimeMillis();
            long wp1 = time1 * TxConstants.MAX_TX_PER_MS;
            TransactionEdit edit1 = TransactionEdit.createStarted(wp1, wp1 - 10, time1 + 100000,
                    TransactionType.LONG);
            TransactionEdit edit2 = TransactionEdit.createAborted(wp1, TransactionType.LONG, null);

            long time2 = time1 + 100;
            long wp2 = time2 * TxConstants.MAX_TX_PER_MS;
            TransactionEdit edit3 = TransactionEdit.createStarted(wp2, wp2 - 10, time2 + 100000,
                    TransactionType.LONG);
            TransactionEdit edit4 = TransactionEdit.createInvalid(wp2);

            long time3 = time1 + 200;
            long wp3 = time3 * TxConstants.MAX_TX_PER_MS;
            TransactionEdit edit5 = TransactionEdit.createStarted(wp3, wp3 - 10, time3 + 100000,
                    TransactionType.LONG);
            TransactionEdit edit6 = TransactionEdit.createInvalid(wp3);
            TransactionEdit edit7 = TransactionEdit.createAborted(wp3, TransactionType.LONG, null);

            // write transaction edits
            TransactionLog log = storage.createLog(time1);
            log.append(edit1);
            log.append(edit2);
            log.append(edit3);
            log.append(edit4);
            log.append(edit5);
            log.append(edit6);
            log.append(edit7);
            log.close();

            // Start transaction manager
            TransactionManager txm = new TransactionManager(conf, storage, new TxMetricsCollector());
            txm.startAndWait();
            try {
                // Verify that all txns are in invalid list.
                TransactionSnapshot snapshot1 = txm.getCurrentState();
                Assert.assertEquals(ImmutableList.of(wp1, wp2, wp3), snapshot1.getInvalid());
                Assert.assertEquals(0, snapshot1.getInProgress().size());
                Assert.assertEquals(0, snapshot1.getCommittedChangeSets().size());
                Assert.assertEquals(0, snapshot1.getCommittedChangeSets().size());
            } finally {
                txm.stopAndWait();
            }
        } finally {
            if (storage != null) {
                storage.stopAndWait();
            }
        }
    }

    @Test
    public void testTruncateInvalidTxEditReplay() throws Exception {
        Configuration conf = getConfiguration("testTruncateInvalidTxEditReplay");
        TransactionStateStorage storage = null;
        try {
            storage = getStorage(conf);
            storage.startAndWait();

            // Create some txns, and invalidate all of them.
            long time1 = System.currentTimeMillis();
            long wp1 = time1 * TxConstants.MAX_TX_PER_MS;
            TransactionEdit edit1 = TransactionEdit.createStarted(wp1, wp1 - 10, time1 + 100000,
                    TransactionType.LONG);
            TransactionEdit edit2 = TransactionEdit.createInvalid(wp1);

            long time2 = time1 + 100;
            long wp2 = time2 * TxConstants.MAX_TX_PER_MS;
            TransactionEdit edit3 = TransactionEdit.createStarted(wp2, wp2 - 10, time2 + 10000,
                    TransactionType.SHORT);
            TransactionEdit edit4 = TransactionEdit.createInvalid(wp2);

            long time3 = time1 + 2000;
            long wp3 = time3 * TxConstants.MAX_TX_PER_MS;
            TransactionEdit edit5 = TransactionEdit.createStarted(wp3, wp3 - 10, time3 + 100000,
                    TransactionType.LONG);
            TransactionEdit edit6 = TransactionEdit.createInvalid(wp3);

            long time4 = time1 + 2100;
            long wp4 = time4 * TxConstants.MAX_TX_PER_MS;
            TransactionEdit edit7 = TransactionEdit.createStarted(wp4, wp4 - 10, time4 + 10000,
                    TransactionType.SHORT);
            TransactionEdit edit8 = TransactionEdit.createInvalid(wp4);

            // remove wp1 and wp3 from invalid list
            TransactionEdit edit9 = TransactionEdit.createTruncateInvalidTx(ImmutableSet.of(wp1, wp3));
            // truncate invalid transactions before time3
            TransactionEdit edit10 = TransactionEdit.createTruncateInvalidTxBefore(time3);

            // write transaction edits
            TransactionLog log = storage.createLog(time1);
            log.append(edit1);
            log.append(edit2);
            log.append(edit3);
            log.append(edit4);
            log.append(edit5);
            log.append(edit6);
            log.append(edit7);
            log.append(edit8);
            log.append(edit9);
            log.append(edit10);
            log.close();

            // Start transaction manager
            TransactionManager txm = new TransactionManager(conf, storage, new TxMetricsCollector());
            txm.startAndWait();
            try {
                // Only wp4 should be in invalid list.
                TransactionSnapshot snapshot = txm.getCurrentState();
                Assert.assertEquals(ImmutableList.of(wp4), snapshot.getInvalid());
                Assert.assertEquals(0, snapshot.getInProgress().size());
                Assert.assertEquals(0, snapshot.getCommittedChangeSets().size());
                Assert.assertEquals(0, snapshot.getCommittedChangeSets().size());
            } finally {
                txm.stopAndWait();
            }
        } finally {
            if (storage != null) {
                storage.stopAndWait();
            }
        }
    }

    /**
     * Generates a new snapshot object with semi-randomly populated values.  This does not necessarily accurately
     * represent a typical snapshot's distribution of values, as we only set an upper bound on pointer values.
     *
     * We generate a new snapshot with the contents:
     * <ul>
     *   <li>readPointer = 1M + (random % 1M)</li>
     *   <li>writePointer = readPointer + 1000</li>
     *   <li>waterMark = writePointer + 1000</li>
     *   <li>inProgress = one each for (writePointer - 500)..writePointer, ~ 5% "long" transaction</li>
     *   <li>invalid = 100 randomly distributed, 0..1M</li>
     *   <li>committing = one each, (readPointer + 1)..(readPointer + 100)</li>
     *   <li>committed = one each, (readPointer - 1000)..readPointer</li>
     * </ul>
     * @return a new snapshot of transaction state.
     */
    private TransactionSnapshot createRandomSnapshot() {
        // limit readPointer to a reasonable range, but make it > 1M so we can assign enough keys below
        long readPointer = (Math.abs(random.nextLong()) % 1000000L) + 1000000L;
        long writePointer = readPointer + 1000L;

        // generate in progress -- assume last 500 write pointer values
        NavigableMap<Long, TransactionManager.InProgressTx> inProgress = Maps.newTreeMap();
        long startPointer = writePointer - 500L;
        for (int i = 0; i < 500; i++) {
            long currentTime = System.currentTimeMillis();
            // make some "long" transactions
            if (i % 20 == 0) {
                inProgress.put(startPointer + i, new TransactionManager.InProgressTx(startPointer - 1,
                        currentTime + TimeUnit.DAYS.toSeconds(1), TransactionType.LONG));
            } else {
                inProgress.put(startPointer + i, new TransactionManager.InProgressTx(startPointer - 1,
                        currentTime + 300000L, TransactionType.SHORT));
            }
        }

        // make 100 random invalid IDs
        LongArrayList invalid = new LongArrayList();
        for (int i = 0; i < 100; i++) {
            invalid.add(Math.abs(random.nextLong()) % 1000000L);
        }

        // make 100 committing entries, 10 keys each
        Map<Long, Set<ChangeId>> committing = Maps.newHashMap();
        for (int i = 0; i < 100; i++) {
            committing.put(readPointer + i, generateChangeSet(10));
        }

        // make 1000 committed entries, 10 keys each
        long startCommitted = readPointer - 1000L;
        NavigableMap<Long, Set<ChangeId>> committed = Maps.newTreeMap();
        for (int i = 0; i < 1000; i++) {
            committed.put(startCommitted + i, generateChangeSet(10));
        }

        return new TransactionSnapshot(System.currentTimeMillis(), readPointer, writePointer, invalid, inProgress,
                committing, committed);
    }

    private Set<ChangeId> generateChangeSet(int numEntries) {
        Set<ChangeId> changes = Sets.newHashSet();
        for (int i = 0; i < numEntries; i++) {
            byte[] bytes = new byte[8];
            random.nextBytes(bytes);
            changes.add(new ChangeId(bytes));
        }
        return changes;
    }
}