All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.omid.transaction.AbstractTransactionManager Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.omid.transaction;

import org.apache.phoenix.thirdparty.com.google.common.base.Function;
import org.apache.phoenix.thirdparty.com.google.common.base.Optional;
import org.apache.phoenix.thirdparty.com.google.common.hash.Hashing;
import org.apache.phoenix.thirdparty.com.google.common.util.concurrent.Futures;
import org.apache.phoenix.thirdparty.com.google.common.util.concurrent.MoreExecutors;
import org.apache.omid.committable.CommitTable;
import org.apache.omid.committable.CommitTable.CommitTimestamp;
import org.apache.omid.metrics.Counter;
import org.apache.omid.metrics.MetricsRegistry;
import org.apache.omid.metrics.Timer;
import org.apache.omid.transaction.Transaction.Status;
import org.apache.omid.tso.client.AbortException;
import org.apache.omid.tso.client.CellId;
import org.apache.omid.tso.client.ConnectionException;
import org.apache.omid.tso.client.ServiceUnavailableException;
import org.apache.omid.tso.client.TSOProtocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.concurrent.ExecutionException;


import static org.apache.omid.metrics.MetricsUtils.name;

/**
 * Omid's base abstract implementation of the {@link TransactionManager} interface.
 *
 * Provides extra methods to allow transaction manager developers to perform
 * different actions before/after the methods exposed by the {@link TransactionManager} interface.
 *
 * So, this abstract class must be extended by particular implementations of
 * transaction managers related to different storage systems (HBase...)
 */
public abstract class AbstractTransactionManager implements TransactionManager {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractTransactionManager.class);

    public interface TransactionFactory {

        AbstractTransaction createTransaction(long transactionId, long epoch, AbstractTransactionManager tm);

    }

    private final PostCommitActions postCommitter;
    protected final TSOProtocol tsoClient;
    protected final CommitTable.Client commitTableClient;
    private final CommitTable.Writer commitTableWriter;
    private final TransactionFactory transactionFactory;

    // Metrics
    private final Timer startTimestampTimer;
    private final Timer commitTimer;
    private final Timer fenceTimer;
    private final Counter committedTxsCounter;
    private final Counter rolledbackTxsCounter;
    private final Counter errorTxsCounter;
    private final Counter invalidatedTxsCounter;

    /**
     * Base constructor
     *
     * @param metrics
     *            instrumentation metrics
     * @param postCommitter
     *            post commit action executor
     * @param tsoClient
     *            a client for accessing functionality of the status oracle
     * @param commitTableClient
     *            a client for accessing functionality of the commit table
     * @param transactionFactory
     *            a transaction factory to create the specific transaction
     *            objects required by the transaction manager being implemented.
     */
    public AbstractTransactionManager(MetricsRegistry metrics,
                                      PostCommitActions postCommitter,
                                      TSOProtocol tsoClient,
                                      CommitTable.Client commitTableClient,
                                      CommitTable.Writer commitTableWriter,
                                      TransactionFactory transactionFactory) {

        this.tsoClient = tsoClient;
        this.postCommitter = postCommitter;
        this.commitTableClient = commitTableClient;
        this.commitTableWriter = commitTableWriter;
        this.transactionFactory = transactionFactory;

        // Metrics configuration
        this.startTimestampTimer = metrics.timer(name("omid", "tm", "hbase", "startTimestamp", "latency"));
        this.commitTimer = metrics.timer(name("omid", "tm", "hbase", "commit", "latency"));
        this.fenceTimer = metrics.timer(name("omid", "tm", "hbase", "fence", "latency"));
        this.committedTxsCounter = metrics.counter(name("omid", "tm", "hbase", "committedTxs"));
        this.rolledbackTxsCounter = metrics.counter(name("omid", "tm", "hbase", "rolledbackTxs"));
        this.errorTxsCounter = metrics.counter(name("omid", "tm", "hbase", "erroredTxs"));
        this.invalidatedTxsCounter = metrics.counter(name("omid", "tm", "hbase", "invalidatedTxs"));

    }

    /**
     * Allows transaction manager developers to perform actions before creating a transaction.
     * @throws TransactionManagerException in case of any issues
     */
    public void preBegin() throws TransactionManagerException {}

    /**
     * @see org.apache.omid.transaction.TransactionManager#begin()
     */
    @Override
    public final Transaction begin() throws TransactionException {

        try {
            preBegin();

            long startTimestamp, epoch;

            // The loop is required for HA scenarios where we get the timestamp
            // but when getting the epoch, the client is connected to a new TSOServer
            // When this happen, the epoch will be larger than the startTimestamp,
            // so we need to start the transaction again. We use the fact that epoch
            // is always smaller or equal to a timestamp, and therefore, we first need
            // to get the timestamp and then the epoch.
            startTimestampTimer.start();
            try {
                do {
                    startTimestamp = tsoClient.getNewStartTimestamp().get();
                    epoch = tsoClient.getEpoch();
                } while (epoch > startTimestamp);
            } finally {
                startTimestampTimer.stop();
            }

            AbstractTransaction tx = transactionFactory.createTransaction(startTimestamp, epoch, this);

            postBegin(tx);

            return tx;
        } catch (TransactionManagerException e) {
            throw new TransactionException("An error has occured during PreBegin/PostBegin", e);
        } catch (ExecutionException e) {
            throw new TransactionException("Could not get new timestamp", e);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            throw new TransactionException("Interrupted getting timestamp", ie);
        }
    }

    /**
     * Generates hash ID for table name, this hash is later-on sent to the TSO and used for fencing
     * @param tableName - the table name
     * @return
     */
    abstract public long getHashForTable(byte[] tableName);

    /**
     * Return the commit table client
     * @return commitTableClient
     */
    public CommitTable.Client getCommitTableClient() {
        return commitTableClient;
    }

    /**
     * @see org.apache.omid.transaction.TransactionManager#fence(byte[])
     */
    @Override
    public final Transaction fence(byte[] tableName) throws TransactionException {
        long fenceTimestamp;
        long tableID = getHashForTable(tableName); Hashing.murmur3_128().newHasher().putBytes(tableName).hash().asLong();

        try {
            fenceTimer.start();
            try {
                fenceTimestamp = tsoClient.getFence(tableID).get();
            } finally {
                fenceTimer.stop();
            }

            AbstractTransaction tx = transactionFactory.createTransaction(fenceTimestamp, fenceTimestamp, this);

            return tx;
        } catch (ExecutionException e) {
            throw new TransactionException("Could not get fence", e);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            throw new TransactionException("Interrupted creating a fence", ie);
        }
    }

    /**
     * Allows transaction manager developers to perform actions after having started a transaction.
     * @param transaction
     *            the transaction that was just created.
     * @throws TransactionManagerException  in case of any issues
     */
    public void postBegin(AbstractTransaction transaction) throws TransactionManagerException {}

    /**
     * Allows transaction manager developers to perform actions before committing a transaction.
     * @param transaction
     *            the transaction that is going to be committed.
     * @throws TransactionManagerException  in case of any issues
     */
    public void preCommit(AbstractTransaction transaction) throws TransactionManagerException {}

    /**
     * @see org.apache.omid.transaction.TransactionManager#commit(Transaction)
     */
    @Override
    public final void commit(Transaction transaction) throws RollbackException, TransactionException {

        AbstractTransaction tx = enforceAbstractTransactionAsParam(transaction);
        enforceTransactionIsInRunningState(tx);

        if (tx.isRollbackOnly()) { // Manage explicit user rollback
            rollback(tx);
            throw new RollbackException(tx + ": Tx was set to rollback explicitly");
        }

        try {

            preCommit(tx);

            commitTimer.start();
            try {
                if (tx.getWriteSet().isEmpty() && tx.getConflictFreeWriteSet().isEmpty()) {
                    markReadOnlyTransaction(tx); // No need for read-only transactions to contact the TSO Server
                } else {
                    if (tsoClient.isLowLatency())
                        commitLowLatencyTransaction(tx);
                    else
                        commitRegularTransaction(tx);
                }
                committedTxsCounter.inc();
            } finally {
                commitTimer.stop();
            }

            postCommit(tx);

        } catch (TransactionManagerException e) {
            throw new TransactionException(e.getMessage(), e);
        }

    }

    /**
     * Allows transaction manager developers to perform actions after committing a transaction.
     * @param transaction
     *            the transaction that was committed.
     * @throws TransactionManagerException in case of any issues
     */
    public void postCommit(AbstractTransaction transaction) throws TransactionManagerException {}

    /**
     * Allows transaction manager developers to perform actions before rolling-back a transaction.
     * @param transaction the transaction that is going to be rolled-back.
     * @throws TransactionManagerException in case of any issues
     */
    public void preRollback(AbstractTransaction transaction) throws TransactionManagerException {}

    /**
     * @see org.apache.omid.transaction.TransactionManager#rollback(Transaction)
     */
    @Override
    public final void rollback(Transaction transaction) throws TransactionException {

        AbstractTransaction tx = enforceAbstractTransactionAsParam(transaction);
        enforceTransactionIsInRunningState(tx);

        try {

            preRollback(tx);

            // Make sure its commit timestamp is 0, so the cleanup does the right job
            tx.setCommitTimestamp(0);
            tx.setStatus(Status.ROLLEDBACK);

            postRollback(tx);

        } catch (TransactionManagerException e) {
            throw new TransactionException(e.getMessage(), e);
        } finally {
            tx.cleanup();
        }

    }

    /**
     * Allows transaction manager developers to perform actions after rolling-back a transaction.
     * @param transaction
     *            the transaction that was rolled-back.
     * @throws TransactionManagerException in case of any issues
     */
    public void postRollback(AbstractTransaction transaction) throws TransactionManagerException {}


    protected abstract void closeResources() throws IOException;

    /**
     * @see java.io.Closeable#close()
     */
    @Override
    public final void close() throws IOException {
        tsoClient.close();
        closeResources();
    }

    // ----------------------------------------------------------------------------------------------------------------
    // Helper methods
    // ----------------------------------------------------------------------------------------------------------------

    private void enforceTransactionIsInRunningState(Transaction transaction) {

        if (transaction.getStatus() != Status.RUNNING) {
            throw new IllegalArgumentException("Transaction was already " + transaction.getStatus());
        }

    }

    @SuppressWarnings("unchecked")
    // NOTE: We are sure that tx is not parametrized
    private AbstractTransaction enforceAbstractTransactionAsParam(Transaction tx) {

        if (tx instanceof AbstractTransaction) {
            return (AbstractTransaction) tx;
        } else {
            throw new IllegalArgumentException(
                    "The transaction object passed is not an instance of AbstractTransaction");
        }

    }

    private void markReadOnlyTransaction(AbstractTransaction readOnlyTx) {

        readOnlyTx.setStatus(Status.COMMITTED_RO);

    }

    private void commitLowLatencyTransaction(AbstractTransaction tx)
            throws RollbackException, TransactionException {
        try {

            long commitTs = tsoClient.commit(tx.getStartTimestamp(), tx.getWriteSet(), tx.getConflictFreeWriteSet()).get();
            boolean committed = commitTableWriter.atomicAddCommittedTransaction(tx.getStartTimestamp(),commitTs);
            if (!committed) {
                // Transaction has been invalidated by other client
                rollback(tx);
                commitTableClient.deleteCommitEntry(tx.getStartTimestamp());
                rolledbackTxsCounter.inc();
                throw new RollbackException("Transaction " + tx.getTransactionId() + " got invalidated");
            }
            certifyCommitForTx(tx, commitTs);
            updateShadowCellsAndRemoveCommitTableEntry(tx, postCommitter);

        } catch (ExecutionException e) {
            if (e.getCause() instanceof AbortException) { // TSO reports Tx conflicts as AbortExceptions in the future
                rollback(tx);
                rolledbackTxsCounter.inc();
                throw new RollbackException(tx.getStartTimestamp() + ": Conflicts detected in writeset", e.getCause());
            }

            if (e.getCause() instanceof ServiceUnavailableException || e.getCause() instanceof ConnectionException) {
                errorTxsCounter.inc();
                rollback(tx); // Rollback proactively cause it's likely that a new TSOServer is now master
                throw new RollbackException(tx.getStartTimestamp() + " rolled-back precautionary", e.getCause());
            } else {
                throw new TransactionException(tx.getStartTimestamp() + ": cannot determine Tx outcome", e.getCause());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void commitRegularTransaction(AbstractTransaction tx)
            throws RollbackException, TransactionException
    {

        try {

            long commitTs = tsoClient.commit(tx.getStartTimestamp(), tx.getWriteSet(), tx.getConflictFreeWriteSet()).get();
            certifyCommitForTx(tx, commitTs);
            updateShadowCellsAndRemoveCommitTableEntry(tx, postCommitter);

        } catch (ExecutionException e) {

            if (e.getCause() instanceof AbortException) { // TSO reports Tx conflicts as AbortExceptions in the future
                rollback(tx);
                rolledbackTxsCounter.inc();
                throw new RollbackException(tx.getStartTimestamp() + ": Conflicts detected in writeset", e.getCause());
            }

            if (e.getCause() instanceof ServiceUnavailableException || e.getCause() instanceof ConnectionException) {

                errorTxsCounter.inc();
                try {
                    LOG.warn("Can't contact the TSO for receiving outcome for Tx {}. Checking Commit Table...", tx.getStartTimestamp());
                    // Check the commit table to find if the target TSO woke up in the meantime and added the commit
                    // TODO: Decide what we should we do if we can not contact the commit table
                    Optional commitTimestamp =
                            commitTableClient.getCommitTimestamp(tx.getStartTimestamp()).get();
                    if (commitTimestamp.isPresent()) {
                        if (commitTimestamp.get().isValid()) {
                            LOG.warn("{}: Valid commit TS found in Commit Table. Committing Tx...", tx.getStartTimestamp());
                            certifyCommitForTx(tx, commitTimestamp.get().getValue());
                            postCommitter.updateShadowCells(tx); // But do NOT remove transaction from commit table
                        } else { // Probably another Tx in a new TSO Server invalidated this transaction
                            LOG.warn("{}: Invalidated commit TS found in Commit Table. Rolling-back...", tx.getStartTimestamp());
                            rollback(tx);
                            throw new RollbackException(tx.getStartTimestamp() + " invalidated by other Tx started", e.getCause());
                        }
                    } else {
                        LOG.warn("{}: Trying to invalidate Tx proactively in Commit Table...", tx.getStartTimestamp());
                        boolean invalidated = commitTableClient.tryInvalidateTransaction(tx.getStartTimestamp()).get();
                        if (invalidated) {
                            LOG.warn("{}: Invalidated proactively in Commit Table. Rolling-back Tx...", tx.getStartTimestamp());
                            invalidatedTxsCounter.inc();
                            rollback(tx); // Rollback proactively cause it's likely that a new TSOServer is now master
                            throw new RollbackException(tx.getStartTimestamp() + " rolled-back precautionary", e.getCause());
                        } else {
                            LOG.warn("{}: Invalidation could NOT be completed. Re-checking Commit Table...", tx.getStartTimestamp());
                            // TODO: Decide what we should we do if we can not contact the commit table
                            commitTimestamp = commitTableClient.getCommitTimestamp(tx.getStartTimestamp()).get();
                            if (commitTimestamp.isPresent() && commitTimestamp.get().isValid()) {
                                LOG.warn("{}: Valid commit TS found in Commit Table. Committing Tx...", tx.getStartTimestamp());
                                certifyCommitForTx(tx, commitTimestamp.get().getValue());
                                postCommitter.updateShadowCells(tx); // But do NOT remove transaction from commit table
                            } else {
                                LOG.error("{}: Can't determine Transaction outcome", tx.getStartTimestamp());
                                throw new TransactionException(tx.getStartTimestamp() + ": cannot determine Tx outcome");
                            }
                        }
                    }
                } catch (ExecutionException e1) {
                    throw new TransactionException(tx.getStartTimestamp() + ": problem reading commitTS from Commit Table", e1);
                } catch (InterruptedException e1) {
                    Thread.currentThread().interrupt();
                    throw new TransactionException(tx.getStartTimestamp() + ": interrupted while reading commitTS from Commit Table", e1);
                }
            } else {
                throw new TransactionException(tx.getStartTimestamp() + ": cannot determine Tx outcome", e.getCause());
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            throw new TransactionException(tx.getStartTimestamp() + ": interrupted during commit", ie);

        }

    }

    private void updateShadowCellsAndRemoveCommitTableEntry(final AbstractTransaction tx,
                                                            final PostCommitActions postCommitter) {

        Futures.transform(postCommitter.updateShadowCells(tx), new Function() {
            @Override
            public Void apply(Void aVoid) {
                postCommitter.removeCommitTableEntry(tx);
                return null;
            }
        }, MoreExecutors.directExecutor());

    }

    private void certifyCommitForTx(AbstractTransaction txToSetup, long commitTS) {

        txToSetup.setStatus(Status.COMMITTED);
        txToSetup.setCommitTimestamp(commitTS);

    }

    public boolean isLowLatency() {
        return tsoClient.isLowLatency();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy