co.cask.tephra.TransactionExecutorTest.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.tephra.TransactionExecutorTest.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;

import co.cask.tephra.inmemory.InMemoryTxSystemClient;
import co.cask.tephra.runtime.ConfigModule;
import co.cask.tephra.runtime.DiscoveryModules;
import co.cask.tephra.runtime.TransactionModules;
import co.cask.tephra.snapshot.DefaultSnapshotCodec;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import com.google.inject.util.Modules;
import org.apache.hadoop.conf.Configuration;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Tests the transaction executor.
 */
public class TransactionExecutorTest {
    private static DummyTxClient txClient;
    private static TransactionExecutorFactory factory;

    @ClassRule
    public static TemporaryFolder tmpFolder = new TemporaryFolder();

    @BeforeClass
    public static void setup() throws IOException {
        final Configuration conf = new Configuration();
        conf.set(TxConstants.Persist.CFG_TX_SNAPHOT_CODEC_CLASSES, DefaultSnapshotCodec.class.getName());
        conf.set(TxConstants.Manager.CFG_TX_SNAPSHOT_DIR, tmpFolder.newFolder().getAbsolutePath());
        Injector injector = Guice.createInjector(new ConfigModule(conf),
                new DiscoveryModules().getInMemoryModules(),
                Modules.override(new TransactionModules().getInMemoryModules()).with(new AbstractModule() {
                    @Override
                    protected void configure() {
                        TransactionManager txManager = new TransactionManager(conf);
                        txManager.startAndWait();
                        bind(TransactionManager.class).toInstance(txManager);
                        bind(TransactionSystemClient.class).to(DummyTxClient.class).in(Singleton.class);
                    }
                }));

        txClient = (DummyTxClient) injector.getInstance(TransactionSystemClient.class);
        factory = injector.getInstance(TransactionExecutorFactory.class);
    }

    final DummyTxAware ds1 = new DummyTxAware(), ds2 = new DummyTxAware();
    final Collection<TransactionAware> txAwares = ImmutableList.<TransactionAware>of(ds1, ds2);

    private TransactionExecutor getExecutor() {
        return factory.createExecutor(txAwares);
    }

    private TransactionExecutor getExecutorWithNoRetry() {
        return new DefaultTransactionExecutor(txClient, txAwares, RetryStrategies.noRetries());
    }

    static final byte[] A = { 'a' };
    static final byte[] B = { 'b' };

    final TransactionExecutor.Function<Integer, Integer> testFunction = new TransactionExecutor.Function<Integer, Integer>() {
        @Override
        public Integer apply(@Nullable Integer input) {
            ds1.addChange(A);
            ds2.addChange(B);
            if (input == null) {
                throw new RuntimeException("function failed");
            }
            return input * input;
        }
    };

    @Before
    public void resetTxAwares() {
        ds1.reset();
        ds2.reset();
    }

    @Test
    public void testSuccessful() throws TransactionFailureException, InterruptedException {
        // execute: add a change to ds1 and ds2
        Integer result = getExecutor().execute(testFunction, 10);
        // verify both are committed and post-committed
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertTrue(ds2.committed);
        Assert.assertTrue(ds1.postCommitted);
        Assert.assertTrue(ds2.postCommitted);
        Assert.assertFalse(ds1.rolledBack);
        Assert.assertFalse(ds2.rolledBack);
        Assert.assertTrue(100 == result);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Committed);
    }

    @Test
    public void testPostCommitFailure() throws TransactionFailureException, InterruptedException {
        ds1.failPostCommitTxOnce = InduceFailure.ThrowException;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, 10);
            Assert.fail("post commit failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertEquals("post failure", e.getCause().getMessage());
        }
        // verify both are committed and post-committed
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertTrue(ds2.committed);
        Assert.assertTrue(ds1.postCommitted);
        Assert.assertTrue(ds2.postCommitted);
        Assert.assertFalse(ds1.rolledBack);
        Assert.assertFalse(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Committed);
    }

    @Test
    public void testPersistFailure() throws TransactionFailureException, InterruptedException {
        ds1.failCommitTxOnce = InduceFailure.ThrowException;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, 10);
            Assert.fail("persist failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertEquals("persist failure", e.getCause().getMessage());
        }
        // verify both are rolled back and tx is aborted
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Aborted);
    }

    @Test
    public void testPersistFalse() throws TransactionFailureException, InterruptedException {
        ds1.failCommitTxOnce = InduceFailure.ReturnFalse;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, 10);
            Assert.fail("persist failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertNull(e.getCause()); // in this case, the ds simply returned false
        }
        // verify both are rolled back and tx is aborted
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Aborted);
    }

    @Test
    public void testPersistAndRollbackFailure() throws TransactionFailureException, InterruptedException {
        ds1.failCommitTxOnce = InduceFailure.ThrowException;
        ds1.failRollbackTxOnce = InduceFailure.ThrowException;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, 10);
            Assert.fail("persist failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertEquals("persist failure", e.getCause().getMessage());
        }
        // verify both are rolled back and tx is invalidated
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Invalidated);
    }

    @Test
    public void testPersistAndRollbackFalse() throws TransactionFailureException, InterruptedException {
        ds1.failCommitTxOnce = InduceFailure.ReturnFalse;
        ds1.failRollbackTxOnce = InduceFailure.ReturnFalse;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, 10);
            Assert.fail("persist failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertNull(e.getCause()); // in this case, the ds simply returned false
        }
        // verify both are rolled back and tx is invalidated
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Invalidated);
    }

    @Test
    public void testNoIndefiniteRetryByDefault() throws TransactionFailureException, InterruptedException {
        // we want retry by default, so that engineers don't miss it
        txClient.failCommits = 1000;
        try {
            // execute: add a change to ds1 and ds2
            getExecutor().execute(testFunction, 10);
            Assert.fail("commit failed too many times to retry - exception should be thrown");
        } catch (TransactionConflictException e) {
            Assert.assertNull(e.getCause());
        }

        txClient.failCommits = 0;

        // verify both are rolled back and tx is aborted
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertTrue(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Aborted);
    }

    @Test
    public void testRetryByDefault() throws TransactionFailureException, InterruptedException {
        // we want retry by default, so that engineers don't miss it
        txClient.failCommits = 2;
        // execute: add a change to ds1 and ds2
        getExecutor().execute(testFunction, 10);
        // should not fail, but continue

        // verify both are committed
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertTrue(ds2.committed);
        Assert.assertTrue(ds1.postCommitted);
        Assert.assertTrue(ds2.postCommitted);
        Assert.assertFalse(ds1.rolledBack);
        Assert.assertFalse(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Committed);
    }

    @Test
    public void testCommitFalse() throws TransactionFailureException, InterruptedException {
        txClient.failCommits = 1;
        // execute: add a change to ds1 and ds2
        try {
            getExecutorWithNoRetry().execute(testFunction, 10);
            Assert.fail("commit failed - exception should be thrown");
        } catch (TransactionConflictException e) {
            Assert.assertNull(e.getCause());
        }
        // verify both are rolled back and tx is aborted
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertTrue(ds1.committed);
        Assert.assertTrue(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Aborted);
    }

    @Test
    public void testCanCommitFalse() throws TransactionFailureException, InterruptedException {
        txClient.failCanCommitOnce = true;
        // execute: add a change to ds1 and ds2
        try {
            getExecutorWithNoRetry().execute(testFunction, 10);
            Assert.fail("commit failed - exception should be thrown");
        } catch (TransactionConflictException e) {
            Assert.assertNull(e.getCause());
        }
        // verify both are rolled back and tx is aborted
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertTrue(ds2.checked);
        Assert.assertFalse(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Aborted);
    }

    @Test
    public void testChangesAndRollbackFailure() throws TransactionFailureException, InterruptedException {
        ds1.failChangesTxOnce = InduceFailure.ThrowException;
        ds1.failRollbackTxOnce = InduceFailure.ThrowException;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, 10);
            Assert.fail("get changes failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertEquals("changes failure", e.getCause().getMessage());
        }
        // verify both are rolled back and tx is invalidated
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertTrue(ds1.checked);
        Assert.assertFalse(ds2.checked);
        Assert.assertFalse(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Invalidated);
    }

    @Test
    public void testFunctionAndRollbackFailure() throws TransactionFailureException, InterruptedException {
        ds1.failRollbackTxOnce = InduceFailure.ReturnFalse;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, null);
            Assert.fail("function failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertEquals("function failed", e.getCause().getMessage());
        }
        // verify both are rolled back and tx is invalidated
        Assert.assertTrue(ds1.started);
        Assert.assertTrue(ds2.started);
        Assert.assertFalse(ds1.checked);
        Assert.assertFalse(ds2.checked);
        Assert.assertFalse(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertTrue(ds1.rolledBack);
        Assert.assertTrue(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Invalidated);
    }

    @Test
    public void testStartAndRollbackFailure() throws TransactionFailureException, InterruptedException {
        ds1.failStartTxOnce = InduceFailure.ThrowException;
        // execute: add a change to ds1 and ds2
        try {
            getExecutor().execute(testFunction, 10);
            Assert.fail("start failed - exception should be thrown");
        } catch (TransactionFailureException e) {
            Assert.assertEquals("start failure", e.getCause().getMessage());
        }
        // verify both are not rolled back and tx is aborted
        Assert.assertTrue(ds1.started);
        Assert.assertFalse(ds2.started);
        Assert.assertFalse(ds1.checked);
        Assert.assertFalse(ds2.checked);
        Assert.assertFalse(ds1.committed);
        Assert.assertFalse(ds2.committed);
        Assert.assertFalse(ds1.postCommitted);
        Assert.assertFalse(ds2.postCommitted);
        Assert.assertFalse(ds1.rolledBack);
        Assert.assertFalse(ds2.rolledBack);
        Assert.assertEquals(txClient.state, DummyTxClient.CommitState.Aborted);
    }

    enum InduceFailure {
        NoFailure, ReturnFalse, ThrowException
    }

    static class DummyTxAware implements TransactionAware {

        Transaction tx;
        boolean started = false;
        boolean committed = false;
        boolean checked = false;
        boolean rolledBack = false;
        boolean postCommitted = false;
        List<byte[]> changes = Lists.newArrayList();

        InduceFailure failStartTxOnce = InduceFailure.NoFailure;
        InduceFailure failChangesTxOnce = InduceFailure.NoFailure;
        InduceFailure failCommitTxOnce = InduceFailure.NoFailure;
        InduceFailure failPostCommitTxOnce = InduceFailure.NoFailure;
        InduceFailure failRollbackTxOnce = InduceFailure.NoFailure;

        void addChange(byte[] key) {
            changes.add(key);
        }

        void reset() {
            tx = null;
            started = false;
            checked = false;
            committed = false;
            rolledBack = false;
            postCommitted = false;
            changes.clear();
        }

        @Override
        public void startTx(Transaction tx) {
            reset();
            started = true;
            this.tx = tx;
            if (failStartTxOnce == InduceFailure.ThrowException) {
                failStartTxOnce = InduceFailure.NoFailure;
                throw new RuntimeException("start failure");
            }
        }

        @Override
        public void updateTx(Transaction tx) {
            this.tx = tx;
        }

        @Override
        public Collection<byte[]> getTxChanges() {
            checked = true;
            if (failChangesTxOnce == InduceFailure.ThrowException) {
                failChangesTxOnce = InduceFailure.NoFailure;
                throw new RuntimeException("changes failure");
            }
            return ImmutableList.copyOf(changes);
        }

        @Override
        public boolean commitTx() throws Exception {
            committed = true;
            if (failCommitTxOnce == InduceFailure.ThrowException) {
                failCommitTxOnce = InduceFailure.NoFailure;
                throw new RuntimeException("persist failure");
            }
            if (failCommitTxOnce == InduceFailure.ReturnFalse) {
                failCommitTxOnce = InduceFailure.NoFailure;
                return false;
            }
            return true;
        }

        @Override
        public void postTxCommit() {
            postCommitted = true;
            if (failPostCommitTxOnce == InduceFailure.ThrowException) {
                failPostCommitTxOnce = InduceFailure.NoFailure;
                throw new RuntimeException("post failure");
            }
        }

        @Override
        public boolean rollbackTx() throws Exception {
            rolledBack = true;
            if (failRollbackTxOnce == InduceFailure.ThrowException) {
                failRollbackTxOnce = InduceFailure.NoFailure;
                throw new RuntimeException("rollback failure");
            }
            if (failRollbackTxOnce == InduceFailure.ReturnFalse) {
                failRollbackTxOnce = InduceFailure.NoFailure;
                return false;
            }
            return true;
        }

        @Override
        public String getTransactionAwareName() {
            return "dummy";
        }
    }

    static class DummyTxClient extends InMemoryTxSystemClient {

        boolean failCanCommitOnce = false;
        int failCommits = 0;

        enum CommitState {
            Started, Committed, Aborted, Invalidated
        }

        CommitState state = CommitState.Started;

        @Inject
        DummyTxClient(TransactionManager txmgr) {
            super(txmgr);
        }

        @Override
        public boolean canCommit(Transaction tx, Collection<byte[]> changeIds)
                throws TransactionNotInProgressException {
            if (failCanCommitOnce) {
                failCanCommitOnce = false;
                return false;
            } else {
                return super.canCommit(tx, changeIds);
            }
        }

        @Override
        public boolean commit(Transaction tx) throws TransactionNotInProgressException {
            if (failCommits-- > 0) {
                return false;
            } else {
                state = CommitState.Committed;
                return super.commit(tx);
            }
        }

        @Override
        public Transaction startLong() {
            state = CommitState.Started;
            return super.startLong();
        }

        @Override
        public Transaction startShort() {
            state = CommitState.Started;
            return super.startShort();
        }

        @Override
        public Transaction startShort(int timeout) {
            state = CommitState.Started;
            return super.startShort(timeout);
        }

        @Override
        public void abort(Transaction tx) {
            state = CommitState.Aborted;
            super.abort(tx);
        }

        @Override
        public boolean invalidate(long tx) {
            state = CommitState.Invalidated;
            return super.invalidate(tx);
        }
    }
}