/**
* 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.hadoop.hive.ql.lockmgr;
import com.google.common.annotations.VisibleForTesting;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.metastore.IMetaStoreClient;
import org.apache.hadoop.hive.ql.io.AcidUtils;
import org.apache.hadoop.hive.ql.plan.HiveOperation;
import org.apache.hadoop.hive.ql.plan.LockDatabaseDesc;
import org.apache.hadoop.hive.ql.plan.LockTableDesc;
import org.apache.hadoop.hive.ql.plan.UnlockDatabaseDesc;
import org.apache.hadoop.hive.ql.plan.UnlockTableDesc;
import org.apache.hive.common.util.ShutdownHookManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.hive.common.JavaUtils;
import org.apache.hadoop.hive.common.ValidTxnList;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.metastore.LockComponentBuilder;
import org.apache.hadoop.hive.metastore.LockRequestBuilder;
import org.apache.hadoop.hive.metastore.api.*;
import org.apache.hadoop.hive.ql.Context;
import org.apache.hadoop.hive.ql.ErrorMsg;
import org.apache.hadoop.hive.ql.QueryPlan;
import org.apache.hadoop.hive.ql.hooks.Entity;
import org.apache.hadoop.hive.ql.hooks.ReadEntity;
import org.apache.hadoop.hive.ql.hooks.WriteEntity;
import org.apache.hadoop.hive.ql.metadata.Hive;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.metadata.Table;
import org.apache.thrift.TException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* An implementation of HiveTxnManager that stores the transactions in the metastore database.
* There should be 1 instance o {@link DbTxnManager} per {@link org.apache.hadoop.hive.ql.session.SessionState}
* with a single thread accessing it at a time, with the exception of {@link #heartbeat()} method.
* The later may (usually will) be called from a timer thread.
* See {@link #getMS()} for more important concurrency/metastore access notes.
*
* Each statement that the TM (transaction manager) should be aware of should belong to a transaction.
* Effectively, that means any statement that has side effects. Exceptions are statements like
* Show Compactions, Show Tables, Use Database foo, etc. The transaction is started either
* explicitly ( via Start Transaction SQL statement from end user - not fully supported) or
* implicitly by the {@link org.apache.hadoop.hive.ql.Driver} (which looks exactly as autoCommit=true
* from end user poit of view). See more at {@link #isExplicitTransaction}.
*/
public final class DbTxnManager extends HiveTxnManagerImpl {
static final private String CLASS_NAME = DbTxnManager.class.getName();
static final private Logger LOG = LoggerFactory.getLogger(CLASS_NAME);
private volatile DbLockManager lockMgr = null;
/**
* The Metastore NEXT_TXN_ID.NTXN_NEXT is initialized to 1; it contains the next available
* transaction id. Thus is 1 is first transaction id.
*/
private volatile long txnId = 0;
/**
* assigns a unique monotonically increasing ID to each statement
* which is part of an open transaction. This is used by storage
* layer (see {@link org.apache.hadoop.hive.ql.io.AcidUtils#deltaSubdir(long, long, int)})
* to keep apart multiple writes of the same data within the same transaction
* Also see {@link org.apache.hadoop.hive.ql.io.AcidOutputFormat.Options}
*/
private int writeId = -1;
/**
* counts number of statements in the current transaction
*/
private int numStatements = 0;
/**
* if {@code true} it means current transaction is started via START TRANSACTION which means it cannot
* include any Operations which cannot be rolled back (drop partition; write to non-acid table).
* If false, it's a single statement transaction which can include any statement. This is not a
* contradiction from the user point of view who doesn't know anything about the implicit txn
* and cannot call rollback (the statement of course can fail in which case there is nothing to
* rollback (assuming the statement is well implemented)).
*
* This is done so that all commands run in a transaction which simplifies implementation and
* allows a simple implementation of multi-statement txns which don't require a lock manager
* capable of deadlock detection. (todo: not fully implemented; elaborate on how this LM works)
*
* Also, critically important, ensuring that everything runs in a transaction assigns an order
* to all operations in the system - needed for replication/DR.
*
* We don't want to allow non-transactional statements in a user demarcated txn because the effect
* of such statement is "visible" immediately on statement completion, but the user may
* issue a rollback but the action of the statement can't be undone (and has possibly already been
* seen by another txn). For example,
* start transaction
* insert into transactional_table values(1);
* insert into non_transactional_table select * from transactional_table;
* rollback
*
* The user would be in for a surprise especially if they are not aware of transactional
* properties of the tables involved.
*
* As a side note: what should the lock manager do with locks for non-transactional resources?
* Should it it release them at the end of the stmt or txn?
* Some interesting thoughts: http://mysqlmusings.blogspot.com/2009/02/mixing-engines-in-transactions.html
*/
private boolean isExplicitTransaction = false;
/**
* To ensure transactions don't nest.
*/
private int startTransactionCount = 0;
// QueryId for the query in current transaction
private String queryId;
// ExecutorService for sending heartbeat to metastore periodically.
private static ScheduledExecutorService heartbeatExecutorService = null;
private ScheduledFuture<?> heartbeatTask = null;
private Runnable shutdownRunner = null;
private static final int SHUTDOWN_HOOK_PRIORITY = 0;
/**
* We do this on every call to make sure TM uses same MS connection as is used by the caller (Driver,
* SemanticAnalyzer, etc). {@code Hive} instances are cached using ThreadLocal and
* {@link IMetaStoreClient} is cached within {@code Hive} with additional logic. Futhermore, this
* ensures that multiple threads are not sharing the same Thrift client (which could happen
* if we had cached {@link IMetaStoreClient} here.
*
* ThreadLocal gets cleaned up automatically when its thread goes away
* https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html. This is especially
* important for threads created by {@link #heartbeatExecutorService} threads.
*
* Embedded {@link DbLockManager} follows the same logic.
* @return IMetaStoreClient
* @throws LockException on any errors
*/
IMetaStoreClient getMS() throws LockException {
try {
return Hive.get(conf).getMSC();
}
catch(HiveException|MetaException e) {
String msg = "Unable to reach Hive Metastore: " + e.getMessage();
LOG.error(msg, e);
throw new LockException(e);
}
}
DbTxnManager() {
shutdownRunner = new Runnable() {
@Override
public void run() {
if (heartbeatExecutorService != null
&& !heartbeatExecutorService.isShutdown()
&& !heartbeatExecutorService.isTerminated()) {
LOG.info("Shutting down Heartbeater thread pool.");
heartbeatExecutorService.shutdown();
}
}
};
ShutdownHookManager.addShutdownHook(shutdownRunner, SHUTDOWN_HOOK_PRIORITY);
}
@Override
void setHiveConf(HiveConf conf) {
super.setHiveConf(conf);
if (!conf.getBoolVar(HiveConf.ConfVars.HIVE_SUPPORT_CONCURRENCY)) {
throw new RuntimeException(ErrorMsg.DBTXNMGR_REQUIRES_CONCURRENCY.getMsg());
}
}
@Override
public long openTxn(Context ctx, String user) throws LockException {
return openTxn(ctx, user, 0);
}
@VisibleForTesting
long openTxn(Context ctx, String user, long delay) throws LockException {
/*Q: why don't we lock the snapshot here??? Instead of having client make an explicit call
whenever it chooses
A: If we want to rely on locks for transaction scheduling we must get the snapshot after lock
acquisition. Relying on locks is a pessimistic strategy which works better under high
contention.*/
init();
getLockManager();
if(isTxnOpen()) {
throw new LockException("Transaction already opened. " + JavaUtils.txnIdToString(txnId));
}
try {
txnId = getMS().openTxn(user);
writeId = 0;
numStatements = 0;
isExplicitTransaction = false;
startTransactionCount = 0;
LOG.debug("Opened " + JavaUtils.txnIdToString(txnId));
ctx.setHeartbeater(startHeartbeat(delay));
return txnId;
} catch (TException e) {
throw new LockException(e, ErrorMsg.METASTORE_COMMUNICATION_FAILED);
}
}
/**
* we don't expect multiple threads to call this method concurrently but {@link #lockMgr} will
* be read by a different threads than one writing it, thus it's {@code volatile}
*/
@Override
public HiveLockManager getLockManager() throws LockException {
init();
if (lockMgr == null) {
lockMgr = new DbLockManager(conf, this);
}
return lockMgr;
}
@Override
public void acquireLocks(QueryPlan plan, Context ctx, String username) throws LockException {
try {
acquireLocksWithHeartbeatDelay(plan, ctx, username, 0);
}
catch(LockException e) {
if(e.getCause() instanceof TxnAbortedException) {
txnId = 0;
writeId = -1;
}
throw e;
}
}
/**
* Watermark to include in error msgs and logs
* @param queryPlan
* @return
*/
private static String getQueryIdWaterMark(QueryPlan queryPlan) {
return "queryId=" + queryPlan.getQueryId();
}
private void markExplicitTransaction(QueryPlan queryPlan) throws LockException {
isExplicitTransaction = true;
if(++startTransactionCount > 1) {
throw new LockException(null, ErrorMsg.OP_NOT_ALLOWED_IN_TXN, queryPlan.getOperationName(),
JavaUtils.txnIdToString(getCurrentTxnId()), queryPlan.getQueryId());
}
}
/**
* Ensures that the current SQL statement is appropriate for the current state of the
* Transaction Manager (e.g. can call commit unless you called start transaction)
*
* Note that support for multi-statement txns is a work-in-progress so it's only supported in
* HiveConf#HIVE_IN_TEST/HiveConf#TEZ_HIVE_IN_TEST.
* @param queryPlan
* @throws LockException
*/
private void verifyState(QueryPlan queryPlan) throws LockException {
if(!isTxnOpen()) {
throw new LockException("No transaction context for operation: " + queryPlan.getOperationName() +
" for " + getQueryIdWaterMark(queryPlan));
}
if(queryPlan.getOperation() == null) {
throw new IllegalStateException("Unkown HiverOperation for " + getQueryIdWaterMark(queryPlan));
}
numStatements++;
switch (queryPlan.getOperation()) {
case START_TRANSACTION:
markExplicitTransaction(queryPlan);
break;
case COMMIT:
case ROLLBACK:
if(!isTxnOpen()) {
throw new LockException(null, ErrorMsg.OP_NOT_ALLOWED_WITHOUT_TXN, queryPlan.getOperationName());
}
if(!isExplicitTransaction) {
throw new LockException(null, ErrorMsg.OP_NOT_ALLOWED_IN_IMPLICIT_TXN, queryPlan.getOperationName());
}
break;
default:
if(!queryPlan.getOperation().isAllowedInTransaction() && isExplicitTransaction) {
//for example, drop table in an explicit txn is not allowed
//in some cases this requires looking at more than just the operation
//for example HiveOperation.LOAD - OK if target is MM table but not OK if non-acid table
throw new LockException(null, ErrorMsg.OP_NOT_ALLOWED_IN_TXN, queryPlan.getOperationName(),
JavaUtils.txnIdToString(getCurrentTxnId()), queryPlan.getQueryId());
}
}
/*
Should we allow writing to non-transactional tables in an explicit transaction? The user may
issue ROLLBACK but these tables won't rollback.
Can do this by checking ReadEntity/WriteEntity to determine whether it's reading/writing
any non acid and raise an appropriate error
* Driver.acidSinks and Driver.acidInQuery can be used if any acid is in the query*/
}
/**
* Normally client should call {@link #acquireLocks(org.apache.hadoop.hive.ql.QueryPlan, org.apache.hadoop.hive.ql.Context, String)}
* @param isBlocking if false, the method will return immediately; thus the locks may be in LockState.WAITING
* @return null if no locks were needed
*/
@VisibleForTesting
LockState acquireLocks(QueryPlan plan, Context ctx, String username, boolean isBlocking) throws LockException {
init();
// Make sure we've built the lock manager
getLockManager();
verifyState(plan);
boolean atLeastOneLock = false;
queryId = plan.getQueryId();
switch (plan.getOperation()) {
case SET_AUTOCOMMIT:
/**This is here for documentation purposes. This TM doesn't support this - only has one
* mode of operation documented at {@link DbTxnManager#isExplicitTransaction}*/
return null;
}
LockRequestBuilder rqstBuilder = new LockRequestBuilder(queryId);
//link queryId to txnId
LOG.info("Setting lock request transaction to " + JavaUtils.txnIdToString(txnId) + " for queryId=" + queryId);
rqstBuilder.setTransactionId(txnId)
.setUser(username);
// For each source to read, get a shared lock
for (ReadEntity input : plan.getInputs()) {
if (!input.needsLock() || input.isUpdateOrDelete() ||
(input.getType() == Entity.Type.TABLE && input.getTable().isTemporary())) {
// We don't want to acquire read locks during update or delete as we'll be acquiring write
// locks instead. Also, there's no need to lock temp tables since they're session wide
continue;
}
LockComponentBuilder compBuilder = new LockComponentBuilder();
compBuilder.setShared();
compBuilder.setOperationType(DataOperationType.SELECT);
Table t = null;
switch (input.getType()) {
case DATABASE:
compBuilder.setDbName(input.getDatabase().getName());
break;
case TABLE:
t = input.getTable();
compBuilder.setDbName(t.getDbName());
compBuilder.setTableName(t.getTableName());
break;
case PARTITION:
case DUMMYPARTITION:
compBuilder.setPartitionName(input.getPartition().getName());
t = input.getPartition().getTable();
compBuilder.setDbName(t.getDbName());
compBuilder.setTableName(t.getTableName());
break;
default:
// This is a file or something we don't hold locks for.
continue;
}
if(t != null) {
compBuilder.setIsAcid(AcidUtils.isAcidTable(t));
}
LockComponent comp = compBuilder.build();
LOG.debug("Adding lock component to lock request " + comp.toString());
rqstBuilder.addLockComponent(comp);
atLeastOneLock = true;
}
// For each source to write to, get the appropriate lock type. If it's
// an OVERWRITE, we need to get an exclusive lock. If it's an insert (no
// overwrite) than we need a shared. If it's update or delete then we
// need a SEMI-SHARED.
for (WriteEntity output : plan.getOutputs()) {
LOG.debug("output is null " + (output == null));
if (output.getType() == Entity.Type.DFS_DIR || output.getType() == Entity.Type.LOCAL_DIR ||
(output.getType() == Entity.Type.TABLE && output.getTable().isTemporary())) {
// We don't lock files or directories. We also skip locking temp tables.
continue;
}
LockComponentBuilder compBuilder = new LockComponentBuilder();
Table t = null;
switch (output.getType()) {
case DATABASE:
compBuilder.setDbName(output.getDatabase().getName());
break;
case TABLE:
case DUMMYPARTITION: // in case of dynamic partitioning lock the table
t = output.getTable();
compBuilder.setDbName(t.getDbName());
compBuilder.setTableName(t.getTableName());
break;
case PARTITION:
compBuilder.setPartitionName(output.getPartition().getName());
t = output.getPartition().getTable();
compBuilder.setDbName(t.getDbName());
compBuilder.setTableName(t.getTableName());
break;
default:
// This is a file or something we don't hold locks for.
continue;
}
switch (output.getWriteType()) {
/* base this on HiveOperation instead? this and DDL_NO_LOCK is peppered all over the code...
Seems much cleaner if each stmt is identified as a particular HiveOperation (which I'd think
makes sense everywhere). This however would be problematic for merge...*/
case DDL_EXCLUSIVE:
case INSERT_OVERWRITE:
compBuilder.setExclusive();
compBuilder.setOperationType(DataOperationType.NO_TXN);
break;
case INSERT:
assert t != null;
if(AcidUtils.isAcidTable(t)) {
compBuilder.setShared();
}
else {
if (conf.getBoolVar(HiveConf.ConfVars.HIVE_TXN_STRICT_LOCKING_MODE)) {
compBuilder.setExclusive();
} else { // this is backward compatible for non-ACID resources, w/o ACID semantics
compBuilder.setShared();
}
}
compBuilder.setOperationType(DataOperationType.INSERT);
break;
case DDL_SHARED:
compBuilder.setShared();
compBuilder.setOperationType(DataOperationType.NO_TXN);
break;
case UPDATE:
compBuilder.setSemiShared();
compBuilder.setOperationType(DataOperationType.UPDATE);
break;
case DELETE:
compBuilder.setSemiShared();
compBuilder.setOperationType(DataOperationType.DELETE);
break;
case DDL_NO_LOCK:
continue; // No lock required here
default:
throw new RuntimeException("Unknown write type " +
output.getWriteType().toString());
}
if(t != null) {
compBuilder.setIsAcid(AcidUtils.isAcidTable(t));
}
compBuilder.setIsDynamicPartitionWrite(output.isDynamicPartitionWrite());
LockComponent comp = compBuilder.build();
LOG.debug("Adding lock component to lock request " + comp.toString());
rqstBuilder.addLockComponent(comp);
atLeastOneLock = true;
}
//plan
// Make sure we need locks. It's possible there's nothing to lock in
// this operation.
if (!atLeastOneLock) {
LOG.debug("No locks needed for queryId" + queryId);
return null;
}
List<HiveLock> locks = new ArrayList<HiveLock>(1);
LockState lockState = lockMgr.lock(rqstBuilder.build(), queryId, isBlocking, locks);
ctx.setHiveLocks(locks);
return lockState;
}
private static Table getTable(WriteEntity we) {
Table t = we.getTable();
if(t == null) {
throw new IllegalStateException("No table info for " + we);
}
return t;
}
/**
* @param delay time to delay for first heartbeat
*/
@VisibleForTesting
void acquireLocksWithHeartbeatDelay(QueryPlan plan, Context ctx, String username, long delay) throws LockException {
LockState ls = acquireLocks(plan, ctx, username, true);
if (ls != null && !isTxnOpen()) { // If there's no lock, we don't need to do heartbeat
// Start heartbeat for read-only queries which don't open transactions but requires locks.
// For those that require transactions, the heartbeat has already been started in openTxn.
ctx.setHeartbeater(startHeartbeat(delay));
}
}
@Override
public void releaseLocks(List<HiveLock> hiveLocks) throws LockException {
if (lockMgr != null) {
stopHeartbeat();
lockMgr.releaseLocks(hiveLocks);
}
}
@Override
public void commitTxn() throws LockException {
if (!isTxnOpen()) {
throw new RuntimeException("Attempt to commit before opening a transaction");
}
try {
lockMgr.clearLocalLockRecords();
stopHeartbeat();
LOG.debug("Committing txn " + JavaUtils.txnIdToString(txnId));
getMS().commitTxn(txnId);
} catch (NoSuchTxnException e) {
LOG.error("Metastore could not find " + JavaUtils.txnIdToString(txnId));
throw new LockException(e, ErrorMsg.TXN_NO_SUCH_TRANSACTION, JavaUtils.txnIdToString(txnId));
} catch (TxnAbortedException e) {
LockException le = new LockException(e, ErrorMsg.TXN_ABORTED, JavaUtils.txnIdToString(txnId), e.getMessage());
LOG.error(le.getMessage());
throw le;
} catch (TException e) {
throw new LockException(ErrorMsg.METASTORE_COMMUNICATION_FAILED.getMsg(),
e);
} finally {
txnId = 0;
writeId = -1;
numStatements = 0;
}
}
@Override
public void rollbackTxn() throws LockException {
if (!isTxnOpen()) {
throw new RuntimeException("Attempt to rollback before opening a transaction");
}
try {
lockMgr.clearLocalLockRecords();
stopHeartbeat();
LOG.debug("Rolling back " + JavaUtils.txnIdToString(txnId));
getMS().rollbackTxn(txnId);
} catch (NoSuchTxnException e) {
LOG.error("Metastore could not find " + JavaUtils.txnIdToString(txnId));
throw new LockException(e, ErrorMsg.TXN_NO_SUCH_TRANSACTION, JavaUtils.txnIdToString(txnId));
} catch(TxnAbortedException e) {
throw new LockException(e, ErrorMsg.TXN_ABORTED, JavaUtils.txnIdToString(txnId));
} catch (TException e) {
throw new LockException(ErrorMsg.METASTORE_COMMUNICATION_FAILED.getMsg(),
e);
} finally {
txnId = 0;
writeId = -1;
numStatements = 0;
}
}
@Override
public void heartbeat() throws LockException {
List<HiveLock> locks;
if(isTxnOpen()) {
// Create one dummy lock so we can go through the loop below, though we only
//really need txnId
DbLockManager.DbHiveLock dummyLock = new DbLockManager.DbHiveLock(0L);
locks = new ArrayList<>(1);
locks.add(dummyLock);
}
else {
locks = lockMgr.getLocks(false, false);
}
if(LOG.isInfoEnabled()) {
StringBuilder sb = new StringBuilder("Sending heartbeat for ")
.append(JavaUtils.txnIdToString(txnId)).append(" and");
for(HiveLock lock : locks) {
sb.append(" ").append(lock.toString());
}
LOG.info(sb.toString());
}
if(!isTxnOpen() && locks.isEmpty()) {
// No locks, no txn, we outta here.
if (LOG.isDebugEnabled()) {
LOG.debug("No need to send heartbeat as there is no transaction and no locks.");
}
return;
}
for (HiveLock lock : locks) {
long lockId = ((DbLockManager.DbHiveLock)lock).lockId;
try {
/**
* This relies on the ThreadLocal caching, which implies that the same {@link IMetaStoreClient},
* in particular the Thrift connection it uses is never shared between threads
*/
getMS().heartbeat(txnId, lockId);
} catch (NoSuchLockException e) {
LOG.error("Unable to find lock " + JavaUtils.lockIdToString(lockId));
throw new LockException(e, ErrorMsg.LOCK_NO_SUCH_LOCK, JavaUtils.lockIdToString(lockId));
} catch (NoSuchTxnException e) {
LOG.error("Unable to find transaction " + JavaUtils.txnIdToString(txnId));
throw new LockException(e, ErrorMsg.TXN_NO_SUCH_TRANSACTION, JavaUtils.txnIdToString(txnId));
} catch (TxnAbortedException e) {
LockException le = new LockException(e, ErrorMsg.TXN_ABORTED, JavaUtils.txnIdToString(txnId), e.getMessage());
LOG.error(le.getMessage());
throw le;
} catch (TException e) {
throw new LockException(
ErrorMsg.METASTORE_COMMUNICATION_FAILED.getMsg() + "(" + JavaUtils.txnIdToString(txnId)
+ "," + lock.toString() + ")", e);
}
}
}
/**
* Start the heartbeater threadpool and return the task.
* @param initialDelay time to delay before first execution, in milliseconds
* @return heartbeater
*/
private Heartbeater startHeartbeat(long initialDelay) throws LockException {
long heartbeatInterval = getHeartbeatInterval(conf);
assert heartbeatInterval > 0;
Heartbeater heartbeater = new Heartbeater(this, conf, queryId);
// For negative testing purpose..
if(conf.getBoolVar(HiveConf.ConfVars.HIVE_IN_TEST) && conf.getBoolVar(HiveConf.ConfVars.HIVETESTMODEFAILHEARTBEATER)) {
initialDelay = 0;
} else if (initialDelay == 0) {
initialDelay = heartbeatInterval;
}
heartbeatTask = heartbeatExecutorService.scheduleAtFixedRate(
heartbeater, initialDelay, heartbeatInterval, TimeUnit.MILLISECONDS);
LOG.info("Started heartbeat with delay/interval = " + initialDelay + "/" + heartbeatInterval +
" " + TimeUnit.MILLISECONDS + " for query: " + queryId);
return heartbeater;
}
private void stopHeartbeat() throws LockException {
if (heartbeatTask != null) {
heartbeatTask.cancel(true);
long startTime = System.currentTimeMillis();
long sleepInterval = 100;
while (!heartbeatTask.isCancelled() && !heartbeatTask.isDone()) {
// We will wait for 30 seconds for the task to be cancelled.
// If it's still not cancelled (unlikely), we will just move on.
long now = System.currentTimeMillis();
if (now - startTime > 30000) {
LOG.warn("Heartbeat task cannot be cancelled for unknown reason. QueryId: " + queryId);
break;
}
try {
Thread.sleep(sleepInterval);
} catch (InterruptedException e) {
}
sleepInterval *= 2;
}
if (heartbeatTask.isCancelled() || heartbeatTask.isDone()) {
LOG.info("Stopped heartbeat for query: " + queryId);
}
heartbeatTask = null;
queryId = null;
}
}
@Override
public ValidTxnList getValidTxns() throws LockException {
init();
try {
return getMS().getValidTxns(txnId);
} catch (TException e) {
throw new LockException(ErrorMsg.METASTORE_COMMUNICATION_FAILED.getMsg(),
e);
}
}
@Override
public String getTxnManagerName() {
return CLASS_NAME;
}
@Override
public boolean supportsExplicitLock() {
return false;
}
@Override
public int lockTable(Hive db, LockTableDesc lockTbl) throws HiveException {
super.lockTable(db, lockTbl);
throw new UnsupportedOperationException();
}
@Override
public int unlockTable(Hive hiveDB, UnlockTableDesc unlockTbl) throws HiveException {
super.unlockTable(hiveDB, unlockTbl);
throw new UnsupportedOperationException();
}
@Override
public int lockDatabase(Hive hiveDB, LockDatabaseDesc lockDb) throws HiveException {
super.lockDatabase(hiveDB, lockDb);
throw new UnsupportedOperationException();
}
@Override
public int unlockDatabase(Hive hiveDB, UnlockDatabaseDesc unlockDb) throws HiveException {
super.unlockDatabase(hiveDB, unlockDb);
throw new UnsupportedOperationException();
}
@Override
public boolean useNewShowLocksFormat() {
return true;
}
@Override
public boolean supportsAcid() {
return true;
}
/**
* In an explicit txn start_transaction is the 1st statement and we record the snapshot at the
* start of the txn for Snapshot Isolation. For Read Committed (not supported yet) we'd record
* it before executing each statement (but after lock acquisition if using lock based concurrency
* control).
* For implicit txn, the stmt that triggered/started the txn is the first statement
*/
@Override
public boolean recordSnapshot(QueryPlan queryPlan) {
assert isTxnOpen();
assert numStatements > 0 : "was acquireLocks() called already?";
if(queryPlan.getOperation() == HiveOperation.START_TRANSACTION) {
//here if start of explicit txn
assert isExplicitTransaction;
assert numStatements == 1;
return true;
}
else if(!isExplicitTransaction) {
assert numStatements == 1 : "numStatements=" + numStatements + " in implicit txn";
if (queryPlan.hasAcidResourcesInQuery()) {
//1st and only stmt in implicit txn and uses acid resource
return true;
}
}
return false;
}
@Override
public boolean isImplicitTransactionOpen() {
if(!isTxnOpen()) {
//some commands like "show databases" don't start implicit transactions
return false;
}
if(!isExplicitTransaction) {
assert numStatements == 1 : "numStatements=" + numStatements;
return true;
}
return false;
}
@Override
protected void destruct() {
try {
stopHeartbeat();
if (shutdownRunner != null) {
ShutdownHookManager.removeShutdownHook(shutdownRunner);
}
if (isTxnOpen()) rollbackTxn();
if (lockMgr != null) lockMgr.close();
} catch (Exception e) {
LOG.error("Caught exception " + e.getClass().getName() + " with message <" + e.getMessage()
+ ">, swallowing as there is nothing we can do with it.");
// Not much we can do about it here.
}
}
private void init() throws LockException {
if (conf == null) {
throw new RuntimeException("Must call setHiveConf before any other methods.");
}
initHeartbeatExecutorService();
}
private synchronized void initHeartbeatExecutorService() {
if (heartbeatExecutorService != null && !heartbeatExecutorService.isShutdown()
&& !heartbeatExecutorService.isTerminated()) {
return;
}
heartbeatExecutorService =
Executors.newScheduledThreadPool(
conf.getIntVar(HiveConf.ConfVars.HIVE_TXN_HEARTBEAT_THREADPOOL_SIZE), new ThreadFactory() {
private final AtomicInteger threadCounter = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
return new HeartbeaterThread(r, "Heartbeater-" + threadCounter.getAndIncrement());
}
});
((ScheduledThreadPoolExecutor) heartbeatExecutorService).setRemoveOnCancelPolicy(true);
}
public static class HeartbeaterThread extends Thread {
HeartbeaterThread(Runnable target, String name) {
super(target, name);
setDaemon(true);
}
}
@Override
public boolean isTxnOpen() {
return txnId > 0;
}
@Override
public long getCurrentTxnId() {
return txnId;
}
@Override
public int getWriteIdAndIncrement() {
assert isTxnOpen();
return writeId++;
}
private static long getHeartbeatInterval(Configuration conf) throws LockException {
// Retrieve HIVE_TXN_TIMEOUT in MILLISECONDS (it's defined as SECONDS),
// then divide it by 2 to give us a safety factor.
long interval =
HiveConf.getTimeVar(conf, HiveConf.ConfVars.HIVE_TXN_TIMEOUT, TimeUnit.MILLISECONDS) / 2;
if (interval == 0) {
throw new LockException(HiveConf.ConfVars.HIVE_TXN_MANAGER.toString() + " not set," +
" heartbeats won't be sent");
}
return interval;
}
/**
* Heartbeater thread
*/
public static class Heartbeater implements Runnable {
private HiveTxnManager txnMgr;
private HiveConf conf;
LockException lockException;
private final String queryId;
public LockException getLockException() {
return lockException;
}
/**
*
* @param txnMgr transaction manager for this operation
*/
Heartbeater(HiveTxnManager txnMgr, HiveConf conf, String queryId) {
this.txnMgr = txnMgr;
this.conf = conf;
lockException = null;
this.queryId = queryId;
}
/**
* Send a heartbeat to the metastore for locks and transactions.
*/
@Override
public void run() {
try {
// For negative testing purpose..
if(conf.getBoolVar(HiveConf.ConfVars.HIVE_IN_TEST) && conf.getBoolVar(HiveConf.ConfVars.HIVETESTMODEFAILHEARTBEATER)) {
throw new LockException(HiveConf.ConfVars.HIVETESTMODEFAILHEARTBEATER.name() + "=true");
}
LOG.debug("Heartbeating...");
txnMgr.heartbeat();
} catch (LockException e) {
LOG.error("Failed trying to heartbeat queryId=" + queryId + ": " + e.getMessage());
lockException = e;
} catch (Throwable t) {
LOG.error("Failed trying to heartbeat queryId=" + queryId + ": " + t.getMessage(), t);
lockException =
new LockException("Failed trying to heartbeat queryId=" + queryId + ": "
+ t.getMessage(), t);
}
}
}
}