Bug 1253111 - WIP Part 3: Introduce BatchingAtomicUploader and use it when server supports batching mode r=rnewman draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Fri, 29 Jul 2016 19:08:13 -0700
changeset 394580 bdaf9763e6e25523734026511c5f3b40697a3ecb
parent 394579 02a86a317942210296657a6bf916143f6bd387bb
child 526847 dfe8582aa60db31e32ae59e463606a42213d0e2c
push id24610
push usergkruglov@mozilla.com
push dateSat, 30 Jul 2016 02:12:47 +0000
reviewersrnewman
bugs1253111
milestone50.0a1
Bug 1253111 - WIP Part 3: Introduce BatchingAtomicUploader and use it when server supports batching mode r=rnewman MozReview-Commit-ID: 3RozuZkWE4R
mobile/android/base/android-services.mozbuild
mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingAtomicUploader.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingAtomicUploaderDelegate.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -1009,16 +1009,18 @@ sync_java_files = [TOPSRCDIR + '/mobile/
     'sync/repositories/Repository.java',
     'sync/repositories/RepositorySession.java',
     'sync/repositories/RepositorySessionBundle.java',
     'sync/repositories/Server11Repository.java',
     'sync/repositories/Server11RepositorySession.java',
     'sync/repositories/StoreFailedException.java',
     'sync/repositories/StoreTracker.java',
     'sync/repositories/StoreTrackingRepositorySession.java',
+    'sync/repositories/uploaders/BatchingAtomicUploader.java',
+    'sync/repositories/uploaders/BatchingAtomicUploaderDelegate.java',
     'sync/repositories/uploaders/MayUploadProvider.java',
     'sync/repositories/uploaders/RecordUploader.java',
     'sync/repositories/uploaders/RecordUploadRunnable.java',
     'sync/repositories/uploaders/ServerUploader.java',
     'sync/repositories/uploaders/UnsafeBatchingUploader.java',
     'sync/Server11PreviousPostFailedException.java',
     'sync/Server11RecordPostFailedException.java',
     'sync/setup/activities/ActivityUtils.java',
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java
@@ -38,17 +38,17 @@ public class MozResponse {
     return this.response;
   }
 
   public int getStatusCode() {
     return this.response.getStatusLine().getStatusCode();
   }
 
   public boolean wasSuccessful() {
-    return this.getStatusCode() == 200;
+    return this.getStatusCode() == 200 || this.getStatusCode() == 202;
   }
 
   public boolean isInvalidAuthentication() {
     return this.getStatusCode() == HttpStatus.SC_UNAUTHORIZED;
   }
 
   /**
    * Fetch the content type of the HTTP response body.
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java
@@ -2,31 +2,32 @@
  * 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.repositories;
 
 import java.net.URISyntaxException;
 
 import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 
 /**
  * A kind of Server11Repository that supports explicit setting of limit and sort on operations.
  *
  * @author rnewman
  *
  */
 public class ConstrainedServer11Repository extends Server11Repository {
 
   private String sort = null;
   private long limit  = -1;
 
-  public ConstrainedServer11Repository(String collection, String storageURL, AuthHeaderProvider authHeaderProvider, InfoCollections infoCollections, long limit, String sort) throws URISyntaxException {
-    super(collection, storageURL, authHeaderProvider, infoCollections);
+  public ConstrainedServer11Repository(String collection, String storageURL, AuthHeaderProvider authHeaderProvider, InfoCollections infoCollections, InfoConfiguration infoConfiguration, long limit, String sort) throws URISyntaxException {
+    super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration);
     this.limit = limit;
     this.sort  = sort;
   }
 
   @Override
   protected String getDefaultSort() {
     return sort;
   }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java
@@ -4,57 +4,62 @@
 
 package org.mozilla.gecko.sync.repositories;
 
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 
 import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 
 import android.content.Context;
+import android.support.annotation.NonNull;
 
 /**
  * A Server11Repository implements fetching and storing against the Sync 1.1 API.
  * It doesn't do crypto: that's the job of the middleware.
  *
  * @author rnewman
  */
 public class Server11Repository extends Repository {
   protected String collection;
   protected URI collectionURI;
   protected final AuthHeaderProvider authHeaderProvider;
   protected final InfoCollections infoCollections;
 
+  private final InfoConfiguration infoConfiguration;
+
   /**
    * Construct a new repository that fetches and stores against the Sync 1.1. API.
    *
    * @param collection name.
    * @param storageURL full URL to storage endpoint.
    * @param authHeaderProvider to use in requests; may be null.
    * @param infoCollections instance; must not be null.
    * @throws URISyntaxException
    */
-  public Server11Repository(String collection, String storageURL, AuthHeaderProvider authHeaderProvider, InfoCollections infoCollections) throws URISyntaxException {
+  public Server11Repository(@NonNull String collection, @NonNull String storageURL, AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections, @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException {
     if (collection == null) {
       throw new IllegalArgumentException("collection must not be null");
     }
     if (storageURL == null) {
       throw new IllegalArgumentException("storageURL must not be null");
     }
     if (infoCollections == null) {
       throw new IllegalArgumentException("infoCollections must not be null");
     }
     this.collection = collection;
     this.collectionURI = new URI(storageURL + (storageURL.endsWith("/") ? collection : "/" + collection));
     this.authHeaderProvider = authHeaderProvider;
     this.infoCollections = infoCollections;
+    this.infoConfiguration = infoConfiguration;
   }
 
   @Override
   public void createSession(RepositorySessionCreationDelegate delegate,
                             Context context) {
     delegate.onSessionCreated(new Server11RepositorySession(this));
   }
 
@@ -114,9 +119,13 @@ public class Server11Repository extends 
 
   public AuthHeaderProvider getAuthHeaderProvider() {
     return authHeaderProvider;
   }
 
   public boolean updateNeeded(long lastSyncTimestamp) {
     return infoCollections.updateNeeded(collection, lastSyncTimestamp);
   }
+
+  public InfoConfiguration getInfoConfiguration() {
+    return infoConfiguration;
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java
@@ -20,16 +20,17 @@ import org.mozilla.gecko.sync.net.SyncSt
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 import org.mozilla.gecko.sync.repositories.uploaders.RecordUploader;
+import org.mozilla.gecko.sync.repositories.uploaders.BatchingAtomicUploader;
 import org.mozilla.gecko.sync.repositories.uploaders.ServerUploader;
 import org.mozilla.gecko.sync.repositories.uploaders.UnsafeBatchingUploader;
 
 public class Server11RepositorySession extends RepositorySession {
   public static final String LOG_TAG = "Server11Session";
 
   /**
    * Used to track outstanding requests, so that we can abort them as needed.
@@ -157,17 +158,21 @@ public class Server11RepositorySession e
   }
 
   @Override
   public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
     Logger.debug(LOG_TAG, "Setting store delegate to " + delegate);
     this.delegate = delegate;
 
     // Now that we have the delegate, we can initialize our uploader.
+    if (serverRepository.getInfoConfiguration().isBatchingModeSupported()) {
+      uploader = new BatchingAtomicUploader(this, storeWorkQueue, delegate);
+    } else {
       uploader = new UnsafeBatchingUploader(this, storeWorkQueue, delegate);
+    }
   }
 
   private String flattenIDs(String[] guids) {
     // Consider using Utils.toDelimitedString if and when the signature changes
     // to Collection<String> guids.
     if (guids.length == 0) {
       return "";
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingAtomicUploader.java
@@ -0,0 +1,162 @@
+/* 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.repositories.uploaders;
+
+import android.net.Uri;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.Server11RecordPostFailedException;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.lang.reflect.Array;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Uploader which implements batching semantics introduced in Sync 1.5.
+ *
+ * Records are uploaded in batches according to limits defined in InfoConfiguration object.
+ * Batch ID is returned once first post succeeds, and is maintained across uploads, to ensure our
+ * uploads are grouped together. Last batch commits the series (with commit=true GET param).
+ *
+ * Last-Modified header returned with the first post success is maintained across uploads, to guard
+ * against concurrent-modification errors (different uploader commits before we're done).
+ */
+public class BatchingAtomicUploader extends RecordUploader {
+    private static final String LOG_TAG = "BatchingAtomicUploader";
+
+    private static final String QUERY_PARAM_BATCH = "batch";
+    private static final String QUERY_PARAM_TRUE = "true";
+    private static final String QUERY_PARAM_BATCH_COMMIT = "commit";
+
+    private final InfoConfiguration configuration;
+    private final Uri collectionUri;
+
+    // These will be set after a first response from the server.
+    private Integer batchId;
+    private String lastModified;
+
+    private final ArrayList<String> successRecordGuids = new ArrayList<>();
+    private final ArrayList<String> failedRecordGuids = new ArrayList<>();
+
+    public BatchingAtomicUploader(final Server11RepositorySession repositorySession, final ExecutorService workQueue, final RepositorySessionStoreDelegate sessionStoreDelegate) {
+        super(repositorySession, workQueue, sessionStoreDelegate);
+        configuration = repositorySession.getServerRepository().getInfoConfiguration();
+        collectionUri = Uri.parse(repositorySession.getServerRepository().collectionURI().toString());
+    }
+
+    @Override
+    public void flushRecords(final boolean lastBatch) {
+        flush(batchId, lastBatch);
+    }
+
+    @Override
+    public boolean shouldFlush(final int delta, final int byteCount, final ArrayList<byte[]> recordsBuffer) {
+        return (delta + byteCount > configuration.getRequestLimitValue(InfoConfiguration.MAX_POST_BYTES)) ||
+                (recordsBuffer.size() >= configuration.getRequestLimitValue(InfoConfiguration.MAX_POST_RECORDS));
+    }
+
+    private void flush(final Integer batchId, final boolean isLastBatch) {
+        final Uri.Builder uriBuilder = collectionUri.buildUpon();
+
+        if (batchId != null) {
+            uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, batchId.toString());
+        } else {
+            uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, QUERY_PARAM_TRUE);
+        }
+
+        if (isLastBatch) {
+            uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH_COMMIT, QUERY_PARAM_TRUE);
+        }
+
+        final URI uriToPost;
+        try {
+            uriToPost = new URI(uriBuilder.build().toString());
+        // Should never happen, Uri.Builder should produce a well-formed URI.
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("Could not create URI for posting a batch, bailing.");
+        }
+
+        final ArrayList<byte[]> outgoing = recordsBuffer;
+        final ArrayList<String> outgoingGuids = recordGuidsBuffer;
+
+        // TODO is there any chance of leaking BatchingAtomicUploaderDelegate through a runnable which could significantly outlive this uploader?
+        workQueue.execute(new RecordUploadRunnable(
+                new BatchingAtomicUploaderMayUploadProvider(),
+                uriToPost,
+                new BatchingAtomicUploaderDelegate(this, outgoingGuids, isLastBatch),
+                outgoing,
+                byteCount
+        ));
+
+        recordsBuffer = new ArrayList<>();
+        recordGuidsBuffer = new ArrayList<>();
+        byteCount = PER_BATCH_OVERHEAD;
+    }
+
+    /**
+     * We've been told by our delegate that our whole batch succeeded. Let's inform store delegate.
+     * @param response success response to our commit post
+     */
+    public void lastBatchSucceeded(final SyncStorageResponse response) {
+        final long normalizedTimestamp = getNormalizedTimestamp(LOG_TAG, response);
+        Logger.trace(LOG_TAG, "Passing back upload X-Weave-Timestamp: " + normalizedTimestamp);
+        bumpUploadTimestamp(normalizedTimestamp);
+
+        for (String guid : successRecordGuids) {
+            sessionStoreDelegate.onRecordStoreSucceeded(guid);
+        }
+    }
+
+    public synchronized Integer getBatchId() {
+        return batchId;
+    }
+
+    public synchronized void setBatchId(Integer batchId) {
+        this.batchId = batchId;
+    }
+
+    public synchronized String getLastModified() {
+        return lastModified;
+    }
+
+    public synchronized void setLastModified(final String lastModified) {
+        this.lastModified = lastModified;
+    }
+
+    public synchronized void recordSucceeded(final String recordGuid) {
+        successRecordGuids.add(recordGuid);
+    }
+
+    public synchronized void recordFailed(final String recordGuid) {
+        recordFailed(new Server11RecordPostFailedException(), recordGuid);
+    }
+
+    public synchronized void recordFailed(final Exception e, final String recordGuid) {
+        // TODO should we keep track of these records?
+        // can we recover from this at all, without bailing out of syncing this stage?
+        sessionStoreDelegate.onRecordStoreFailed(e, recordGuid);
+    }
+
+    public Server11RepositorySession getRepositorySession() {
+        return repositorySession;
+    }
+
+    // TODO keep track of any failed batches, and probably cascade-fail the rest
+    private class BatchingAtomicUploaderMayUploadProvider implements MayUploadProvider {
+        public boolean mayUpload() {
+            return true;
+        }
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingAtomicUploaderDelegate.java
@@ -0,0 +1,167 @@
+/* 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.repositories.uploaders;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import java.util.ArrayList;
+
+public class BatchingAtomicUploaderDelegate implements SyncStorageRequestDelegate {
+    private static final String LOG_TAG = "BatchingAtomicUploaderDelegate";
+
+    private static final String X_LAST_MODIFIED = "X-Last-Modified";
+    private static final String KEY_BATCH = "batch";
+
+    private final BatchingAtomicUploader uploader;
+    private final ArrayList<String> postedRecordGuids;
+    private final boolean isLastBatch;
+
+    public BatchingAtomicUploaderDelegate(final BatchingAtomicUploader uploader, final ArrayList<String> postedRecordGuids, final boolean isLastBatch) {
+        this.uploader = uploader;
+        this.postedRecordGuids = postedRecordGuids;
+        this.isLastBatch = isLastBatch;
+    }
+
+    @Override
+    public AuthHeaderProvider getAuthHeaderProvider() {
+        return uploader.getRepositorySession().getServerRepository().getAuthHeaderProvider();
+    }
+
+    @Override
+    public String ifUnmodifiedSince() {
+        return uploader.getLastModified();
+    }
+
+    @Override
+    public void handleRequestSuccess(final SyncStorageResponse response) {
+        if (response.getStatusCode() != 202) {
+            handleRequestError(
+                    new IllegalStateException("Received non-202 success code while in batching mode")
+            );
+            return;
+        }
+
+        if (!response.httpResponse().containsHeader(X_LAST_MODIFIED)) {
+            handleRequestError(
+                    new IllegalStateException("Response did not have a Last-Modified header")
+            );
+            return;
+        }
+
+        final ExtendedJSONObject body;
+        try {
+            body = response.jsonObjectBody(); // jsonObjectBody() throws or returns non-null.
+        } catch (Exception e) {
+            Logger.error(LOG_TAG, "Got exception parsing POST success body.", e);
+            this.handleRequestError(e);
+            return;
+        }
+
+        if (!body.containsKey(KEY_BATCH)) {
+            handleRequestError(
+                    new IllegalStateException("Response did not have a batch ID")
+            );
+            return;
+        }
+
+        final Integer currentBatchId = uploader.getBatchId();
+        final Integer newBatchId = body.getIntegerSafely(KEY_BATCH);
+
+        if (currentBatchId == null) {
+            uploader.setBatchId(newBatchId);
+
+        } else if (!currentBatchId.equals(newBatchId)) {
+            handleRequestError(
+                    new IllegalStateException("Response had a batch ID different from previous responses")
+            );
+            return;
+        }
+
+        final String currentLastModified = uploader.getLastModified();
+        final String newLastModified = response.httpResponse().getFirstHeader(X_LAST_MODIFIED).getValue();
+
+        if (currentLastModified == null) {
+            uploader.setLastModified(newLastModified);
+
+        } else if (!isLastBatch && !currentLastModified.equals(newLastModified)) {
+            handleRequestError(
+                    new IllegalStateException("Last-Modified timestamp changed mid-way through the batch")
+            );
+            return;
+
+        } else if (isLastBatch && currentLastModified.equals(newLastModified)) {
+            handleRequestError(
+                    new IllegalStateException("Last-Modified timestamp didn't change after committing")
+            );
+            return;
+        }
+
+        // All looks good up to this point, let's process success/failed arrays now
+
+        JSONArray success;
+        try {
+            success = body.getArray("success");
+        } catch (NonArrayJSONException e) {
+            handleRequestError(e);
+            return;
+        }
+
+        if (success != null && success.size() > 0) {
+            Logger.trace(LOG_TAG, "Successful records: " + success.toString());
+            for (Object o : success) {
+                try {
+                    uploader.recordSucceeded((String) o);
+                } catch (ClassCastException e) {
+                    Logger.error(LOG_TAG, "Got exception parsing POST success guid.", e);
+                    // Not much to be done.
+
+                    // TODO this feels off. We should handle this type of validation explicitely.
+                    // TODO should we pass these types of errors to handleRequestError?
+                }
+            }
+        }
+        success = null;
+
+        ExtendedJSONObject failed;
+        try {
+            failed = body.getObject("failed");
+        } catch (NonObjectJSONException e) {
+            handleRequestError(e);
+            return;
+        }
+
+        if (failed != null && failed.object.size() > 0) {
+            Logger.debug(LOG_TAG, "Failed records: " + failed.object.toString());
+            for (String guid : failed.keySet()) {
+                uploader.recordFailed(guid);
+            }
+        }
+        failed = null;
+
+        if (isLastBatch) {
+            uploader.lastBatchSucceeded(response);
+        }
+    }
+
+    @Override
+    public void handleRequestFailure(final SyncStorageResponse response) {
+        this.handleRequestError(new HTTPFailureException(response));
+    }
+
+    @Override
+    public void handleRequestError(Exception e) {
+        for (String guid : postedRecordGuids) {
+            uploader.recordFailed(e, guid);
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java
@@ -45,16 +45,17 @@ public class AndroidBrowserBookmarksServ
     AuthHeaderProvider authHeaderProvider = session.getAuthHeaderProvider();
     final JSONRecordFetcher countsFetcher = new JSONRecordFetcher(session.config.infoCollectionCountsURL(), authHeaderProvider);
     String collection = getCollection();
     return new SafeConstrainedServer11Repository(
         collection,
         session.config.storageURL(),
         session.getAuthHeaderProvider(),
         session.config.infoCollections,
+        session.config.infoConfiguration,
         BOOKMARKS_REQUEST_LIMIT,
         BOOKMARKS_SORT,
         countsFetcher);
   }
 
   @Override
   protected Repository getLocalRepository() {
     return new AndroidBrowserBookmarksRepository();
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java
@@ -45,16 +45,17 @@ public class AndroidBrowserHistoryServer
   @Override
   protected Repository getRemoteRepository() throws URISyntaxException {
     String collection = getCollection();
     return new ConstrainedServer11Repository(
                                              collection,
                                              session.config.storageURL(),
                                              session.getAuthHeaderProvider(),
                                              session.config.infoCollections,
+                                             session.config.infoConfiguration,
                                              HISTORY_REQUEST_LIMIT,
                                              HISTORY_SORT);
   }
 
   @Override
   protected RecordFactory getRecordFactory() {
     return new HistoryRecordFactory();
   }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java
@@ -40,16 +40,17 @@ public class FormHistoryServerSyncStage 
   @Override
   protected Repository getRemoteRepository() throws URISyntaxException {
     String collection = getCollection();
     return new ConstrainedServer11Repository(
         collection,
         session.config.storageURL(),
         session.getAuthHeaderProvider(),
         session.config.infoCollections,
+        session.config.infoConfiguration,
         FORM_HISTORY_REQUEST_LIMIT,
         FORM_HISTORY_SORT);
   }
 
   @Override
   protected Repository getLocalRepository() {
     return new FormHistoryRepositorySession.FormHistoryRepository();
   }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.stage;
 
 import java.net.URISyntaxException;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.InfoCounts;
 import org.mozilla.gecko.sync.JSONRecordFetcher;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 
@@ -32,21 +33,22 @@ public class SafeConstrainedServer11Repo
 
   // This can be lazily evaluated if we need it.
   private final JSONRecordFetcher countFetcher;
 
   public SafeConstrainedServer11Repository(String collection,
                                            String storageURL,
                                            AuthHeaderProvider authHeaderProvider,
                                            InfoCollections infoCollections,
+                                           InfoConfiguration infoConfiguration,
                                            long limit,
                                            String sort,
                                            JSONRecordFetcher countFetcher)
     throws URISyntaxException {
-    super(collection, storageURL, authHeaderProvider, infoCollections, limit, sort);
+    super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration, limit, sort);
     if (countFetcher == null) {
       throw new IllegalArgumentException("countFetcher must not be null");
     }
     this.countFetcher = countFetcher;
   }
 
   @Override
   public void createSession(RepositorySessionCreationDelegate delegate,
--- 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
@@ -140,17 +140,18 @@ public abstract class ServerSyncStage ex
   protected abstract RecordFactory getRecordFactory();
 
   // Override this in subclasses.
   protected Repository getRemoteRepository() throws URISyntaxException {
     String collection = getCollection();
     return new Server11Repository(collection,
                                   session.config.storageURL(),
                                   session.getAuthHeaderProvider(),
-                                  session.config.infoCollections);
+                                  session.config.infoCollections,
+                                  session.config.infoConfiguration);
   }
 
   /**
    * Return a Crypto5Middleware-wrapped Server11Repository.
    *
    * @throws NoCollectionKeysSetException
    * @throws URISyntaxException
    */