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