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

com.foundationdb.sql.jdbc.xa.PGXAConnection Maven / Gradle / Ivy

/*-------------------------------------------------------------------------
*
* Copyright (c) 2009-2011, PostgreSQL Global Development Group
*
*-------------------------------------------------------------------------
*/
package com.foundationdb.sql.jdbc.xa;

import com.foundationdb.sql.jdbc.PGConnection;
import com.foundationdb.sql.jdbc.core.BaseConnection;
import com.foundationdb.sql.jdbc.core.Logger;
import com.foundationdb.sql.jdbc.core.ProtocolConnection;
import com.foundationdb.sql.jdbc.ds.PGPooledConnection;
import com.foundationdb.sql.jdbc.util.GT;
import com.foundationdb.sql.jdbc.util.PSQLException;
import com.foundationdb.sql.jdbc.util.PSQLState;

import javax.sql.*;

import java.sql.*;
import java.util.*;
import java.lang.reflect.*;

import javax.transaction.xa.Xid;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.XAException;

/**
 * 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 PGXAConnection extends PGPooledConnection implements XAConnection, XAResource
{
    /**
     * Underlying physical database connection. It's used for issuing PREPARE TRANSACTION/
     * COMMIT PREPARED/ROLLBACK PREPARED commands.
     */
    private final BaseConnection conn;
    private final Logger logger;

    /*
     * PGXAConnection-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 PGXAConnection(BaseConnection conn) throws SQLException
    {
        super(conn, true, true);
        this.conn = conn;
        this.state = STATE_IDLE;
	this.logger = conn.getLogger();
    }


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

    public Connection getConnection() throws SQLException
    {
        if (logger.logDebug())
            debug("PGXAConnection.getConnection called");

        Connection conn = 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)
            conn.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(conn);
        return (Connection)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Connection.class, PGConnection.class}, handler);
    }

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

        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]).booleanValue()))
                {
		    throw new PSQLException(GT.tr("Transaction control methods setAutoCommit(true), commit, rollback and setSavePoint not allowed while an XA transaction is active."),
					    PSQLState.OBJECT_NOT_IN_STATE);
                }
            }
            try {
                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
     */
    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 PGXAException(GT.tr("Invalid flags"), XAException.XAER_INVAL);

        if (xid == null)
            throw new PGXAException(GT.tr("xid must not be null"), XAException.XAER_INVAL);

        if (state == STATE_ACTIVE)
            throw new PGXAException(GT.tr("Connection is busy with another transaction"), 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 PGXAException(GT.tr("suspend/resume not implemented"), XAException.XAER_RMERR);

        // It's ok to join an ended transaction. WebLogic does that.
        if (flags == TMJOIN)
        {
            if (state != STATE_ENDED)
                throw new PGXAException(GT.tr("Transaction interleaving not implemented"), XAException.XAER_RMERR);

            if (!xid.equals(currentXid))
                throw new PGXAException(GT.tr("Transaction interleaving not implemented"), XAException.XAER_RMERR);
        } else if(state == STATE_ENDED)
            throw new PGXAException(GT.tr("Transaction interleaving not implemented"), XAException.XAER_RMERR);

        try
        {
            localAutoCommitMode = conn.getAutoCommit();
            conn.setAutoCommit(false);
        }
        catch (SQLException ex)
        {
            throw new PGXAException(GT.tr("Error disabling autocommit"), ex, 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.
     */
    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 PGXAException(GT.tr("Invalid flags"), XAException.XAER_INVAL);

        if (xid == null)
            throw new PGXAException(GT.tr("xid must not be null"), XAException.XAER_INVAL);

        if (state != STATE_ACTIVE || !currentXid.equals(xid))
            throw new PGXAException(GT.tr("tried to call end without corresponding start call"), XAException.XAER_PROTO);

        // Check implementation deficiency preconditions
        if (flags == XAResource.TMSUSPEND)
            throw new PGXAException(GT.tr("suspend/resume not implemented"), 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
     */
    public int prepare(Xid xid) throws XAException {
        if (logger.logDebug())
            debug("preparing transaction xid = " + xid);

        // Check preconditions
        if (!currentXid.equals(xid))
        {
            throw new PGXAException(GT.tr("Not implemented: Prepare must be issued using the same connection that started the transaction"),
                                    XAException.XAER_RMERR);
        }
        if (state != STATE_ENDED)
            throw new PGXAException(GT.tr("Prepare called before end"), XAException.XAER_INVAL);

        state = STATE_IDLE;
        currentXid = null;

        if (!conn.haveMinimumServerVersion("8.1"))
            throw new PGXAException(GT.tr("Server versions prior to 8.1 do not support two-phase commit."), XAException.XAER_RMERR);

        try
        {
            String s = RecoveredXid.xidToString(xid);

            Statement stmt = conn.createStatement();
            try
            {
                stmt.executeUpdate("PREPARE TRANSACTION '" + s + "'");
            }
            finally
            {
                stmt.close();
            }
            conn.setAutoCommit(localAutoCommitMode);

            return XA_OK;
        }
        catch (SQLException ex)
        {
            throw new PGXAException(GT.tr("Error preparing transaction"), ex, 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
     */
    public Xid[] recover(int flag) throws XAException {
        // Check preconditions
        if (flag != TMSTARTRSCAN && flag != TMENDRSCAN && flag != TMNOFLAGS && flag != (TMSTARTRSCAN | TMENDRSCAN))
            throw new PGXAException(GT.tr("Invalid flag"), 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();
                try
                {
                    // 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.
                    ResultSet rs = stmt.executeQuery("SELECT gid FROM pg_prepared_xacts");
                    LinkedList l = new LinkedList();
                    while (rs.next())
                    {
                        Xid recoveredXid = RecoveredXid.stringToXid(rs.getString(1));
                        if (recoveredXid != null)
                            l.add(recoveredXid);
                    }
                    rs.close();

                    return (Xid[]) l.toArray(new Xid[l.size()]);
                }
                finally
                {
                    stmt.close();
                }
            }
            catch (SQLException ex)
            {
                throw new PGXAException(GT.tr("Error during recover"), ex, 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
     */
    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);
                Statement stmt = conn.createStatement();
                try
                {
                    stmt.executeUpdate("ROLLBACK PREPARED '" + s + "'");
                }
                finally
                {
                    stmt.close();
                }
            }
        }
        catch (SQLException ex)
        {
        	if (PSQLState.UNDEFINED_OBJECT.getState().equals(ex.getSQLState()))
    		{
        		throw new PGXAException(GT.tr("Error rolling back prepared transaction"), ex, XAException.XAER_NOTA);
            }
            throw new PGXAException(GT.tr("Error rolling back prepared transaction"), ex, XAException.XAER_RMERR);
        }
    }

    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 PGXAException(GT.tr("xid must not be null"), 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 PGXAException(GT.tr("Not implemented: one-phase commit must be issued using the same connection that was used to start it"),
                                        XAException.XAER_RMERR);
            }
            if (state != STATE_ENDED)
                throw new PGXAException(GT.tr("commit called before end"), XAException.XAER_PROTO);

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

            conn.commit();
            conn.setAutoCommit(localAutoCommitMode);
        }
        catch (SQLException ex)
        {
            throw new PGXAException(GT.tr("Error during one-phase commit"), ex, 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.getTransactionState() != ProtocolConnection.TRANSACTION_IDLE)
                throw new PGXAException(GT.tr("Not implemented: 2nd phase commit must be issued using an idle connection"),
                                        XAException.XAER_RMERR);

            String s = RecoveredXid.xidToString(xid);

            localAutoCommitMode = conn.getAutoCommit();
            conn.setAutoCommit(true);
            Statement stmt = conn.createStatement();
            try
            {
                stmt.executeUpdate("COMMIT PREPARED '" + s + "'");
            }
            finally
            {
                stmt.close();
                conn.setAutoCommit(localAutoCommitMode);
            }
        }
        catch (SQLException ex)
        {
            throw new PGXAException(GT.tr("Error committing prepared transaction"), ex, XAException.XAER_RMERR);
        }
    }

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

    /**
     * Does nothing, since we don't do heuristics, 
     */
    public void forget(Xid xid) throws XAException {
        throw new PGXAException(GT.tr("Heuristic commit/rollback not supported"), XAException.XAER_NOTA);
    }

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

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy