TODOs: Try to join with exponential backoff

Previously, we would only attempt to join a syncgroup twice. Now,
we'll do it up to 5 times (though we can change that default). These
tries are done with exponential backoff, so theoretically there is
no need for a limit.

The reason this wasn't included before is that it seemed to trigger
sync breakages; however, those problems have been resolved now that
syncgroup ad failures don't break sync anymore.

Change-Id: I7ba05a178c243718a24b5c2920a9a510be48fe25
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
index 33bfa9e..04958b0 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
@@ -19,6 +19,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
 import javax.annotation.Nullable;
@@ -54,6 +55,8 @@
     private static final String
             TAG = SyncbaseMain.class.getSimpleName();
 
+    private static int DEFAULT_MAX_JOIN_ATTEMPTS = 5;
+
     private final IdGenerator mIdGenerator = new IdGenerator(IdAlphabets.COLLECTION_ID, true);
     private final Map<String, MainListTracker> mTaskTrackers = new HashMap<>();
 
@@ -90,27 +93,7 @@
 
                     Log.d(TAG, "Found a list id from userdata watch: " + listId + " with owner: "
                             + ownerBlessing);
-                    trap(Futures.catchingAsync(joinListSyncgroup(listId, ownerBlessing),
-                            SyncgroupJoinFailedException.class, new
-                                    AsyncFunction<SyncgroupJoinFailedException, SyncgroupSpec>() {
-                        public ListenableFuture<SyncgroupSpec> apply(@Nullable
-                                                                     SyncgroupJoinFailedException
-                                                                             input) throws
-                                Exception {
-                            Log.d(TAG, "Join failed. Sleeping and trying again: " + listId);
-                            return sExecutor.schedule(new Callable<SyncgroupSpec>() {
-
-                                @Override
-                                public SyncgroupSpec call() throws Exception {
-                                    Log.d(TAG, "Sleep done. Trying again: " + listId);
-
-                                    // If this errors, then we will not get another chance to see
-                                    // this syncgroup until the app is restarted.
-                                    return joinListSyncgroup(listId, ownerBlessing).get();
-                                }
-                            }, RETRY_DELAY, TimeUnit.MILLISECONDS);
-                        }
-                    }));
+                    trap(joinWithBackoff(listId, ownerBlessing));
 
                     MainListTracker listTracker = new MainListTracker(getVContext(), getDatabase(),
                             listId, listener);
@@ -170,6 +153,47 @@
                 CLOUD_NAME, CLOUD_BLESSING, memberInfo);
     }
 
+    private ListenableFuture<SyncgroupSpec> joinWithBackoff(String listId, String ownerBlessing) {
+        return joinWithBackoff(listId, ownerBlessing, 0, DEFAULT_MAX_JOIN_ATTEMPTS);
+    }
+
+    private ListenableFuture<SyncgroupSpec> joinWithBackoff(final String listId, final String
+            ownerBlessing, final int numTimes, final int limit) {
+        final String debugString = (numTimes + 1) + "/" + limit + " for: " + listId;
+        Log.d(TAG, "Join attempt " + debugString);
+        if (numTimes + 1 == limit) { // final attempt!
+            return joinListSyncgroup(listId, ownerBlessing);
+        }
+        final long delay = RETRY_DELAY * (1 << numTimes);
+        return Futures.catchingAsync(
+                joinListSyncgroup(listId, ownerBlessing),
+                SyncgroupJoinFailedException.class,
+                new AsyncFunction<SyncgroupJoinFailedException, SyncgroupSpec>() {
+                    public ListenableFuture<SyncgroupSpec> apply(@Nullable
+                                                                 SyncgroupJoinFailedException
+                                                                         input) {
+                        Log.d(TAG, "Join failed. Sleeping " + debugString + " with delay " + delay);
+                        return sExecutor.schedule(new Callable<SyncgroupSpec>() {
+
+
+                            @Override
+                            public SyncgroupSpec call() {
+                                Log.d(TAG, "Sleep done. Retry " + debugString);
+
+                                // If this errors, then we will not get another chance to
+                                // see this syncgroup until the app is restarted.
+                                try {
+                                    return joinWithBackoff(listId, ownerBlessing,
+                                            numTimes + 1, limit).get();
+                                } catch (InterruptedException | ExecutionException e) {
+                                    return null;
+                                }
+                            }
+                        }, delay, TimeUnit.MILLISECONDS);
+                    }
+                });
+    }
+
     private ListenableFuture<Void> createListSyncgroup(Id id) {
         String listId = id.getName();
         final String sgName = computeListSyncgroupName(listId);
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
index 13365d4..87ae010 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
@@ -99,7 +99,7 @@
     protected static final String LISTS_PREFIX = "lists_";
     protected static final long
             SHORT_TIMEOUT = 2500,
-            RETRY_DELAY = 2000,
+            RETRY_DELAY = 300,
             MEMBER_TIMER_DELAY = 100,
             MEMBER_TIMER_PERIOD = 5000;
     public static final String