nl.topicus.jdbc.xa.CloudSpannerXAConnection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spanner-jdbc Show documentation
Show all versions of spanner-jdbc Show documentation
JDBC Driver for Google Cloud Spanner
/*-------------------------------------------------------------------------
*
* 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;
}
}