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

nl.topicus.jdbc.xa.CloudSpannerXAConnection Maven / Gradle / Ivy

There is a newer version: 1.1.6
Show newest version
/*-------------------------------------------------------------------------
*
* Copyright (c) 2009-2014, PostgreSQL Global Development Group
*
*-------------------------------------------------------------------------
*/

package nl.topicus.jdbc.xa;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;
import javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import nl.topicus.jdbc.shaded.com.google.rpc.Code;
import nl.topicus.jdbc.CloudSpannerConnection;
import nl.topicus.jdbc.CloudSpannerPooledConnection;
import nl.topicus.jdbc.ICloudSpannerConnection;
import nl.topicus.jdbc.Logger;
import nl.topicus.jdbc.exception.CloudSpannerSQLException;

/**
 * The PostgreSQL implementation of {@link XAResource}.
 *
 * This implementation doesn't support transaction interleaving (see JTA specification, section
 * 3.4.4) and suspend/resume.
 *
 * Two-phase commit requires PostgreSQL server version 8.1 or higher.
 *
 * @author Heikki Linnakangas ([email protected])
 */
public class CloudSpannerXAConnection extends CloudSpannerPooledConnection
    implements XAConnection, XAResource {
  /**
   * A static flag that is used to checked whether the existence of the XA_PREPARED_MUTATIONS table
   * has been checked
   */
  private static boolean checkedTableExistence = false;

  private static final String CREATE_TABLE =
      "CREATE TABLE XA_PREPARED_MUTATIONS (XID STRING(150) NOT NULL, NUMBER INT64 NOT NULL, MUTATION STRING(MAX) NOT NULL) PRIMARY KEY (XID, NUMBER)";

  private static final String CHECK_TABLE_EXISTENCE =
      "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME=?";

  public static final String XA_PREPARED_MUTATIONS_TABLE = "XA_PREPARED_MUTATIONS";

  public static final String XA_XID_COLUMN = "XID";

  public static final String XA_NUMBER_COLUMN = "NUMBER";

  public static final String XA_MUTATION_COLUMN = "MUTATION";

  /**
   * Underlying physical database connection. It's used for issuing PREPARE TRANSACTION/ COMMIT
   * PREPARED/ROLLBACK PREPARED commands.
   */
  private final CloudSpannerConnection conn;
  private final Logger logger;

  /*
   * CloudSpannerXAConnection-object can be in one of three states:
   *
   * IDLE Not associated with a XA-transaction. You can still call getConnection and use the
   * connection outside XA. currentXid is null. autoCommit is true on a connection by getConnection,
   * per normal JDBC rules, though the caller can change it to false and manage transactions itself
   * using Connection.commit and rollback.
   *
   * ACTIVE start has been called, and we're associated with an XA transaction. currentXid is valid.
   * autoCommit is false on a connection returned by getConnection, and should not be messed with by
   * the caller or the XA transaction will be broken.
   *
   * ENDED end has been called, but the transaction has not yet been prepared. currentXid is still
   * valid. You shouldn't use the connection for anything else than issuing a XAResource.commit or
   * rollback.
   */
  private Xid currentXid;

  private int state;

  private static final int STATE_IDLE = 0;
  private static final int STATE_ACTIVE = 1;
  private static final int STATE_ENDED = 2;

  /*
   * When an XA transaction is started, we put the underlying connection into non-autocommit mode.
   * The old setting is saved in localAutoCommitMode, so that we can restore it when the XA
   * transaction ends and the connection returns into local transaction mode.
   */
  private boolean localAutoCommitMode = true;

  private void debug(String s) {
    logger.debug("XAResource " + Integer.toHexString(this.hashCode()) + ": " + s);
  }

  public CloudSpannerXAConnection(CloudSpannerConnection conn) throws SQLException {
    this(conn, true);
  }

  public CloudSpannerXAConnection(CloudSpannerConnection conn, boolean createXATable)
      throws SQLException {
    super(conn, true, true);
    this.conn = conn;
    this.state = STATE_IDLE;
    this.logger = conn.getLogger();
    if (createXATable)
      checkAndCreateTable();
  }

  private void checkAndCreateTable() throws SQLException {
    if (!checkedTableExistence) {
      checkTableExistence(conn);
    }
  }

  private static synchronized void checkTableExistence(Connection conn) throws SQLException {
    if (checkedTableExistence)
      return;
    boolean createTable = false;
    try (PreparedStatement statement = conn.prepareStatement(CHECK_TABLE_EXISTENCE)) {
      statement.setString(1, XA_PREPARED_MUTATIONS_TABLE);
      try (ResultSet rs = statement.executeQuery()) {
        if (!rs.next()) {
          createTable = true;
        }
      }
      if (createTable)
        createTable(conn);

      checkedTableExistence = true;
    }
  }

  private static synchronized void createTable(Connection conn) throws SQLException {
    try (PreparedStatement statement = conn.prepareStatement(CREATE_TABLE)) {
      statement.executeUpdate();
    }
  }

  /****
   * XAConnection interface
   ****/

  @Override
  public ICloudSpannerConnection getConnection() throws SQLException {
    if (logger.logDebug()) {
      debug("CloudSpannerXAConnection.getConnection called");
    }

    Connection connection = super.getConnection();

    // When we're outside an XA transaction, autocommit
    // is supposed to be true, per usual JDBC convention.
    // When an XA transaction is in progress, it should be
    // false.
    if (state == STATE_IDLE) {
      connection.setAutoCommit(true);
    }

    /*
     * Wrap the connection in a proxy to forbid application from fiddling with transaction state
     * directly during an XA transaction
     */
    ConnectionHandler handler = new ConnectionHandler(connection);
    return (ICloudSpannerConnection) Proxy.newProxyInstance(getClass().getClassLoader(),
        new Class[] {Connection.class, ICloudSpannerConnection.class}, handler);
  }

  @Override
  public XAResource getXAResource() {
    return this;
  }

  /*
   * A java.sql.Connection proxy class to forbid calls to transaction control methods while the
   * connection is used for an XA transaction.
   */
  private class ConnectionHandler implements InvocationHandler {
    private Connection con;

    public ConnectionHandler(Connection con) {
      this.con = con;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if (state != STATE_IDLE) {
        String methodName = method.getName();
        if (methodName.equals("commit") || methodName.equals("rollback")
            || methodName.equals("setSavePoint")
            || (methodName.equals("setAutoCommit") && (Boolean) args[0])) {
          throw new CloudSpannerSQLException(
              CloudSpannerXAException.COMMIT_METHODS_NOT_ALLOWED_WHEN_ACTIVE,
              Code.FAILED_PRECONDITION);
        }
      }
      try {
        /*
         * If the argument to equals-method is also a wrapper, present the original unwrapped
         * connection to the underlying equals method.
         */
        if (method.getName().equals("equals")) {
          Object arg = args[0];
          if (Proxy.isProxyClass(arg.getClass())) {
            InvocationHandler h = Proxy.getInvocationHandler(arg);
            if (h instanceof ConnectionHandler) {
              // unwrap argument
              args = new Object[] {((ConnectionHandler) h).con};
            }
          }
        }

        return method.invoke(con, args);
      } catch (InvocationTargetException ex) {
        throw ex.getTargetException();
      }
    }
  }

  /**** XAResource interface ****/

  /**
   * Preconditions: 1. flags must be one of TMNOFLAGS, TMRESUME or TMJOIN 2. xid != null 3.
   * connection must not be associated with a transaction 4. the TM hasn't seen the xid before
   *
   * Implementation deficiency preconditions: 1. TMRESUME not supported. 2. if flags is TMJOIN, we
   * must be in ended state, and xid must be the current transaction 3. unless flags is TMJOIN,
   * previous transaction using the connection must be committed or prepared or rolled back
   *
   * Postconditions: 1. Connection is associated with the transaction
   * 
   * @see XAResource#start(Xid, int)
   */
  @Override
  public void start(Xid xid, int flags) throws XAException {
    if (logger.logDebug()) {
      debug("starting transaction xid = " + xid);
    }

    // Check preconditions
    if (flags != XAResource.TMNOFLAGS && flags != XAResource.TMRESUME
        && flags != XAResource.TMJOIN) {
      throw new CloudSpannerXAException(CloudSpannerXAException.INVALID_FLAGS,
          Code.INVALID_ARGUMENT, XAException.XAER_INVAL);
    }

    if (xid == null) {
      throw new CloudSpannerXAException(CloudSpannerXAException.XID_NOT_NULL, Code.INVALID_ARGUMENT,
          XAException.XAER_INVAL);
    }

    if (state == STATE_ACTIVE) {
      throw new CloudSpannerXAException(CloudSpannerXAException.CONNECTION_BUSY,
          Code.FAILED_PRECONDITION, XAException.XAER_PROTO);
    }

    // We can't check precondition 4 easily, so we don't. Duplicate xid will
    // be catched in prepare
    // phase.

    // Check implementation deficiency preconditions
    if (flags == TMRESUME) {
      throw new CloudSpannerXAException(CloudSpannerXAException.SUSPEND_NOT_IMPLEMENTED,
          Code.UNIMPLEMENTED, XAException.XAER_RMERR);
    }

    // It's ok to join an ended transaction. WebLogic does that.
    if (flags == TMJOIN) {
      if (state != STATE_ENDED) {
        throw new CloudSpannerXAException(CloudSpannerXAException.INTERLEAVING_NOT_IMPLEMENTED,
            Code.UNIMPLEMENTED, XAException.XAER_RMERR);
      }

      if (!xid.equals(currentXid)) {
        throw new CloudSpannerXAException(CloudSpannerXAException.INTERLEAVING_NOT_IMPLEMENTED,
            Code.UNIMPLEMENTED, XAException.XAER_RMERR);
      }
    } else if (state == STATE_ENDED) {
      throw new CloudSpannerXAException(CloudSpannerXAException.INTERLEAVING_NOT_IMPLEMENTED,
          Code.UNIMPLEMENTED, XAException.XAER_RMERR);
    }

    // Only need save localAutoCommitMode for NOFLAGS, TMRESUME and TMJOIN
    // already saved old
    // localAutoCommitMode.
    if (flags == TMNOFLAGS) {
      try {
        localAutoCommitMode = conn.getAutoCommit();
        conn.setAutoCommit(false);
      } catch (CloudSpannerSQLException ex) {
        throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_DISABLING_AUTOCOMMIT, ex,
            XAException.XAER_RMERR);
      } catch (SQLException ex) {
        throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_DISABLING_AUTOCOMMIT, ex,
            Code.UNKNOWN, XAException.XAER_RMERR);
      }
    }

    // Preconditions are met, Associate connection with the transaction
    state = STATE_ACTIVE;
    currentXid = xid;
  }

  /**
   * Preconditions: 1. Flags is one of TMSUCCESS, TMFAIL, TMSUSPEND 2. xid != null 3. Connection is
   * associated with transaction xid
   *
   * Implementation deficiency preconditions: 1. Flags is not TMSUSPEND
   *
   * Postconditions: 1. connection is disassociated from the transaction.
   * 
   * @see XAResource#end(Xid, int)
   */
  @Override
  public void end(Xid xid, int flags) throws XAException {
    if (logger.logDebug()) {
      debug("ending transaction xid = " + xid);
    }

    // Check preconditions

    if (flags != XAResource.TMSUSPEND && flags != XAResource.TMFAIL
        && flags != XAResource.TMSUCCESS) {
      throw new CloudSpannerXAException(CloudSpannerXAException.INVALID_FLAGS,
          Code.INVALID_ARGUMENT, XAException.XAER_INVAL);
    }

    if (xid == null) {
      throw new CloudSpannerXAException(CloudSpannerXAException.XID_NOT_NULL, Code.INVALID_ARGUMENT,
          XAException.XAER_INVAL);
    }

    if (state != STATE_ACTIVE || !currentXid.equals(xid)) {
      throw new CloudSpannerXAException(CloudSpannerXAException.END_WITHOUT_START,
          Code.FAILED_PRECONDITION, XAException.XAER_PROTO);
    }

    // Check implementation deficiency preconditions
    if (flags == XAResource.TMSUSPEND) {
      throw new CloudSpannerXAException(CloudSpannerXAException.SUSPEND_NOT_IMPLEMENTED,
          Code.UNIMPLEMENTED, XAException.XAER_RMERR);
    }

    // We ignore TMFAIL. It's just a hint to the RM. We could roll back
    // immediately
    // if TMFAIL was given.

    // All clear. We don't have any real work to do.
    state = STATE_ENDED;
  }

  /**
   * Preconditions: 1. xid != null 2. xid is in ended state
   *
   * Implementation deficiency preconditions: 1. xid was associated with this connection
   *
   * Postconditions: 1. Transaction is prepared
   * 
   * @see XAResource#prepare(Xid)
   */
  @Override
  public int prepare(Xid xid) throws XAException {
    if (logger.logDebug()) {
      debug("preparing transaction xid = " + xid);
    }

    // Check preconditions
    if (!currentXid.equals(xid)) {
      throw new CloudSpannerXAException(CloudSpannerXAException.PREPARE_WITH_SAME,
          Code.UNIMPLEMENTED, XAException.XAER_RMERR);
    }
    if (state != STATE_ENDED) {
      throw new CloudSpannerXAException(CloudSpannerXAException.PREPARE_BEFORE_END,
          Code.FAILED_PRECONDITION, XAException.XAER_INVAL);
    }

    state = STATE_IDLE;
    currentXid = null;

    try {
      String s = RecoveredXid.xidToString(xid);
      conn.prepareTransaction(s);
      conn.setAutoCommit(localAutoCommitMode);

      return XA_OK;
    } catch (CloudSpannerSQLException ex) {
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_PREPARING, ex,
          XAException.XAER_RMERR);
    } catch (SQLException ex) {
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_PREPARING, ex, Code.UNKNOWN,
          XAException.XAER_RMERR);
    }
  }

  /**
   * Preconditions: 1. flag must be one of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS or TMSTARTTRSCAN |
   * TMENDRSCAN 2. if flag isn't TMSTARTRSCAN or TMSTARTRSCAN | TMENDRSCAN, a recovery scan must be
   * in progress
   *
   * Postconditions: 1. list of prepared xids is returned
   * 
   * @see XAResource#recover(int)
   */
  @Override
  public Xid[] recover(int flag) throws XAException {
    // Check preconditions
    if (flag != TMSTARTRSCAN && flag != TMENDRSCAN && flag != TMNOFLAGS
        && flag != (TMSTARTRSCAN | TMENDRSCAN)) {
      throw new CloudSpannerXAException(CloudSpannerXAException.INVALID_FLAGS,
          Code.INVALID_ARGUMENT, XAException.XAER_INVAL);
    }

    // We don't check for precondition 2, because we would have to add some
    // additional state in
    // this object to keep track of recovery scans.

    // All clear. We return all the xids in the first TMSTARTRSCAN call, and
    // always return
    // an empty array otherwise.
    if ((flag & TMSTARTRSCAN) == 0) {
      return new Xid[0];
    } else {
      try (Statement stmt = conn.createStatement()) {
        // If this connection is simultaneously used for a
        // transaction,
        // this query gets executed inside that transaction. It's
        // OK,
        // except if the transaction is in abort-only state and the
        // backed refuses to process new queries. Hopefully not a
        // problem
        // in practise.
        try (ResultSet rs = stmt.executeQuery(
            "SELECT DISTINCT " + XA_XID_COLUMN + " FROM " + XA_PREPARED_MUTATIONS_TABLE)) {
          LinkedList l = new LinkedList<>();
          while (rs.next()) {
            Xid recoveredXid = RecoveredXid.stringToXid(rs.getString(1));
            if (recoveredXid != null) {
              l.add(recoveredXid);
            }
          }
          return l.toArray(new Xid[l.size()]);
        }
      } catch (CloudSpannerSQLException ex) {
        throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_RECOVER, ex,
            XAException.XAER_RMERR);
      } catch (SQLException ex) {
        throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_RECOVER, ex, Code.UNKNOWN,
            XAException.XAER_RMERR);
      }
    }
  }

  /**
   * Preconditions: 1. xid is known to the RM or it's in prepared state
   *
   * Implementation deficiency preconditions: 1. xid must be associated with this connection if it's
   * not in prepared state.
   *
   * Postconditions: 1. Transaction is rolled back and disassociated from connection
   */
  @Override
  public void rollback(Xid xid) throws XAException {
    if (logger.logDebug()) {
      debug("rolling back xid = " + xid);
    }

    // We don't explicitly check precondition 1.

    try {
      if (currentXid != null && xid.equals(currentXid)) {
        state = STATE_IDLE;
        currentXid = null;
        conn.rollback();
        conn.setAutoCommit(localAutoCommitMode);
      } else {
        String s = RecoveredXid.xidToString(xid);
        conn.setAutoCommit(true);
        conn.rollbackPreparedTransaction(s);
      }
    } catch (CloudSpannerSQLException ex) {
      if (ex.getCode().equals(Code.NOT_FOUND)) {
        throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_ROLLBACK_PREPARED, ex,
            XAException.XAER_NOTA);
      }
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_ROLLBACK_PREPARED, ex,
          XAException.XAER_RMERR);
    } catch (SQLException ex) {
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_ROLLBACK_PREPARED, ex,
          Code.UNKNOWN, XAException.XAER_RMERR);
    }
  }

  @Override
  public void commit(Xid xid, boolean onePhase) throws XAException {
    if (logger.logDebug()) {
      debug("committing xid = " + xid + (onePhase ? " (one phase) " : " (two phase)"));
    }

    if (xid == null) {
      throw new CloudSpannerXAException(CloudSpannerXAException.XID_NOT_NULL, Code.INVALID_ARGUMENT,
          XAException.XAER_INVAL);
    }

    if (onePhase) {
      commitOnePhase(xid);
    } else {
      commitPrepared(xid);
    }
  }

  /**
   * Preconditions: 1. xid must in ended state.
   *
   * Implementation deficiency preconditions: 1. this connection must have been used to run the
   * transaction
   *
   * Postconditions: 1. Transaction is committed
   */
  private void commitOnePhase(Xid xid) throws XAException {
    try {
      // Check preconditions
      if (currentXid == null || !currentXid.equals(xid)) {
        // In fact, we don't know if xid is bogus, or if it just wasn't
        // associated with this
        // connection.
        // Assume it's our fault.
        throw new CloudSpannerXAException(CloudSpannerXAException.ONE_PHASE_SAME,
            Code.UNIMPLEMENTED, XAException.XAER_RMERR);
      }
      if (state != STATE_ENDED) {
        throw new CloudSpannerXAException(CloudSpannerXAException.COMMIT_BEFORE_END,
            Code.FAILED_PRECONDITION, XAException.XAER_PROTO);
      }

      // Preconditions are met. Commit
      state = STATE_IDLE;
      currentXid = null;

      conn.commit();
      conn.setAutoCommit(localAutoCommitMode);
    } catch (CloudSpannerSQLException ex) {
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_ONE_PHASE, ex,
          XAException.XAER_RMERR);
    } catch (SQLException ex) {
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_ONE_PHASE, ex, Code.UNKNOWN,
          XAException.XAER_RMERR);
    }
  }

  /**
   * Preconditions: 1. xid must be in prepared state in the server
   *
   * Implementation deficiency preconditions: 1. Connection must be in idle state
   *
   * Postconditions: 1. Transaction is committed
   */
  private void commitPrepared(Xid xid) throws XAException {
    try {
      // Check preconditions. The connection mustn't be used for another
      // other XA or local transaction, or the COMMIT PREPARED command
      // would mess it up.
      if (state != STATE_IDLE || conn.getTransaction().hasBufferedMutations()) {
        throw new CloudSpannerXAException(CloudSpannerXAException.TWO_PHASE_IDLE,
            Code.FAILED_PRECONDITION, XAException.XAER_RMERR);
      }

      String s = RecoveredXid.xidToString(xid);

      localAutoCommitMode = conn.getAutoCommit();
      conn.setAutoCommit(true);
      try {
        conn.commitPreparedTransaction(s);
      } finally {
        conn.setAutoCommit(localAutoCommitMode);
      }
    } catch (CloudSpannerSQLException ex) {
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_COMMIT_PREPARED, ex,
          XAException.XAER_RMERR);
    } catch (SQLException ex) {
      throw new CloudSpannerXAException(CloudSpannerXAException.ERROR_COMMIT_PREPARED, ex,
          Code.UNKNOWN, XAException.XAER_RMERR);
    }
  }

  @Override
  public boolean isSameRM(XAResource xares) throws XAException {
    // This trivial implementation makes sure that the
    // application server doesn't try to use another connection
    // for prepare, commit and rollback commands.
    return xares == this;
  }

  @Override
  public void forget(Xid xid) throws XAException {
    throw new CloudSpannerXAException(CloudSpannerXAException.HEURISTIC_NOT_IMPLEMENTED,
        Code.UNIMPLEMENTED, XAException.XAER_NOTA);
  }

  /**
   * We don't do transaction timeouts. Just returns 0.
   */
  @Override
  public int getTransactionTimeout() {
    return 0;
  }

  /**
   * We don't do transaction timeouts. Returns false.
   */
  @Override
  public boolean setTransactionTimeout(int seconds) {
    return false;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy