Bug 1253111 - Part 1: Introduce new sync stage to handle info/configuration r=rnewman draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Mon, 15 Aug 2016 18:35:17 -0700
changeset 407735 e029d60a063b2e1dd9061362c4d4a647263dd3ec
parent 407722 fa6f91ffb4965302f316635e91d456c8103e08c2
child 407736 4a6a2db595fe72473b033acd4f3180fa799319d4
push id28035
push usergkruglov@mozilla.com
push dateTue, 30 Aug 2016 23:11:36 +0000
reviewersrnewman
bugs1253111
milestone51.0a1
Bug 1253111 - Part 1: Introduce new sync stage to handle info/configuration r=rnewman MozReview-Commit-ID: 7MOgR7A5SOF
mobile/android/base/android-services.mozbuild
mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -888,16 +888,17 @@ sync_java_files = [TOPSRCDIR + '/mobile/
     'sync/delegates/KeyUploadDelegate.java',
     'sync/delegates/MetaGlobalDelegate.java',
     'sync/delegates/WipeServerDelegate.java',
     'sync/EngineSettings.java',
     'sync/ExtendedJSONObject.java',
     'sync/GlobalSession.java',
     'sync/HTTPFailureException.java',
     'sync/InfoCollections.java',
+    'sync/InfoConfiguration.java',
     'sync/InfoCounts.java',
     'sync/JSONRecordFetcher.java',
     'sync/KeyBundleProvider.java',
     'sync/MetaGlobal.java',
     'sync/MetaGlobalException.java',
     'sync/MetaGlobalMissingEnginesException.java',
     'sync/MetaGlobalNotSetException.java',
     'sync/middleware/Crypto5MiddlewareRepository.java',
@@ -1025,16 +1026,17 @@ sync_java_files = [TOPSRCDIR + '/mobile/
     'sync/stage/AbstractSessionManagingSyncStage.java',
     'sync/stage/AndroidBrowserBookmarksServerSyncStage.java',
     'sync/stage/AndroidBrowserHistoryServerSyncStage.java',
     'sync/stage/CheckPreconditionsStage.java',
     'sync/stage/CompletedStage.java',
     'sync/stage/EnsureCrypto5KeysStage.java',
     'sync/stage/FennecTabsServerSyncStage.java',
     'sync/stage/FetchInfoCollectionsStage.java',
+    'sync/stage/FetchInfoConfigurationStage.java',
     'sync/stage/FetchMetaGlobalStage.java',
     'sync/stage/FormHistoryServerSyncStage.java',
     'sync/stage/GlobalSyncStage.java',
     'sync/stage/NoSuchStageException.java',
     'sync/stage/PasswordsServerSyncStage.java',
     'sync/stage/SafeConstrainedServer11Repository.java',
     'sync/stage/ServerSyncStage.java',
     'sync/stage/SyncClientsEngineStage.java',
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
@@ -27,16 +27,17 @@ import org.mozilla.gecko.sync.net.SyncSt
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
 import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage;
 import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
 import org.mozilla.gecko.sync.stage.CompletedStage;
 import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
 import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage;
 import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage;
+import org.mozilla.gecko.sync.stage.FetchInfoConfigurationStage;
 import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
 import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
 import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
 import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage;
@@ -172,16 +173,18 @@ public class GlobalSession implements Ht
   }
 
   protected void prepareStages() {
     Map<Stage, GlobalSyncStage> stages = new EnumMap<Stage, GlobalSyncStage>(Stage.class);
 
     stages.put(Stage.checkPreconditions,      new CheckPreconditionsStage());
     stages.put(Stage.fetchInfoCollections,    new FetchInfoCollectionsStage());
     stages.put(Stage.fetchMetaGlobal,         new FetchMetaGlobalStage());
+    stages.put(Stage.fetchInfoConfiguration,  new FetchInfoConfigurationStage(
+            config.infoConfigurationURL(), getAuthHeaderProvider()));
     stages.put(Stage.ensureKeysStage,         new EnsureCrypto5KeysStage());
 
     stages.put(Stage.syncClientsEngine,       new SyncClientsEngineStage());
 
     stages.put(Stage.syncTabs,                new FennecTabsServerSyncStage());
     stages.put(Stage.syncPasswords,           new PasswordsServerSyncStage());
     stages.put(Stage.syncBookmarks,           new AndroidBrowserBookmarksServerSyncStage());
     stages.put(Stage.syncHistory,             new AndroidBrowserHistoryServerSyncStage());
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java
@@ -0,0 +1,93 @@
+/* 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;
+
+import android.util.Log;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+/**
+ * Wraps and provides access to configuration data returned from info/configuration.
+ * Docs: https://docs.services.mozilla.com/storage/apis-1.5.html#general-info
+ *
+ * - <bold>max_request_bytes</bold>: the maximum size in bytes of the overall
+ *   HTTP request body that will be accepted by the server.
+ *
+ * - <bold>max_post_records</bold>: the maximum number of records that can be
+ *   uploaded to a collection in a single POST request.
+ *
+ * - <bold>max_post_bytes</bold>: the maximum combined size in bytes of the
+ *   record payloads that can be uploaded to a collection in a single
+ *   POST request.
+ *
+ * - <bold>max_total_records</bold>: the maximum number of records that can be
+ *   uploaded to a collection as part of a batched upload.
+ *
+ * - <bold>max_total_bytes</bold>: the maximum combined size in bytes of the
+ *   record payloads that can be uploaded to a collection as part of
+ *   a batched upload.
+ */
+public class InfoConfiguration {
+    private static final String LOG_TAG = "InfoConfiguration";
+
+    public static final String MAX_REQUEST_BYTES = "max_request_bytes";
+    public static final String MAX_POST_RECORDS = "max_post_records";
+    public static final String MAX_POST_BYTES = "max_post_bytes";
+    public static final String MAX_TOTAL_RECORDS = "max_total_records";
+    public static final String MAX_TOTAL_BYTES = "max_total_bytes";
+
+    private static final long DEFAULT_MAX_REQUEST_BYTES = 1048576;
+    private static final long DEFAULT_MAX_POST_RECORDS = 100;
+    private static final long DEFAULT_MAX_POST_BYTES = 1048576;
+    private static final long DEFAULT_MAX_TOTAL_RECORDS = 10000;
+    private static final long DEFAULT_MAX_TOTAL_BYTES = 104857600;
+
+    // While int's upper range is (2^31-1), which in bytes is equivalent to 2.147 GB, let's be optimistic
+    // about the future and use long here, so that this code works if the server decides its clients are
+    // all on fiber and have congress-library sized bookmark collections.
+    // Record counts are long for the sake of simplicity.
+    public final long maxRequestBytes;
+    public final long maxPostRecords;
+    public final long maxPostBytes;
+    public final long maxTotalRecords;
+    public final long maxTotalBytes;
+
+    public InfoConfiguration() {
+        Logger.debug(LOG_TAG, "info/configuration is unavailable, using defaults");
+
+        maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES;
+        maxPostRecords = DEFAULT_MAX_POST_RECORDS;
+        maxPostBytes = DEFAULT_MAX_POST_BYTES;
+        maxTotalRecords = DEFAULT_MAX_TOTAL_RECORDS;
+        maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES;
+    }
+
+    public InfoConfiguration(final ExtendedJSONObject record) {
+        Logger.debug(LOG_TAG, "info/configuration is " + record.toJSONString());
+
+        maxRequestBytes = getValueFromRecord(record, MAX_REQUEST_BYTES, DEFAULT_MAX_REQUEST_BYTES);
+        maxPostRecords = getValueFromRecord(record, MAX_POST_RECORDS, DEFAULT_MAX_POST_RECORDS);
+        maxPostBytes = getValueFromRecord(record, MAX_POST_BYTES, DEFAULT_MAX_POST_BYTES);
+        maxTotalRecords = getValueFromRecord(record, MAX_TOTAL_RECORDS, DEFAULT_MAX_TOTAL_RECORDS);
+        maxTotalBytes = getValueFromRecord(record, MAX_TOTAL_BYTES, DEFAULT_MAX_TOTAL_BYTES);
+    }
+
+    private static Long getValueFromRecord(ExtendedJSONObject record, String key, long defaultValue) {
+        if (!record.containsKey(key)) {
+            return defaultValue;
+        }
+
+        try {
+            Long val = record.getLong(key);
+            if (val == null) {
+                return defaultValue;
+            }
+            return val;
+        } catch (NumberFormatException e) {
+            Log.w(LOG_TAG, "Could not parse key " + key + " from record: " + record, e);
+            return defaultValue;
+        }
+    }
+}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java
@@ -25,16 +25,18 @@ import android.content.SharedPreferences
 
 public class SyncConfiguration {
   private static final String LOG_TAG = "SyncConfiguration";
 
   // These must be set in GlobalSession's constructor.
   public URI             clusterURL;
   public KeyBundle       syncKeyBundle;
 
+  public InfoConfiguration infoConfiguration;
+
   public CollectionKeys  collectionKeys;
   public InfoCollections infoCollections;
   public MetaGlobal      metaGlobal;
   public String          syncID;
 
   protected final String username;
 
   /**
@@ -361,16 +363,20 @@ public class SyncConfiguration {
   protected String infoBaseURL() {
     return clusterURL + "/info/";
   }
 
   public String infoCollectionsURL() {
     return infoBaseURL() + "collections";
   }
 
+  public String infoConfigurationURL() {
+    return infoBaseURL() + "configuration";
+  }
+
   public String infoCollectionCountsURL() {
     return infoBaseURL() + "collection_counts";
   }
 
   public String metaURL() {
     return storageURL() + "/meta/global";
   }
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java
@@ -4,15 +4,16 @@
 
 package org.mozilla.gecko.sync.delegates;
 
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 
 /**
  * A fairly generic delegate to handle fetches of single JSON object blobs, as
- * provided by <code>info/collections</code> and <code>info/collection_counts</code>.
+ * provided by <code>info/configuration</code>, <code>info/collections</code>
+ * and <code>info/collection_counts</code>.
  */
 public interface JSONRecordFetchDelegate {
   public void handleSuccess(ExtendedJSONObject body);
   public void handleFailure(SyncStorageResponse response);
   public void handleError(Exception e);
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java
@@ -0,0 +1,59 @@
+/* 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.stage;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.JSONRecordFetcher;
+import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+/**
+ * Fetches configuration data from info/configurations endpoint.
+ */
+public class FetchInfoConfigurationStage extends AbstractNonRepositorySyncStage {
+    private final String configurationURL;
+    private final AuthHeaderProvider authHeaderProvider;
+
+    public FetchInfoConfigurationStage(final String configurationURL, final AuthHeaderProvider authHeaderProvider) {
+        super();
+        this.configurationURL = configurationURL;
+        this.authHeaderProvider = authHeaderProvider;
+    }
+
+    public class StageInfoConfigurationDelegate implements JSONRecordFetchDelegate {
+        @Override
+        public void handleSuccess(final ExtendedJSONObject result) {
+            session.config.infoConfiguration = new InfoConfiguration(result);
+            session.advance();
+        }
+
+        @Override
+        public void handleFailure(final SyncStorageResponse response) {
+            // Handle all non-404 failures upstream.
+            if (response.getStatusCode() != 404) {
+                session.handleHTTPError(response, "Failure fetching info/configuration");
+                return;
+            }
+
+            // End-point might not be available (404) if server is running an older version.
+            // We will use default config values in this case.
+            session.config.infoConfiguration = new InfoConfiguration();
+            session.advance();
+        }
+
+        @Override
+        public void handleError(final Exception e) {
+            session.abort(e, "Failure fetching info/configuration");
+        }
+    }
+    @Override
+    public void execute() {
+        final StageInfoConfigurationDelegate delegate = new StageInfoConfigurationDelegate();
+        final JSONRecordFetcher fetcher = new JSONRecordFetcher(configurationURL, authHeaderProvider);
+        fetcher.fetch(delegate);
+    }
+}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java
@@ -13,16 +13,17 @@ import java.util.Map;
 import org.mozilla.gecko.sync.GlobalSession;
 
 
 public interface GlobalSyncStage {
   public static enum Stage {
     idle,                       // Start state.
     checkPreconditions,         // Preparation of the basics. TODO: clear status
     fetchInfoCollections,       // Take a look at timestamps.
+    fetchInfoConfiguration,     // Fetch server upload limits
     fetchMetaGlobal,
     ensureKeysStage,
     /*
     ensureSpecialRecords,
     updateEngineTimestamps,
     */
     syncClientsEngine(SyncClientsEngineStage.STAGE_NAME),
     /*