diff --git a/pom.xml b/pom.xml index 659fbf4..34a0bd9 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ com.samskivert samskivert - 1.8.2 + 1.9-SNAPSHOT com.google.guava diff --git a/src/main/java/com/samskivert/depot/PersistenceContext.java b/src/main/java/com/samskivert/depot/PersistenceContext.java index 51f7193..310e3b0 100644 --- a/src/main/java/com/samskivert/depot/PersistenceContext.java +++ b/src/main/java/com/samskivert/depot/PersistenceContext.java @@ -585,10 +585,13 @@ public class PersistenceContext checkAreInitialized(); // le check du sanity boolean isReadOnly = op.isReadOnly(); - Connection conn; + Transaction tx = Transaction.get(); + ConnOp connop = (tx == null) ? new NonTxOp(isReadOnly) : new TxOp(tx); + long preConnect = System.nanoTime(); + Connection conn; try { - conn = _conprov.getConnection(_ident, isReadOnly); + conn = connop.get(); } catch (PersistenceException pe) { throw new DatabaseException("Failed get connection [ident=" + _ident + ", isRO=" + isReadOnly + "]", pe); @@ -616,12 +619,8 @@ public class PersistenceContext stmt.close(); } } - // Always commit if auto-commit is off. If the read-only operation managed to - // acquire some locks, this will release them. Also, we've seen a MySQL bug where - // not committing after a select causes later selects to see stale results. - if (!conn.getAutoCommit()) { - conn.commit(); - } + // let our connop do auto-commit, if appropriate + connop.done(conn); // note the time it took to invoke this operation _stats.noteOp(isReadOnly, preConnect, preInvoke, System.nanoTime()); // have the operation update any appropriate runtime statistics as well @@ -637,10 +636,11 @@ public class PersistenceContext } // let the provider know that the connection failed - _conprov.connectionFailed(_ident, isReadOnly, conn, sqe); + boolean opAllowsRetry = connop.fail(conn, sqe); conn = null; - if (retryOnTransientFailure && _liaison.isTransientException(sqe)) { + if (retryOnTransientFailure && opAllowsRetry && + _liaison.isTransientException(sqe)) { // the MySQL JDBC driver has the annoying habit of including the embedded // exception stack trace in the message of their outer exception; if I want a // fucking stack trace, I'll call printStackTrace() thanksverymuch @@ -653,7 +653,7 @@ public class PersistenceContext } finally { if (conn != null) { - _conprov.releaseConnection(_ident, isReadOnly, conn); + connop.release(conn); } } } @@ -671,6 +671,58 @@ public class PersistenceContext } } + interface ConnOp { + Connection get () throws PersistenceException; + void done (Connection conn) throws SQLException; + boolean fail (Connection conn, SQLException sqe); + void release (Connection conn); + } + + protected class NonTxOp implements ConnOp { + public final boolean readOnly; + public NonTxOp (boolean readOnly) { + this.readOnly = readOnly; + } + public Connection get () throws PersistenceException { + return _conprov.getConnection(_ident, readOnly); + } + public void done (Connection conn) throws SQLException { + // Always commit if auto-commit is off. If the read-only operation managed to acquire + // some locks, this will release them. Also, we've seen a MySQL bug where not + // committing after a select causes later selects to see stale results. + if (!conn.getAutoCommit()) { + conn.commit(); + } + } + public boolean fail (Connection conn, SQLException sqe) { + _conprov.connectionFailed(_ident, readOnly, conn, sqe); + return true; + } + public void release (Connection conn) { + _conprov.releaseConnection(_ident, readOnly, conn); + } + }; + + protected class TxOp implements ConnOp { + public final Transaction tx; + public TxOp (Transaction tx) { + this.tx = tx; + } + public Connection get () throws PersistenceException { + return tx.getConnection(); + } + public void done (Connection conn) throws SQLException { + // nothing to do here, we commit (or not) when the whole tx is done + } + public boolean fail (Connection conn, SQLException sqe) { + tx.connectionFailed(sqe); + return false; + } + public void release (Connection conn) { + // nothing to do here, we release the conn when the whole tx is done + } + } + protected final CanMigrate _canMigrate; protected String _ident; protected ConnectionProvider _conprov; diff --git a/src/main/java/com/samskivert/depot/Transaction.java b/src/main/java/com/samskivert/depot/Transaction.java new file mode 100644 index 0000000..62d3e2b --- /dev/null +++ b/src/main/java/com/samskivert/depot/Transaction.java @@ -0,0 +1,164 @@ +// +// Depot library - a Java relational persistence library +// https://github.com/threerings/depot/blob/master/LICENSE + +package com.samskivert.depot; + +import com.samskivert.io.PersistenceException; +import com.samskivert.jdbc.ConnectionProvider; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Allows database operations to be performed in a transaction. Transactions can be performed + * manually as follows: + *
{@code
+ * PersistenceContext ctx = ...;
+ * Transaction tx = Transaction.start(ctx);
+ * try {
+ *   _someRepo.updateFoo(...);
+ *   _someOtherRepo.updateBar(...);
+ *   tx.commit();
+ * } catch (RuntimeException re)  {
+ *   tx.rollback();
+ *   throw re;
+ * }
+ * }
+ * + * Or one can make use of the {@link Transaction#perform} helper method: + *
{@code
+ * PersistenceContext ctx = ...;
+ * Transaction.perform(ctx, new Runnable() {
+ *   public void run () {
+ *     _someRepo.updateFoo(...);
+ *     _someOtherRepo.updateBar(...);
+ *   }
+ * });
+ * }
+ * + *

Caveats:

+ * + *

All repositories involved in a transaction must use the same {@code PersistenceContext} + * supplied when starting the transaction. Attempting to use a repository which references a + * different context will cause an exception to be thrown (and the transaction to be aborted, + * assuming you've structured your code correctly).

+ * + *

Transactions may not be nested.

+ */ +public class Transaction { + + /** + * Starts a transaction using the supplied persistence context. You must eventually + * call {@link #commit} or {@link #rollback} on the returned transaction. + */ + public static Transaction start (PersistenceContext ctx) + { + Transaction tx = get(); + if (tx != null) throw new DatabaseException("Nested transactions not supported."); + tx = new Transaction(ctx); + _activeTx.set(tx); + return tx; + } + + /** + * Returns the currently active transaction (on the caller's thread), or null. + */ + public static Transaction get () + { + return _activeTx.get(); + } + + /** + * Returns true if a transaction is currently active on the caller's thread. + */ + public static boolean inTransaction () + { + return get() != null; + } + + /** + * Performs {@code op} inside a transaction, committing it if {@code op} completes + * successfully, rolling it back if {@code op} throws any exceptions. + */ + public static void perform (PersistenceContext ctx, Runnable op) + { + Transaction tx = start(ctx); + try { + op.run(); + tx.commit(); + } catch (RuntimeException re) { + tx.rollback(); + throw re; + } + } + + /** + * Commits this transaction. + */ + public void commit () + { + if (_conn != null) { + checkActive("commit"); + try { + _conn.commit(); + } catch (SQLException sqe) { + throw new DatabaseException("Transaction commit failure", sqe); + } finally { + release(); + } + } + } + + /** + * Aborts this transaction, rolling back any database operations performed thus far. + */ + public void rollback () + { + if (_conn != null) { + checkActive("rollback"); + try { + _conn.rollback(); + } catch (SQLException sqe) { + throw new DatabaseException("Transaction rollback failure", sqe); + } finally { + release(); + } + } + } + + /** The persistence context in which this transaction is operating. */ + public final PersistenceContext ctx; + + Connection getConnection () + throws PersistenceException + { + if (_conn == null) _conn = ctx._conprov.getTxConnection(ctx._ident); + return _conn; + } + + void connectionFailed (SQLException sqe) + { + ctx._conprov.txConnectionFailed(ctx._ident, _conn, sqe); + } + + protected Transaction (PersistenceContext ctx) + { + this.ctx = ctx; + } + + protected void checkActive (String action) { + if (_activeTx.get() != this) throw new IllegalStateException( + "Attempted to " + action + " non-active transaction"); + } + + protected void release () { + ctx._conprov.releaseTxConnection(ctx._ident, _conn); + _conn = null; + _activeTx.set(null); + } + + protected static final ThreadLocal _activeTx = new ThreadLocal(); + + /** The connection being used for this transaction. */ + protected Connection _conn; +}