org.postgresql.xa.PGXAConnection Maven / Gradle / Ivy
/*
* Copyright (c) 2009, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.xa;
import org.postgresql.PGConnection;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.TransactionState;
import org.postgresql.ds.PGPooledConnection;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
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.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
/**
* 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 {
private static final Logger LOGGER = Logger.getLogger(PGXAConnection.class.getName());
/**
* Underlying physical database connection. It's used for issuing PREPARE TRANSACTION/ COMMIT
* PREPARED/ROLLBACK PREPARED commands.
*/
private final BaseConnection conn;
/*
* 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) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "XAResource {0}: {1}", new Object[]{Integer.toHexString(this.hashCode()), s});
}
}
public PGXAConnection(BaseConnection conn) throws SQLException {
super(conn, true, true);
this.conn = conn;
this.state = STATE_IDLE;
}
/**
* XAConnection interface
*/
public Connection getConnection() throws SQLException {
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 final Connection con;
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 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 {
/*
* 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
*/
public void start(Xid xid, int flags) throws XAException {
if (LOGGER.isLoggable(Level.FINEST)) {
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);
}
// Only need save localAutoCommitMode for NOFLAGS, TMRESUME and TMJOIN already saved old
// localAutoCommitMode.
if (flags == TMNOFLAGS) {
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.isLoggable(Level.FINEST)) {
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.isLoggable(Level.FINEST)) {
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;
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 where database = current_database()");
LinkedList l = new LinkedList();
while (rs.next()) {
Xid recoveredXid = RecoveredXid.stringToXid(rs.getString(1));
if (recoveredXid != null) {
l.add(recoveredXid);
}
}
rs.close();
return 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.isLoggable(Level.FINEST)) {
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.isLoggable(Level.FINEST)) {
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() != TransactionState.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 - 2025 Weber Informatics LLC | Privacy Policy