Package co.cask.tephra.persist

Source Code of co.cask.tephra.persist.AbstractTransactionStateStorageTest

/*
* 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.TxConstants;
import co.cask.tephra.metrics.TxMetricsCollector;
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 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 = 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.getWritePointer() > tx3.getWritePointer());
      // tx1 must be visble from tx2, but tx3 and tx4 must not
      Assert.assertTrue(tx2.isVisible(tx1.getWritePointer()));
      Assert.assertFalse(tx2.isVisible(tx3.getWritePointer()));
      Assert.assertFalse(tx2.isVisible(tx4.getWritePointer()));
      // 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.getWritePointer()));
      Assert.assertTrue(tx5.isVisible(tx2.getWritePointer()));
      Assert.assertFalse(tx5.isVisible(tx3.getWritePointer()));
      Assert.assertFalse(tx5.isVisible(tx4.getWritePointer()));
      // 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.getWritePointer() > tx.getWritePointer());
    } 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);

      // 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();
      }
    }
  }

  /**
   * 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));
      } else {
        inProgress.put(startPointer + i,
                       new TransactionManager.InProgressTx(startPointer - 1, currentTime + 300000L));
      }
    }

    // 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;
  }

  /**
   * Generates a number of semi-random {@link TransactionEdit} instances.
   * These are just randomly selected from the possible states, so would not necessarily reflect a real-world
   * distribution.
   *
   * @param numEntries how many entries to generate in the returned list.
   * @return a list of randomly generated transaction log edits.
   */
  private List<TransactionEdit> createRandomEdits(int numEntries) {
    List<TransactionEdit> edits = Lists.newArrayListWithCapacity(numEntries);
    for (int i = 0; i < numEntries; i++) {
      TransactionEdit.State nextType = TransactionEdit.State.values()[random.nextInt(6)];
      long writePointer = Math.abs(random.nextLong());
      switch (nextType) {
        case INPROGRESS:
          edits.add(
            TransactionEdit.createStarted(writePointer, writePointer - 1,
                                          System.currentTimeMillis() + 300000L));
          break;
        case COMMITTING:
          edits.add(TransactionEdit.createCommitting(writePointer, generateChangeSet(10)));
          break;
        case COMMITTED:
          edits.add(TransactionEdit.createCommitted(writePointer, generateChangeSet(10), writePointer + 1,
                                                    random.nextBoolean()));
          break;
        case INVALID:
          edits.add(TransactionEdit.createInvalid(writePointer));
          break;
        case ABORTED:
          edits.add(TransactionEdit.createAborted(writePointer));
          break;
        case MOVE_WATERMARK:
          edits.add(TransactionEdit.createMoveWatermark(writePointer));
          break;
      }
    }
    return edits;
  }
}
TOP

Related Classes of co.cask.tephra.persist.AbstractTransactionStateStorageTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.