/*
* Copyright (c) 2009, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.test.xa;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.postgresql.test.TestUtil;
import org.postgresql.test.jdbc2.optional.BaseDataSourceTest;
import org.postgresql.xa.PGXADataSource;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Random;
import javax.sql.XAConnection;
import javax.sql.XADataSource;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
public class XADataSourceTest {
private XADataSource _ds;
private Connection _conn;
private boolean connIsSuper;
private XAConnection xaconn;
private XAResource xaRes;
private Connection conn;
public XADataSourceTest() {
_ds = new PGXADataSource();
BaseDataSourceTest.setupDataSource((PGXADataSource) _ds);
}
@Before
public void setUp() throws Exception {
_conn = TestUtil.openDB();
// Check if we're operating as a superuser; some tests require it.
Statement st = _conn.createStatement();
st.executeQuery("SHOW is_superuser;");
ResultSet rs = st.getResultSet();
rs.next(); // One row is guaranteed
connIsSuper = rs.getBoolean(1); // One col is guaranteed
st.close();
TestUtil.createTable(_conn, "testxa1", "foo int");
clearAllPrepared();
xaconn = _ds.getXAConnection();
xaRes = xaconn.getXAResource();
conn = xaconn.getConnection();
}
@After
public void tearDown() throws SQLException {
xaconn.close();
clearAllPrepared();
TestUtil.dropTable(_conn, "testxa1");
TestUtil.closeDB(_conn);
}
private void clearAllPrepared() throws SQLException {
Statement st = _conn.createStatement();
try {
ResultSet rs = st.executeQuery(
"SELECT x.gid, x.owner = current_user "
+ "FROM pg_prepared_xacts x "
+ "WHERE x.database = current_database()");
Statement st2 = _conn.createStatement();
while (rs.next()) {
// TODO: This should really use org.junit.Assume once we move to JUnit 4
assertTrue("Only prepared xacts owned by current user may be present in db",
rs.getBoolean(2));
st2.executeUpdate("ROLLBACK PREPARED '" + rs.getString(1) + "'");
}
st2.close();
} finally {
st.close();
}
}
static class CustomXid implements Xid {
private static Random rand = new Random(System.currentTimeMillis());
byte[] gtrid = new byte[Xid.MAXGTRIDSIZE];
byte[] bqual = new byte[Xid.MAXBQUALSIZE];
CustomXid(int i) {
rand.nextBytes(gtrid);
gtrid[0] = (byte) i;
gtrid[1] = (byte) i;
gtrid[2] = (byte) i;
gtrid[3] = (byte) i;
gtrid[4] = (byte) i;
bqual[0] = 4;
bqual[1] = 5;
bqual[2] = 6;
}
@Override
public int getFormatId() {
return 0;
}
@Override
public byte[] getGlobalTransactionId() {
return gtrid;
}
@Override
public byte[] getBranchQualifier() {
return bqual;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Xid)) {
return false;
}
Xid other = (Xid) o;
if (other.getFormatId() != this.getFormatId()) {
return false;
}
if (!Arrays.equals(other.getBranchQualifier(), this.getBranchQualifier())) {
return false;
}
if (!Arrays.equals(other.getGlobalTransactionId(), this.getGlobalTransactionId())) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(getBranchQualifier());
result = prime * result + getFormatId();
result = prime * result + Arrays.hashCode(getGlobalTransactionId());
return result;
}
}
/*
* Check that the equals method works for the connection wrapper returned by
* PGXAConnection.getConnection().
*/
@Test
public void testWrapperEquals() throws Exception {
assertTrue("Wrappers should be equal", conn.equals(conn));
}
@Test
public void testOnePhase() throws Exception {
Xid xid = new CustomXid(1);
xaRes.start(xid, XAResource.TMNOFLAGS);
conn.createStatement().executeQuery("SELECT * FROM testxa1");
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.commit(xid, true);
}
@Test
public void testTwoPhaseCommit() throws Exception {
Xid xid = new CustomXid(1);
xaRes.start(xid, XAResource.TMNOFLAGS);
conn.createStatement().executeQuery("SELECT * FROM testxa1");
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.prepare(xid);
xaRes.commit(xid, false);
}
@Test
public void testCloseBeforeCommit() throws Exception {
Xid xid = new CustomXid(5);
xaRes.start(xid, XAResource.TMNOFLAGS);
assertEquals(1, conn.createStatement().executeUpdate("INSERT INTO testxa1 VALUES (1)"));
conn.close();
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.commit(xid, true);
ResultSet rs = _conn.createStatement().executeQuery("SELECT foo FROM testxa1");
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
}
@Test
public void testRecover() throws Exception {
Xid xid = new CustomXid(12345);
xaRes.start(xid, XAResource.TMNOFLAGS);
conn.createStatement().executeQuery("SELECT * FROM testxa1");
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.prepare(xid);
{
Xid[] recoveredXidArray = xaRes.recover(XAResource.TMSTARTRSCAN);
boolean recoveredXid = false;
for (Xid aRecoveredXidArray : recoveredXidArray) {
if (xid.equals(aRecoveredXidArray)) {
recoveredXid = true;
break;
}
}
assertTrue("Did not recover prepared xid", recoveredXid);
assertEquals(0, xaRes.recover(XAResource.TMNOFLAGS).length);
}
xaRes.rollback(xid);
{
Xid[] recoveredXidArray = xaRes.recover(XAResource.TMSTARTRSCAN);
boolean recoveredXid = false;
for (Xid aRecoveredXidArray : recoveredXidArray) {
if (xaRes.equals(aRecoveredXidArray)) {
recoveredXid = true;
break;
}
}
assertFalse("Recovered rolled back xid", recoveredXid);
}
}
@Test
public void testRollback() throws XAException {
Xid xid = new CustomXid(3);
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.prepare(xid);
xaRes.rollback(xid);
}
@Test
public void testRollbackWithoutPrepare() throws XAException {
Xid xid = new CustomXid(4);
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.rollback(xid);
}
@Test
public void testAutoCommit() throws Exception {
Xid xid = new CustomXid(6);
// When not in an XA transaction, autocommit should be true
// per normal JDBC rules.
assertTrue(conn.getAutoCommit());
// When in an XA transaction, autocommit should be false
xaRes.start(xid, XAResource.TMNOFLAGS);
assertFalse(conn.getAutoCommit());
xaRes.end(xid, XAResource.TMSUCCESS);
assertFalse(conn.getAutoCommit());
xaRes.commit(xid, true);
assertTrue(conn.getAutoCommit());
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.prepare(xid);
assertTrue(conn.getAutoCommit());
xaRes.commit(xid, false);
assertTrue(conn.getAutoCommit());
// Check that autocommit is reset to true after a 1-phase rollback
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.rollback(xid);
assertTrue(conn.getAutoCommit());
// Check that autocommit is reset to true after a 2-phase rollback
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.prepare(xid);
xaRes.rollback(xid);
assertTrue(conn.getAutoCommit());
// Check that autoCommit is set correctly after a getConnection-call
conn = xaconn.getConnection();
assertTrue(conn.getAutoCommit());
xaRes.start(xid, XAResource.TMNOFLAGS);
conn.createStatement().executeQuery("SELECT * FROM testxa1");
java.sql.Timestamp ts1 = getTransactionTimestamp(conn);
conn.close();
conn = xaconn.getConnection();
assertFalse(conn.getAutoCommit());
java.sql.Timestamp ts2 = getTransactionTimestamp(conn);
/*
* Check that we're still in the same transaction. close+getConnection() should not rollback the
* XA-transaction implicitly.
*/
assertEquals(ts1, ts2);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.prepare(xid);
xaRes.rollback(xid);
assertTrue(conn.getAutoCommit());
}
/**
* Get the time the current transaction was started from the server.
*
* This can be used to check that transaction doesn't get committed/ rolled back inadvertently, by
* calling this once before and after the suspected piece of code, and check that they match. It's
* a bit iffy, conceivably you might get the same timestamp anyway if the suspected piece of code
* runs fast enough, and/or the server clock is very coarse grained. But it'll do for testing
* purposes.
*/
private static java.sql.Timestamp getTransactionTimestamp(Connection conn) throws SQLException {
ResultSet rs = conn.createStatement().executeQuery("SELECT now()");
rs.next();
return rs.getTimestamp(1);
}
@Test
public void testEndThenJoin() throws XAException {
Xid xid = new CustomXid(5);
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.start(xid, XAResource.TMJOIN);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.commit(xid, true);
}
@Test
public void testRestoreOfAutoCommit() throws Exception {
conn.setAutoCommit(false);
Xid xid = new CustomXid(14);
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.commit(xid, true);
assertFalse(
"XaResource should have restored connection autocommit mode after commit or rollback to the initial state.",
conn.getAutoCommit());
// Test true case
conn.setAutoCommit(true);
xid = new CustomXid(15);
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.commit(xid, true);
assertTrue(
"XaResource should have restored connection autocommit mode after commit or rollback to the initial state.",
conn.getAutoCommit());
}
@Test
public void testRestoreOfAutoCommitEndThenJoin() throws Exception {
// Test with TMJOIN
conn.setAutoCommit(true);
Xid xid = new CustomXid(16);
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.start(xid, XAResource.TMJOIN);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.commit(xid, true);
assertTrue(
"XaResource should have restored connection autocommit mode after start(TMNOFLAGS) end() start(TMJOIN) and then commit or rollback to the initial state.",
conn.getAutoCommit());
}
/**
* Test how the driver responds to rolling back a transaction that has already been rolled back.
* Check the driver reports the xid does not exist. The db knows the fact. ERROR: prepared
* transaction with identifier "blah" does not exist
*/
@Test
public void testRepeatedRolledBack() throws Exception {
Xid xid = new CustomXid(654321);
xaRes.start(xid, XAResource.TMNOFLAGS);
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.prepare(xid);
// tm crash
xaRes.recover(XAResource.TMSTARTRSCAN);
xaRes.rollback(xid);
try {
xaRes.rollback(xid);
fail("Rollback was successful");
} catch (XAException xae) {
assertEquals("Checking the errorCode is XAER_NOTA indicating the " + "xid does not exist.",
XAException.XAER_NOTA, xae.errorCode);
}
}
/*
* We don't support transaction interleaving. public void testInterleaving1() throws Exception {
* Xid xid1 = new CustomXid(1); Xid xid2 = new CustomXid(2);
*
* xaRes.start(xid1, XAResource.TMNOFLAGS); conn.createStatement().executeUpdate(
* "UPDATE testxa1 SET foo = 'ccc'"); xaRes.end(xid1, XAResource.TMSUCCESS);
*
* xaRes.start(xid2, XAResource.TMNOFLAGS); conn.createStatement().executeUpdate(
* "UPDATE testxa2 SET foo = 'bbb'");
*
* xaRes.commit(xid1, true);
*
* xaRes.end(xid2, XAResource.TMSUCCESS);
*
* xaRes.commit(xid2, true);
*
* } public void testInterleaving2() throws Exception { Xid xid1 = new CustomXid(1); Xid xid2 =
* new CustomXid(2); Xid xid3 = new CustomXid(3);
*
* xaRes.start(xid1, XAResource.TMNOFLAGS); conn.createStatement().executeUpdate(
* "UPDATE testxa1 SET foo = 'aa'"); xaRes.end(xid1, XAResource.TMSUCCESS);
*
* xaRes.start(xid2, XAResource.TMNOFLAGS); conn.createStatement().executeUpdate(
* "UPDATE testxa2 SET foo = 'bb'"); xaRes.end(xid2, XAResource.TMSUCCESS);
*
* xaRes.start(xid3, XAResource.TMNOFLAGS); conn.createStatement().executeUpdate(
* "UPDATE testxa3 SET foo = 'cc'"); xaRes.end(xid3, XAResource.TMSUCCESS);
*
* xaRes.commit(xid1, true); xaRes.commit(xid2, true); xaRes.commit(xid3, true); }
*/
}