Bug 1408710 - Pre: Remove ServerLocalSynchronizer* r=rnewman draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Tue, 14 Nov 2017 15:29:57 -0500
changeset 760191 301a7552fe090e29e013d9e73ca9ca34976ee100
parent 760185 580d833df9c44acec686a9fb88b5f27e9d29f68d
child 760192 d789ac49c365336c104c4e66c9826bf387d85465
push id100565
push userbmo:gkruglov@mozilla.com
push dateMon, 26 Feb 2018 23:36:11 +0000
reviewersrnewman
bugs1408710
milestone60.0a1
Bug 1408710 - Pre: Remove ServerLocalSynchronizer* r=rnewman Theoretical ability to setup synchronizers other than server->local never really manifested itself in anything actually useful, and I don't foresee that design choice as currently expressed being useful in the near future. So, let's take a moment to clear up the layers a little bit. MozReview-Commit-ID: 5fIZc6zYeit
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java
mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestServer15RepositorySession.java
mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java
mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestSynchronizer.java
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
@@ -31,17 +31,16 @@ import org.mozilla.gecko.sync.repositori
 import org.mozilla.gecko.sync.repositories.RecordFactory;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
 import org.mozilla.gecko.sync.repositories.RepositoryStateProvider;
 import org.mozilla.gecko.sync.repositories.Server15Repository;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
-import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
 import org.mozilla.gecko.sync.telemetry.TelemetryCollector;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.List;
@@ -225,17 +224,17 @@ public abstract class ServerSyncStage ex
 
   protected void persistConfig(SynchronizerConfiguration synchronizerConfiguration) {
     synchronizerConfiguration.persist(session.config.getBranch(bundlePrefix()));
   }
 
   public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException {
     Repository remote = wrappedServerRepo();
 
-    Synchronizer synchronizer = new ServerLocalSynchronizer();
+    Synchronizer synchronizer = new Synchronizer();
     synchronizer.repositoryA = remote;
     synchronizer.repositoryB = this.getLocalRepository();
     synchronizer.load(getConfig());
 
     return synchronizer;
   }
 
   /**
deleted file mode 100644
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.sync.synchronizer;
-
-/**
- * A <code>SynchronizerSession</code> designed to be used between a remote
- * server and a local repository.
- * <p>
- * See <code>ServerLocalSynchronizerSession</code> for error handling details.
- */
-public class ServerLocalSynchronizer extends Synchronizer {
-  @Override
-  public SynchronizerSession newSynchronizerSession() {
-    return new ServerLocalSynchronizerSession(this, this);
-  }
-}
deleted file mode 100644
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.sync.synchronizer;
-
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.sync.ReflowIsNecessaryException;
-import org.mozilla.gecko.sync.repositories.FetchFailedException;
-import org.mozilla.gecko.sync.repositories.StoreFailedException;
-
-/**
- * A <code>SynchronizerSession</code> designed to be used between a remote
- * server and a local repository.
- * <p>
- * Handles failure cases as follows (in the order they will occur during a sync):
- * <ul>
- * <li>Remote fetch failures abort.</li>
- * <li>Local store failures are ignored.</li>
- * <li>Local fetch failures abort.</li>
- * <li>Remote store failures abort.</li>
- * </ul>
- */
-public class ServerLocalSynchronizerSession extends SynchronizerSession {
-  protected static final String LOG_TAG = "ServLocSynchronizerSess";
-
-  public ServerLocalSynchronizerSession(Synchronizer synchronizer, SynchronizerSessionDelegate delegate) {
-    super(synchronizer, delegate);
-  }
-
-  @Override
-  public void onFirstFlowCompleted(RecordsChannel recordsChannel) {
-    // If a "reflow exception" was thrown, consider this synchronization failed.
-    final ReflowIsNecessaryException reflowException = recordsChannel.getReflowException();
-    if (reflowException != null) {
-      final String message = "Reflow is necessary: " + reflowException;
-      Logger.warn(LOG_TAG, message + " Aborting session.");
-      delegate.onSynchronizeFailed(this, reflowException, message);
-      return;
-    }
-
-    // Fetch failures always abort.
-    int numRemoteFetchFailed = recordsChannel.getFetchFailureCount();
-    if (numRemoteFetchFailed > 0) {
-      final String message = "Got " + numRemoteFetchFailed + " failures fetching remote records!";
-      Logger.warn(LOG_TAG, message + " Aborting session.");
-      delegate.onSynchronizeFailed(this, new FetchFailedException(), message);
-      return;
-    }
-    Logger.trace(LOG_TAG, "No failures fetching remote records.");
-
-    // Local store failures are ignored.
-    int numLocalStoreFailed = recordsChannel.getStoreFailureCount();
-    if (numLocalStoreFailed > 0) {
-      final String message = "Got " + numLocalStoreFailed + " failures storing local records!";
-      Logger.warn(LOG_TAG, message + " Ignoring local store failures and continuing synchronizer session.");
-    } else {
-      Logger.trace(LOG_TAG, "No failures storing local records.");
-    }
-
-    super.onFirstFlowCompleted(recordsChannel);
-  }
-
-  @Override
-  public void onSecondFlowCompleted(RecordsChannel recordsChannel) {
-    // If a "reflow exception" was thrown, consider this synchronization failed.
-    final ReflowIsNecessaryException reflowException = recordsChannel.getReflowException();
-    if (reflowException != null) {
-      final String message = "Reflow is necessary: " + reflowException;
-      Logger.warn(LOG_TAG, message + " Aborting session.");
-      delegate.onSynchronizeFailed(this, reflowException, message);
-      return;
-    }
-
-    // Fetch failures always abort.
-    int numLocalFetchFailed = recordsChannel.getFetchFailureCount();
-    if (numLocalFetchFailed > 0) {
-      final String message = "Got " + numLocalFetchFailed + " failures fetching local records!";
-      Logger.warn(LOG_TAG, message + " Aborting session.");
-      delegate.onSynchronizeFailed(this, new FetchFailedException(), message);
-      return;
-    }
-    Logger.trace(LOG_TAG, "No failures fetching local records.");
-
-    // Remote store failures abort!
-    int numRemoteStoreFailed = recordsChannel.getStoreFailureCount();
-    if (numRemoteStoreFailed > 0) {
-      final String message = "Got " + numRemoteStoreFailed + " failures storing remote records!";
-      Logger.warn(LOG_TAG, message + " Aborting session.");
-      delegate.onSynchronizeFailed(this, new StoreFailedException(), message);
-      return;
-    }
-    Logger.trace(LOG_TAG, "No failures storing remote records.");
-
-    super.onSecondFlowCompleted(recordsChannel);
-  }
-}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java
@@ -1,40 +1,50 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.synchronizer;
 
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ReflowIsNecessaryException;
 import org.mozilla.gecko.sync.SyncException;
 import org.mozilla.gecko.sync.synchronizer.StoreBatchTracker.Batch;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
 import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionFinishDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
 
 import android.content.Context;
 
 /**
  * I coordinate the moving parts of a sync started by
  * {@link Synchronizer#synchronize}.
  *
  * I flow records twice: first from A to B, and then from B to A. I provide
  * fine-grained feedback by calling my delegate's callback methods.
  *
+ * I handle failure cases during flows as follows (in the order they will occur during a sync):
+ * <ul>
+ * <li>Remote fetch failures abort.</li>
+ * <li>Local store failures are ignored.</li>
+ * <li>Local fetch failures abort.</li>
+ * <li>Remote store failures abort.</li>
+ * </ul>
+ *
  * Initialize me by creating me with a Synchronizer and a
  * SynchronizerSessionDelegate. Kick things off by calling `init` with two
  * RepositorySessionBundles, and then call `synchronize` in your `onInitialized`
  * callback.
  *
  * I always call exactly one of my delegate's `onInitialized` or
  * `onSessionError` callback methods from `init`.
  *
@@ -302,16 +312,44 @@ public class SynchronizerSession impleme
 
   /**
    * Called after the first flow completes.
    * <p>
    * By default, any fetch and store failures are ignored.
    * @param recordsChannel the <code>RecordsChannel</code> (for error testing).
    */
   public void onFirstFlowCompleted(RecordsChannel recordsChannel) {
+    // If a "reflow exception" was thrown, consider this synchronization failed.
+    final ReflowIsNecessaryException reflowException = recordsChannel.getReflowException();
+    if (reflowException != null) {
+      final String message = "Reflow is necessary: " + reflowException;
+      Logger.warn(LOG_TAG, message + " Aborting session.");
+      delegate.onSynchronizeFailed(this, reflowException, message);
+      return;
+    }
+
+    // Fetch failures always abort.
+    int numRemoteFetchFailed = recordsChannel.getFetchFailureCount();
+    if (numRemoteFetchFailed > 0) {
+      final String message = "Got " + numRemoteFetchFailed + " failures fetching remote records!";
+      Logger.warn(LOG_TAG, message + " Aborting session.");
+      delegate.onSynchronizeFailed(this, new FetchFailedException(), message);
+      return;
+    }
+    Logger.trace(LOG_TAG, "No failures fetching remote records.");
+
+    // Local store failures are ignored.
+    int numLocalStoreFailed = recordsChannel.getStoreFailureCount();
+    if (numLocalStoreFailed > 0) {
+      final String message = "Got " + numLocalStoreFailed + " failures storing local records!";
+      Logger.warn(LOG_TAG, message + " Ignoring local store failures and continuing synchronizer session.");
+    } else {
+      Logger.trace(LOG_TAG, "No failures storing local records.");
+    }
+
     Logger.trace(LOG_TAG, "First RecordsChannel onFlowCompleted.");
     pendingATimestamp = sessionA.getLastFetchTimestamp();
     storeEndBTimestamp = sessionB.getLastStoreTimestamp();
     Logger.debug(LOG_TAG, "Fetch end is " + pendingATimestamp + ". Store end is " + storeEndBTimestamp + ". Starting next.");
     numInboundRecords.set(recordsChannel.getFetchCount());
     numInboundRecordsStored.set(recordsChannel.getStoreAcceptedCount());
     numInboundRecordsFailed.set(recordsChannel.getStoreFailureCount());
     numInboundRecordsReconciled.set(recordsChannel.getStoreReconciledCount());
@@ -321,16 +359,45 @@ public class SynchronizerSession impleme
 
   /**
    * Called after the second flow completes.
    * <p>
    * By default, any fetch and store failures are ignored.
    * @param recordsChannel the <code>RecordsChannel</code> (for error testing).
    */
   public void onSecondFlowCompleted(RecordsChannel recordsChannel) {
+    // If a "reflow exception" was thrown, consider this synchronization failed.
+    final ReflowIsNecessaryException reflowException = recordsChannel.getReflowException();
+    if (reflowException != null) {
+      final String message = "Reflow is necessary: " + reflowException;
+      Logger.warn(LOG_TAG, message + " Aborting session.");
+      delegate.onSynchronizeFailed(this, reflowException, message);
+      return;
+    }
+
+    // Fetch failures always abort.
+    int numLocalFetchFailed = recordsChannel.getFetchFailureCount();
+    if (numLocalFetchFailed > 0) {
+      final String message = "Got " + numLocalFetchFailed + " failures fetching local records!";
+      Logger.warn(LOG_TAG, message + " Aborting session.");
+      delegate.onSynchronizeFailed(this, new FetchFailedException(), message);
+      return;
+    }
+    Logger.trace(LOG_TAG, "No failures fetching local records.");
+
+    // Remote store failures abort!
+    int numRemoteStoreFailed = recordsChannel.getStoreFailureCount();
+    if (numRemoteStoreFailed > 0) {
+      final String message = "Got " + numRemoteStoreFailed + " failures storing remote records!";
+      Logger.warn(LOG_TAG, message + " Aborting session.");
+      delegate.onSynchronizeFailed(this, new StoreFailedException(), message);
+      return;
+    }
+    Logger.trace(LOG_TAG, "No failures storing remote records.");
+
     Logger.trace(LOG_TAG, "Second RecordsChannel onFlowCompleted.");
     pendingBTimestamp = sessionB.getLastFetchTimestamp();
     storeEndATimestamp = sessionA.getLastStoreTimestamp();
     Logger.debug(LOG_TAG, "Fetch end is " + pendingBTimestamp + ". Store end is " + storeEndATimestamp + ". Finishing.");
     numOutboundRecords.set(recordsChannel.getFetchCount());
     numOutboundRecordsStored.set(recordsChannel.getStoreAcceptedCount());
     numOutboundRecordsFailed.set(recordsChannel.getStoreFailureCount());
     outboundBatches.set(recordsChannel.getStoreBatches());
--- a/mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestServer15RepositorySession.java
+++ b/mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestServer15RepositorySession.java
@@ -23,17 +23,16 @@ import org.mozilla.gecko.sync.net.BaseRe
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.repositories.FetchFailedException;
 import org.mozilla.gecko.sync.repositories.NonPersistentRepositoryStateProvider;
 import org.mozilla.gecko.sync.repositories.Server15Repository;
 import org.mozilla.gecko.sync.repositories.StoreFailedException;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory;
-import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.simpleframework.http.ContentType;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
@@ -115,23 +114,23 @@ public class TestServer15RepositorySessi
     final Server15Repository remote = new Server15Repository(
             COLLECTION, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30),
             getCollectionURL(COLLECTION), authHeaderProvider, infoCollections, infoConfiguration,
             new NonPersistentRepositoryStateProvider());
     KeyBundle collectionKey = new KeyBundle(TEST_USERNAME, SYNC_KEY);
     Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(remote, collectionKey);
     cryptoRepo.recordFactory = new BookmarkRecordFactory();
 
-    final Synchronizer synchronizer = new ServerLocalSynchronizer();
+    final Synchronizer synchronizer = new Synchronizer();
     synchronizer.repositoryA = cryptoRepo;
     synchronizer.repositoryB = local;
 
     data.startHTTPServer(server);
     try {
-      Exception e = TestServerLocalSynchronizer.doSynchronize(synchronizer);
+      Exception e = TestSynchronizer.doSynchronize(synchronizer);
       return e;
     } finally {
       data.stopHTTPServer();
     }
   }
 
   protected String getCollectionURL(String collection) {
     return LOCAL_BASE_URL + "/storage/" + collection;
deleted file mode 100644
--- a/mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-package org.mozilla.android.sync.test;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.android.sync.test.SynchronizerHelpers.BatchFailStoreWBORepository;
-import org.mozilla.android.sync.test.SynchronizerHelpers.BeginErrorWBORepository;
-import org.mozilla.android.sync.test.SynchronizerHelpers.BeginFailedException;
-import org.mozilla.android.sync.test.SynchronizerHelpers.FailFetchWBORepository;
-import org.mozilla.android.sync.test.SynchronizerHelpers.FinishErrorWBORepository;
-import org.mozilla.android.sync.test.SynchronizerHelpers.FinishFailedException;
-import org.mozilla.android.sync.test.SynchronizerHelpers.SerialFailStoreWBORepository;
-import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.testhelpers.TestRunner;
-import org.mozilla.gecko.background.testhelpers.WBORepository;
-import org.mozilla.gecko.background.testhelpers.WaitHelper;
-import org.mozilla.gecko.sync.repositories.FetchFailedException;
-import org.mozilla.gecko.sync.repositories.StoreFailedException;
-import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
-import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
-import org.mozilla.gecko.sync.synchronizer.Synchronizer;
-import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
-
-import java.util.ArrayList;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-@RunWith(TestRunner.class)
-public class TestServerLocalSynchronizer {
-  public static final String LOG_TAG = "TestServLocSync";
-
-  protected Synchronizer getSynchronizer(WBORepository remote, WBORepository local) {
-    BookmarkRecord[] inbounds = new BookmarkRecord[] {
-        new BookmarkRecord("inboundSucc1", "bookmarks", 1, false),
-        new BookmarkRecord("inboundSucc2", "bookmarks", 1, false),
-        new BookmarkRecord("inboundFail1", "bookmarks", 1, false),
-        new BookmarkRecord("inboundSucc3", "bookmarks", 1, false),
-        new BookmarkRecord("inboundFail2", "bookmarks", 1, false),
-        new BookmarkRecord("inboundFail3", "bookmarks", 1, false),
-    };
-    BookmarkRecord[] outbounds = new BookmarkRecord[] {
-        new BookmarkRecord("outboundFail1", "bookmarks", 1, false),
-        new BookmarkRecord("outboundFail2", "bookmarks", 1, false),
-        new BookmarkRecord("outboundFail3", "bookmarks", 1, false),
-        new BookmarkRecord("outboundFail4", "bookmarks", 1, false),
-        new BookmarkRecord("outboundFail5", "bookmarks", 1, false),
-        new BookmarkRecord("outboundFail6", "bookmarks", 1, false),
-    };
-    for (BookmarkRecord inbound : inbounds) {
-      remote.wbos.put(inbound.guid, inbound);
-    }
-    for (BookmarkRecord outbound : outbounds) {
-      local.wbos.put(outbound.guid, outbound);
-    }
-
-    final Synchronizer synchronizer = new ServerLocalSynchronizer();
-    synchronizer.repositoryA = remote;
-    synchronizer.repositoryB = local;
-    return synchronizer;
-  }
-
-  protected static Exception doSynchronize(final Synchronizer synchronizer) {
-    final ArrayList<Exception> a = new ArrayList<Exception>();
-
-    WaitHelper.getTestWaiter().performWait(new Runnable() {
-      @Override
-      public void run() {
-        synchronizer.synchronize(null, new SynchronizerDelegate() {
-          @Override
-          public void onSynchronized(Synchronizer synchronizer) {
-            Logger.trace(LOG_TAG, "Got onSynchronized.");
-            a.add(null);
-            WaitHelper.getTestWaiter().performNotify();
-          }
-
-          @Override
-          public void onSynchronizeFailed(Synchronizer synchronizer, Exception lastException, String reason) {
-            Logger.trace(LOG_TAG, "Got onSynchronizedFailed.");
-            a.add(lastException);
-            WaitHelper.getTestWaiter().performNotify();
-          }
-        });
-      }
-    });
-
-    assertEquals(1, a.size()); // Should not be called multiple times!
-    return a.get(0);
-  }
-
-  @Test
-  public void testNoErrors() {
-    WBORepository remote = new TrackingWBORepository();
-    WBORepository local  = new TrackingWBORepository();
-
-    Synchronizer synchronizer = getSynchronizer(remote, local);
-    assertNull(doSynchronize(synchronizer));
-
-    assertEquals(12, local.wbos.size());
-    assertEquals(12, remote.wbos.size());
-  }
-
-  @Test
-  public void testLocalFetchErrors() {
-    WBORepository remote = new TrackingWBORepository();
-    WBORepository local  = new FailFetchWBORepository(SynchronizerHelpers.FailMode.FETCH);
-
-    Synchronizer synchronizer = getSynchronizer(remote, local);
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(FetchFailedException.class, e.getClass());
-
-    // Neither session gets finished successfully, so all records are dropped.
-    assertEquals(6, local.wbos.size());
-    assertEquals(6, remote.wbos.size());
-  }
-
-  @Test
-  public void testRemoteFetchErrors() {
-    WBORepository remote = new FailFetchWBORepository(SynchronizerHelpers.FailMode.FETCH);
-    WBORepository local  = new TrackingWBORepository();
-
-    Synchronizer synchronizer = getSynchronizer(remote, local);
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(FetchFailedException.class, e.getClass());
-
-    // Neither session gets finished successfully, so all records are dropped.
-    assertEquals(6, local.wbos.size());
-    assertEquals(6, remote.wbos.size());
-  }
-
-  @Test
-  public void testLocalSerialStoreErrorsAreIgnored() {
-    WBORepository remote = new TrackingWBORepository();
-    WBORepository local  = new SerialFailStoreWBORepository(SynchronizerHelpers.FailMode.FETCH);
-
-    Synchronizer synchronizer = getSynchronizer(remote, local);
-    assertNull(doSynchronize(synchronizer));
-
-    assertEquals(9,  local.wbos.size());
-    assertEquals(12, remote.wbos.size());
-  }
-
-  @Test
-  public void testLocalBatchStoreErrorsAreIgnored() {
-    final int BATCH_SIZE = 3;
-
-    Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new BatchFailStoreWBORepository(BATCH_SIZE));
-
-    Exception e = doSynchronize(synchronizer);
-    assertNull(e);
-  }
-
-  @Test
-  public void testRemoteSerialStoreErrorsAreNotIgnored() throws Exception {
-    Synchronizer synchronizer = getSynchronizer(new SerialFailStoreWBORepository(SynchronizerHelpers.FailMode.STORE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
-
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(StoreFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testRemoteBatchStoreErrorsAreNotIgnoredManyBatches() throws Exception {
-    final int BATCH_SIZE = 3;
-
-    Synchronizer synchronizer = getSynchronizer(new BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
-
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(StoreFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testRemoteBatchStoreErrorsAreNotIgnoredOneBigBatch() throws Exception {
-    final int BATCH_SIZE = 20;
-
-    Synchronizer synchronizer = getSynchronizer(new BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
-
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(StoreFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testSessionRemoteBeginError() {
-    Synchronizer synchronizer = getSynchronizer(new BeginErrorWBORepository(), new TrackingWBORepository());
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(BeginFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testSessionLocalBeginError() {
-    Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new BeginErrorWBORepository());
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(BeginFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testSessionRemoteFinishError() {
-    Synchronizer synchronizer = getSynchronizer(new FinishErrorWBORepository(), new TrackingWBORepository());
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(FinishFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testSessionLocalFinishError() {
-    Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new FinishErrorWBORepository());
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(FinishFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testSessionBothBeginError() {
-    Synchronizer synchronizer = getSynchronizer(new BeginErrorWBORepository(), new BeginErrorWBORepository());
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(BeginFailedException.class, e.getClass());
-  }
-
-  @Test
-  public void testSessionBothFinishError() {
-    Synchronizer synchronizer = getSynchronizer(new FinishErrorWBORepository(), new FinishErrorWBORepository());
-    Exception e = doSynchronize(synchronizer);
-    assertNotNull(e);
-    assertEquals(FinishFailedException.class, e.getClass());
-  }
-}
--- a/mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestSynchronizer.java
+++ b/mobile/android/services/src/test/java/org/mozilla/android/sync/test/TestSynchronizer.java
@@ -9,23 +9,26 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSessionDelegate;
 
+import java.util.ArrayList;
 import java.util.Date;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -385,9 +388,210 @@ public class TestSynchronizer {
     recordEquals(ac, guidC, lastModifiedC, deleted, collection);
     recordEquals(ba, guidA, lastModifiedA, deleted, collection);
     recordEquals(bb, guidB, lastModifiedB, deleted, collection);
     recordEquals(bc, guidC, lastModifiedC, deleted, collection);
     recordEquals(aa, ba);
     recordEquals(ab, bb);
     recordEquals(ac, bc);
   }
+
+  static Exception doSynchronize(final Synchronizer synchronizer) {
+    final ArrayList<Exception> a = new ArrayList<Exception>();
+
+    WaitHelper.getTestWaiter().performWait(new Runnable() {
+      @Override
+      public void run() {
+        synchronizer.synchronize(null, new SynchronizerDelegate() {
+          @Override
+          public void onSynchronized(Synchronizer synchronizer) {
+            Logger.trace(LOG_TAG, "Got onSynchronized.");
+            a.add(null);
+            WaitHelper.getTestWaiter().performNotify();
+          }
+
+          @Override
+          public void onSynchronizeFailed(Synchronizer synchronizer, Exception lastException, String reason) {
+            Logger.trace(LOG_TAG, "Got onSynchronizedFailed.");
+            a.add(lastException);
+            WaitHelper.getTestWaiter().performNotify();
+          }
+        });
+      }
+    });
+
+    assertEquals(1, a.size()); // Should not be called multiple times!
+    return a.get(0);
+  }
+
+  private Synchronizer getSynchronizer(WBORepository remote, WBORepository local) {
+    BookmarkRecord[] inbounds = new BookmarkRecord[] {
+            new BookmarkRecord("inboundSucc1", "bookmarks", 1, false),
+            new BookmarkRecord("inboundSucc2", "bookmarks", 1, false),
+            new BookmarkRecord("inboundFail1", "bookmarks", 1, false),
+            new BookmarkRecord("inboundSucc3", "bookmarks", 1, false),
+            new BookmarkRecord("inboundFail2", "bookmarks", 1, false),
+            new BookmarkRecord("inboundFail3", "bookmarks", 1, false),
+    };
+    BookmarkRecord[] outbounds = new BookmarkRecord[] {
+            new BookmarkRecord("outboundFail1", "bookmarks", 1, false),
+            new BookmarkRecord("outboundFail2", "bookmarks", 1, false),
+            new BookmarkRecord("outboundFail3", "bookmarks", 1, false),
+            new BookmarkRecord("outboundFail4", "bookmarks", 1, false),
+            new BookmarkRecord("outboundFail5", "bookmarks", 1, false),
+            new BookmarkRecord("outboundFail6", "bookmarks", 1, false),
+    };
+    for (BookmarkRecord inbound : inbounds) {
+      remote.wbos.put(inbound.guid, inbound);
+    }
+    for (BookmarkRecord outbound : outbounds) {
+      local.wbos.put(outbound.guid, outbound);
+    }
+
+    final Synchronizer synchronizer = new Synchronizer();
+    synchronizer.repositoryA = remote;
+    synchronizer.repositoryB = local;
+    return synchronizer;
+  }
+
+  @Test
+  public void testNoErrors() {
+    WBORepository remote = new TrackingWBORepository();
+    WBORepository local  = new TrackingWBORepository();
+
+    Synchronizer synchronizer = getSynchronizer(remote, local);
+    assertNull(doSynchronize(synchronizer));
+
+    assertEquals(12, local.wbos.size());
+    assertEquals(12, remote.wbos.size());
+  }
+
+  @Test
+  public void testLocalFetchErrors() {
+    WBORepository remote = new TrackingWBORepository();
+    WBORepository local  = new SynchronizerHelpers.FailFetchWBORepository(SynchronizerHelpers.FailMode.FETCH);
+
+    Synchronizer synchronizer = getSynchronizer(remote, local);
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(FetchFailedException.class, e.getClass());
+
+    // Neither session gets finished successfully, so all records are dropped.
+    assertEquals(6, local.wbos.size());
+    assertEquals(6, remote.wbos.size());
+  }
+
+  @Test
+  public void testRemoteFetchErrors() {
+    WBORepository remote = new SynchronizerHelpers.FailFetchWBORepository(SynchronizerHelpers.FailMode.FETCH);
+    WBORepository local  = new TrackingWBORepository();
+
+    Synchronizer synchronizer = getSynchronizer(remote, local);
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(FetchFailedException.class, e.getClass());
+
+    // Neither session gets finished successfully, so all records are dropped.
+    assertEquals(6, local.wbos.size());
+    assertEquals(6, remote.wbos.size());
+  }
+
+  @Test
+  public void testLocalSerialStoreErrorsAreIgnored() {
+    WBORepository remote = new TrackingWBORepository();
+    WBORepository local  = new SynchronizerHelpers.SerialFailStoreWBORepository(SynchronizerHelpers.FailMode.FETCH);
+
+    Synchronizer synchronizer = getSynchronizer(remote, local);
+    assertNull(doSynchronize(synchronizer));
+
+    assertEquals(9,  local.wbos.size());
+    assertEquals(12, remote.wbos.size());
+  }
+
+  @Test
+  public void testLocalBatchStoreErrorsAreIgnored() {
+    final int BATCH_SIZE = 3;
+
+    Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new SynchronizerHelpers.BatchFailStoreWBORepository(BATCH_SIZE));
+
+    Exception e = doSynchronize(synchronizer);
+    assertNull(e);
+  }
+
+  @Test
+  public void testRemoteSerialStoreErrorsAreNotIgnored() throws Exception {
+    Synchronizer synchronizer = getSynchronizer(new SynchronizerHelpers.SerialFailStoreWBORepository(SynchronizerHelpers.FailMode.STORE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(StoreFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testRemoteBatchStoreErrorsAreNotIgnoredManyBatches() throws Exception {
+    final int BATCH_SIZE = 3;
+
+    Synchronizer synchronizer = getSynchronizer(new SynchronizerHelpers.BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(StoreFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testRemoteBatchStoreErrorsAreNotIgnoredOneBigBatch() throws Exception {
+    final int BATCH_SIZE = 20;
+
+    Synchronizer synchronizer = getSynchronizer(new SynchronizerHelpers.BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(StoreFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testSessionRemoteBeginError() {
+    Synchronizer synchronizer = getSynchronizer(new SynchronizerHelpers.BeginErrorWBORepository(), new TrackingWBORepository());
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(SynchronizerHelpers.BeginFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testSessionLocalBeginError() {
+    Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new SynchronizerHelpers.BeginErrorWBORepository());
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(SynchronizerHelpers.BeginFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testSessionRemoteFinishError() {
+    Synchronizer synchronizer = getSynchronizer(new SynchronizerHelpers.FinishErrorWBORepository(), new TrackingWBORepository());
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(SynchronizerHelpers.FinishFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testSessionLocalFinishError() {
+    Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new SynchronizerHelpers.FinishErrorWBORepository());
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(SynchronizerHelpers.FinishFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testSessionBothBeginError() {
+    Synchronizer synchronizer = getSynchronizer(new SynchronizerHelpers.BeginErrorWBORepository(), new SynchronizerHelpers.BeginErrorWBORepository());
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(SynchronizerHelpers.BeginFailedException.class, e.getClass());
+  }
+
+  @Test
+  public void testSessionBothFinishError() {
+    Synchronizer synchronizer = getSynchronizer(new SynchronizerHelpers.FinishErrorWBORepository(), new SynchronizerHelpers.FinishErrorWBORepository());
+    Exception e = doSynchronize(synchronizer);
+    assertNotNull(e);
+    assertEquals(SynchronizerHelpers.FinishFailedException.class, e.getClass());
+  }
 }