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