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

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); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy