com.scalar.db.sql.springdata.twopc.TwoPcOperationsProcessor Maven / Gradle / Ivy
package com.scalar.db.sql.springdata.twopc;
import com.scalar.db.sql.springdata.exception.ScalarDbNonTransientException;
import com.scalar.db.sql.springdata.exception.ScalarDbTransientException;
import com.scalar.db.sql.springdata.exception.ScalarDbUnknownTransactionStateException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.NonTransientDataAccessException;
class TwoPcOperationsProcessor {
private static final Logger logger = LoggerFactory.getLogger(TwoPcOperationsProcessor.class);
private TwoPcOperationsProcessor() {}
static TwoPcOperationsProcessor create() {
return new TwoPcOperationsProcessor();
}
/**
* Execute 2PC transaction in the following order. A local BEGIN or JOIN needs to be called in
* advance.
*
*
* - Execution phase: Local and remote CRUD operations
*
- Prepare phase: Local and remote prepare operations in parallel
*
- Validation phase: Local and remote validation operations in parallel
*
- Commit phase: Local commit operation is first executed. Remote commit operations are
* executed in parallel after the local commit operation succeeded
*
- (If any operation except for remote commit operation fails) rollback phase: Local and
* remote rollback operations in parallel
*
*
* @param transactionId a transaction ID used for 2PC transaction over local and remote
* participants
* @param executionPhaseOps execution phase task containing both local and remote operations
* (local CRUD, remote service call and so on.) This task is also supposed to start
* transactions with a given transaction ID on remote participants. Local and remote
* operations will be rolled back if any exception is thrown
* @param localPrepareCommitPhaseOps local 2PC operations (prepare, validate, commit and rollback)
* @param remoteOpsProcessor an operation processor for all the remote participants' prepare and
* commit phase operations
* @param a result type returned from {@code executionPhaseOps}
* @throws ScalarDbUnknownTransactionStateException if the 2PC transaction fails due to a
* non-transient cause with an unknown final status. The exception contains {@code
* transactionId}. Note that the final 2PC transaction status is unknown. Whether the
* transaction is actually committed or not needs to be decided by the application side (e.g.
* check if the target record is expectedly updated)
* @throws ScalarDbNonTransientException if the 2PC transaction fails due to a non-transient
* cause. The exception contains {@code transactionId}
* @throws ScalarDbTransientException if the 2PC transaction fails due to a transient cause. The
* exception contains {@code transactionId}
* @return a return value from {@code executionPhaseOps} that contains a nullable result of local
* and remote operations
*/
TwoPcResult execute(
String transactionId,
ExecutionPhaseOperations executionPhaseOps,
LocalPrepareCommitPhaseOperations localPrepareCommitPhaseOps,
RemotePrepareCommitOperationsProcessor
remoteOpsProcessor) {
R result = null;
AtomicBoolean isCommitted = new AtomicBoolean();
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
// Get the result of execution phase local and remote operations
result = executionPhaseOps.execute(transactionId);
// Prepare
Future> future =
executorService.submit(
() ->
remoteOpsProcessor.exec(
(remoteOperation) -> remoteOperation.prepare(transactionId)));
// The point is Spring Data uses ThreadLocal to manage actual SQL sessions. This local
// operation is generated by Spring Data repository class, but it needs to be executed in the
// same thread. This is why we need to execute this operation in the same thread the local
// transaction started.
localPrepareCommitPhaseOps.prepare();
future.get();
// Validate
future =
executorService.submit(
() ->
remoteOpsProcessor.exec(
(twoPcOperations) -> twoPcOperations.validate(transactionId)));
localPrepareCommitPhaseOps.validate();
future.get();
// Commit
// The local coordinator commit is executed first, and then the remote participant commits
// are executed. Failures from remote participant commits are ignored as the global
// transaction is committed by the local coordinator.
localPrepareCommitPhaseOps.commit();
isCommitted.set(true);
try {
future =
executorService.submit(
() ->
remoteOpsProcessor.exec(
(remoteOperation) -> remoteOperation.commit(transactionId)));
future.get();
} catch (Throwable e) {
logger.warn(
"Some remote participant commit failed. But they can be ignored once the coordinator commit succeeded. transactionId:{}",
transactionId,
e);
}
return new TwoPcResult<>(transactionId, result);
} catch (Throwable origException) {
if (isCommitted.get()) {
logger.warn(
"An operation failed, but the coordinator successfully committed the transaction. This transaction is treated as success. transactionId:{}",
transactionId,
origException);
return new TwoPcResult<>(transactionId, result);
} else {
try {
Future> future =
executorService.submit(
() ->
remoteOpsProcessor.exec(
(remoteOperation) -> remoteOperation.rollback(transactionId)));
localPrepareCommitPhaseOps.rollback();
future.get();
} catch (Exception nestedEx) {
logger.warn(
"Failed to execute rollback operations for remote participants. But all prepared records will be lazily rolled back. transactionId:{}",
transactionId,
nestedEx);
}
// Use the cause if the exception happened from Future#get()
Throwable e =
origException instanceof ExecutionException ? origException.getCause() : origException;
// Local transaction will be rolled back by throwing an exception
String errorMessage = "Failed to execute 2PC transaction";
if (e instanceof ScalarDbUnknownTransactionStateException) {
throw new ScalarDbUnknownTransactionStateException(errorMessage, e, transactionId);
} else if (e instanceof NonTransientDataAccessException) {
// This type of unexpected exception can happen from local operation
throw new ScalarDbNonTransientException(errorMessage, e, transactionId);
} else {
// Other exceptions are treated as TransientException
throw new ScalarDbTransientException(errorMessage, e, transactionId);
}
}
} finally {
executorService.shutdown();
try {
// Maybe this needs to be configurable in the future
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
logger.warn("ExecutorService termination is timed out. transactionId:{}", transactionId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Interrupted while waiting the termination. transactionID:{}", transactionId);
}
}
}
}