syncbase.js: runInBatch retries and tweaks for consistency

Change-Id: I530863702ae876882296d49c9cd34e43f79bca64
diff --git a/src/nosql/batch.js b/src/nosql/batch.js
index cd5fa36..157a21d 100644
--- a/src/nosql/batch.js
+++ b/src/nosql/batch.js
@@ -8,7 +8,7 @@
  * @summary
  * runInBatch runs a function with a newly created batch. If the function
  * errors, the batch is aborted. If the function succeeds, the batch is
- * committed. If an error occurs during commit, then the batch is aborted.
+ * committed.
  *
  * @param {module:vanadium.context.Context} ctx Vanadium context.
  * @param {module:syncbase.database.Database} db Database.
@@ -17,41 +17,39 @@
  * batch.
  * @param {module:vanadium~voidCb} cb Callback that will be called after the
  * batch has been committed or aborted.
- *
- * TODO(nlacasse): Add retry loop.
  */
 function runInBatch(ctx, db, opts, fn, cb) {
-  db.beginBatch(ctx, opts, function(err, batchDb) {
-    if (err) {
-      return cb(err);
-    }
-
-    function onError(err) {
-      batchDb.abort(ctx, function() {
-        cb(err);
-      });
-    }
-
-    function onSuccess() {
-      // TODO(nlacasse): Commit() can fail for a number of reasons, e.g. RPC
-      // failure or ErrConcurrentTransaction. Depending on the cause of
-      // failure, it may be desirable to retry the Commit() and/or to call
-      // Abort(). For now, we always abort on a failed commit.
-      batchDb.commit(ctx, function(commitErr) {
-        if (commitErr) {
-          return onError(commitErr);
-        }
-        return cb(null);
-      });
-    }
-
-    fn(batchDb, function(err) {
+  function attempt(cb) {
+    db.beginBatch(ctx, opts, function(err, batchDb) {
       if (err) {
-        return onError(err);
+        return cb(err);
       }
-      onSuccess();
+      fn(batchDb, function(err) {
+        if (err) {
+          return batchDb.abort(ctx, function() {
+            return cb(err);  // return fn error, not abort error
+          });
+        }
+        // TODO(sadovsky): commit() can fail for a number of reasons, e.g. RPC
+        // failure or ErrConcurrentTransaction. Depending on the cause of
+        // failure, it may be desirable to retry the commit() and/or to call
+        // abort().
+        batchDb.commit(ctx, cb);
+      });
     });
-  });
+  }
+
+  function retryLoop(i) {
+    attempt(function(err) {
+      if (err && i < 2) {
+        retryLoop(i + 1);
+      } else {
+        cb(err);
+      }
+    });
+  }
+
+  retryLoop(0);
 }
 
 /**
diff --git a/test/integration/test-run-in-batch.js b/test/integration/test-run-in-batch.js
index 84011f3..cc29d5a 100644
--- a/test/integration/test-run-in-batch.js
+++ b/test/integration/test-run-in-batch.js
@@ -77,7 +77,7 @@
   });
 });
 
-test('runInBatch aborts if commit fails', function(t) {
+test('runInBatch does not abort if commit fails', function(t) {
   var ctx = {};
   var db = new MockDb(true);
 
@@ -90,7 +90,7 @@
 
     t.ok(db.batchDb, 'batch db is created');
     t.ok(db.batchDb.commitCalled, 'batchDb.commit() was called');
-    t.ok(db.batchDb.abortCalled, 'batchDb.abort() was called');
+    t.notok(db.batchDb.abortCalled, 'batchDb.abort() was not called');
 
     t.end();
   });