Add support for restartable transactions when concurrency errors occur

This way applications can more easily recover from concurrency problems
at the database level by retrying the sequence of operations they need
to complete a task.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/client/OrmRunnable.java b/src/main/java/com/google/gwtorm/client/OrmRunnable.java
new file mode 100644
index 0000000..5083928
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/client/OrmRunnable.java
@@ -0,0 +1,43 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+/**
+ * Runs within an isolated database transaction, retrying if necessary.
+ * <p>
+ * The {@link Schema} is free to invoke this runnable multiple times if an
+ * {@link OrmConcurrencyException} is thrown by the run method.
+ * 
+ * @param <T> type of object the run method returns.
+ * @param <S> type of schema the run method needs to perform its work.
+ */
+public interface OrmRunnable<T, S extends Schema> {
+  /**
+   * Execute the task once.
+   * <p>
+   * Implementations should read any state they need within the method, to
+   * ensure they are looking at the most current copy of the data from the
+   * database. If a method is invoked a second time to recover from a
+   * concurrency error it would need to read the data again.
+   * 
+   * @param db active schema handle to query through, and make updates on.
+   * @param txn the current transaction handle. Commit is invoked by the caller.
+   * @param retry true if this is not the first attempt to execute this task.
+   * @return the return value of the function, if any.
+   * @throws OrmException any database error. {@link OrmConcurrencyException}
+   *         may cause the transaction to be retried.
+   */
+  T run(S db, Transaction txn, boolean retry) throws OrmException;
+}
diff --git a/src/main/java/com/google/gwtorm/client/Schema.java b/src/main/java/com/google/gwtorm/client/Schema.java
index b28da3f..84d52d5 100644
--- a/src/main/java/com/google/gwtorm/client/Schema.java
+++ b/src/main/java/com/google/gwtorm/client/Schema.java
@@ -77,6 +77,17 @@
   Transaction beginTransaction() throws OrmException;
 
   /**
+   * Execute a task within a transaction, restarting it if necessary.
+   * 
+   * @param <T> type of return value for the task.
+   * @param <S> type of <code>this</code>.
+   * @param task the task to execute.
+   * @return the return value of the task.
+   * @throws OrmException the task could not be completed successfully.
+   */
+  <T, S extends Schema> T run(OrmRunnable<T, S> task) throws OrmException;
+
+  /**
    * Close the schema and release all resources.
    */
   void close();
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
index 17b6cc0..6645c79 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
@@ -14,7 +14,9 @@
 
 package com.google.gwtorm.jdbc;
 
+import com.google.gwtorm.client.OrmConcurrencyException;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.OrmRunnable;
 import com.google.gwtorm.client.Schema;
 import com.google.gwtorm.client.Transaction;
 import com.google.gwtorm.schema.RelationModel;
@@ -29,6 +31,7 @@
 
 /** Internal base class for implementations of {@link Schema}. */
 public abstract class JdbcSchema implements Schema {
+  private static final int MAX_TRIES = 10;
   private final Database<?> dbDef;
   private Connection conn;
 
@@ -45,6 +48,27 @@
     return dbDef.getDialect();
   }
 
+  public <T, S extends Schema> T run(final OrmRunnable<T, S> task)
+      throws OrmException {
+    for (int attempts = 1;; attempts++) {
+      try {
+        final Transaction txn = beginTransaction();
+        try {
+          return task.run((S) this, txn, attempts > 1);
+        } finally {
+          txn.commit();
+        }
+      } catch (OrmConcurrencyException err) {
+        // If the commit failed, our implementation rolled back automatically.
+        //
+        if (attempts < MAX_TRIES) {
+          continue;
+        }
+        throw err;
+      }
+    }
+  }
+
   public void createSchema() throws OrmException {
     final SqlDialect dialect = dbDef.getDialect();
     final SchemaModel model = dbDef.getSchemaModel();