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