Bug 1140813 - Schedule periodic Reading List syncs. r=rnewman, a=readinglist
authorNick Alexander <nalexander@mozilla.com>
Fri, 27 Mar 2015 17:05:15 -0700
changeset 258214 27f61020a9e4
parent 258213 dff4ad268667
child 258215 14eb337e419a
push id4620
push userrnewman@mozilla.com
push date2015-04-02 16:21 +0000
treeherdermozilla-beta@27f61020a9e4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman, readinglist
bugs1140813
milestone38.0
Bug 1140813 - Schedule periodic Reading List syncs. r=rnewman, a=readinglist ======== https://github.com/mozilla-services/android-sync/commit/a249e77fd1f3d121023a3b2db3bb59dc0c2be539 Author: Nick Alexander <nalexander@mozilla.com> Bug 1140813 - Schedule periodic Reading List syncs.
mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
mobile/android/base/fxa/sync/FxAccountSyncDelegate.java
mobile/android/base/reading/ReadingListSyncAdapter.java
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -4,34 +4,36 @@
 
 package org.mozilla.gecko.fxa.sync;
 
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.SkewHandler;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate;
 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
 import org.mozilla.gecko.fxa.login.Married;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate.Result;
 import org.mozilla.gecko.sync.BackoffHandler;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.PrefsBackoffHandler;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.ThreadPool;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
@@ -109,17 +111,17 @@ public class FxAccountSyncAdapter extend
 
     @Override
     public void rejectSync() {
       super.rejectSync();
     }
 
     protected final Collection<String> stageNamesToSync;
 
-    public SyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) {
+    public SyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) {
       super(latch, syncResult);
       this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync);
     }
 
     public Collection<String> getStageNamesToSync() {
       return this.stageNamesToSync;
     }
   }
@@ -411,17 +413,17 @@ public class FxAccountSyncAdapter extend
           AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
         } catch (Exception e) {
           // Should never happen, but we really don't want to die in a background thread.
           Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e);
         }
       }
     });
 
-    final CountDownLatch latch = new CountDownLatch(1);
+    final BlockingQueue<Result> latch = new LinkedBlockingQueue<>(1);
 
     Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
     Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
 
     final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync);
 
     try {
       // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration.
@@ -532,17 +534,17 @@ public class FxAccountSyncAdapter extend
             syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras, fxAccount);
           } catch (Exception e) {
             syncDelegate.handleError(e);
             return;
           }
         }
       });
 
-      latch.await();
+      latch.take();
     } catch (Exception e) {
       Logger.error(LOG_TAG, "Got error syncing.", e);
       syncDelegate.handleError(e);
     } finally {
       fxAccount.releaseSharedAccountStateLock();
     }
 
     Logger.info(LOG_TAG, "Syncing done.");
--- a/mobile/android/base/fxa/sync/FxAccountSyncDelegate.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncDelegate.java
@@ -1,25 +1,32 @@
 /* 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.fxa.sync;
 
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.BlockingQueue;
 
 import org.mozilla.gecko.fxa.login.State;
 
 import android.content.SyncResult;
 
 public class FxAccountSyncDelegate {
-  protected final CountDownLatch latch;
+  public enum Result {
+    Success,
+    Error,
+    Postponed,
+    Rejected,
+  }
+
+  protected final BlockingQueue<Result> latch;
   protected final SyncResult syncResult;
 
-  public FxAccountSyncDelegate(CountDownLatch latch, SyncResult syncResult) {
+  public FxAccountSyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult) {
     if (latch == null) {
       throw new IllegalArgumentException("latch must not be null");
     }
     if (syncResult == null) {
       throw new IllegalArgumentException("syncResult must not be null");
     }
     this.latch = latch;
     this.syncResult = syncResult;
@@ -46,22 +53,22 @@ public class FxAccountSyncDelegate {
    * progress, until the user intervenes.
    */
   protected void setSyncResultHardError() {
     syncResult.stats.numAuthExceptions += 1;
   }
 
   public void handleSuccess() {
     setSyncResultSuccess();
-    latch.countDown();
+    latch.offer(Result.Success);
   }
 
   public void handleError(Exception e) {
     setSyncResultSoftError();
-    latch.countDown();
+    latch.offer(Result.Error);
   }
 
   /**
    * When the login machine terminates, we might not be in the
    * <code>Married</code> state, and therefore we can't sync. This method
    * messages as much to the user.
    * <p>
    * To avoid stopping us syncing altogether, we set a soft error rather than
@@ -70,34 +77,34 @@ public class FxAccountSyncDelegate {
    * initiated activity mark the Android account as ready to sync again. This
    * is tricky, though, so we play it safe for now.
    *
    * @param finalState
    *          that login machine ended in.
    */
   public void handleCannotSync(State finalState) {
     setSyncResultSoftError();
-    latch.countDown();
+    latch.offer(Result.Error);
   }
 
   public void postponeSync(long millis) {
     if (millis > 0) {
       // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
       // So we don't bother doing this. Instead, we rely on the periodic sync
       // we schedule, and the backoff handler for the rest.
       /*
       Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
       syncResult.delayUntil = millis / 1000;
        */
     }
     setSyncResultSoftError();
-    latch.countDown();
+    latch.offer(Result.Postponed);
   }
 
   /**
    * Simply don't sync, without setting any error flags.
    * This is the appropriate behavior when a routine backoff has not yet
    * been met.
    */
   public void rejectSync() {
-    latch.countDown();
+    latch.offer(Result.Rejected);
   }
-}
\ No newline at end of file
+}
--- a/mobile/android/base/reading/ReadingListSyncAdapter.java
+++ b/mobile/android/base/reading/ReadingListSyncAdapter.java
@@ -2,30 +2,35 @@
  * 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.reading;
 
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Collection;
-import java.util.concurrent.CountDownLatch;
+import java.util.EnumSet;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
 import org.mozilla.gecko.background.ReadingListConstants;
 import org.mozilla.gecko.background.common.PrefsBranch;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FirefoxAccounts.SyncHint;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate.Result;
 import org.mozilla.gecko.sync.BackoffHandler;
 import org.mozilla.gecko.sync.PrefsBackoffHandler;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
@@ -39,16 +44,21 @@ import android.os.Bundle;
 
 public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter {
   public static final String PREF_LOCAL_NAME = "device.localname";
 
   private static final String LOG_TAG = ReadingListSyncAdapter.class.getSimpleName();
   private static final long TIMEOUT_SECONDS = 60;
   protected final ExecutorService executor;
 
+  // Don't sync again if we successfully synced within this duration.
+  private static final int AFTER_SUCCESS_SYNC_DELAY_SECONDS = 5 * 60; // 5 minutes.
+  // Don't sync again if we unsuccessfully synced within this duration.
+  private static final int AFTER_ERROR_SYNC_DELAY_SECONDS = 15 * 60; // 15 minutes.
+
   public ReadingListSyncAdapter(Context context, boolean autoInitialize) {
     super(context, autoInitialize);
     this.executor = Executors.newSingleThreadExecutor();
   }
 
   protected static abstract class SyncAdapterSynchronizerDelegate implements ReadingListSynchronizerDelegate {
     private final FxAccountSyncDelegate syncDelegate;
     private final ContentProviderClient cpc;
@@ -151,16 +161,19 @@ public class ReadingListSyncAdapter exte
     // TODO: backoffs, and everything else handled by a SessionCallback.
   }
 
   @Override
   public void onPerformSync(final Account account, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult) {
     Logger.setThreadLogTag(ReadingListConstants.GLOBAL_LOG_TAG);
     Logger.resetLogging();
 
+    final EnumSet<SyncHint> syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras);
+    FirefoxAccounts.logSyncHints(syncHints);
+
     final Context context = getContext();
     final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
 
     // Don't sync Reading List if we're in a non-default configuration, but allow testing against stage.
     final String accountServerURI = fxAccount.getAccountServerURI();
     final boolean usingDefaultAuthServer = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(accountServerURI);
     final boolean usingStageAuthServer = FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(accountServerURI);
     if (!usingDefaultAuthServer && !usingStageAuthServer) {
@@ -175,73 +188,96 @@ public class ReadingListSyncAdapter exte
     if (!usingDefaultSyncServer && !usingStageSyncServer) {
       Logger.error(LOG_TAG, "Skipping Reading List sync because Sync is not using the prod or stage Sync (token) server.");
       Logger.debug(LOG_TAG, "If the user has chosen to not store Sync data with Mozilla, we shouldn't store Reading List data with Mozilla .");
       // Stop syncing the Reading List entirely.
       ContentResolver.setIsSyncable(account, BrowserContract.READING_LIST_AUTHORITY, 0);
       return;
     }
 
+    Result result = Result.Error;
+    final BlockingQueue<Result> latch = new LinkedBlockingQueue<Result>(1);
+    final FxAccountSyncDelegate syncDelegate = new FxAccountSyncDelegate(latch, syncResult);
+
     // Allow testing against stage.
     final String endpointString;
     if (usingStageAuthServer) {
       endpointString = ReadingListConstants.DEFAULT_DEV_ENDPOINT;
     } else {
       endpointString = ReadingListConstants.DEFAULT_PROD_ENDPOINT;
     }
 
-    Logger.info(LOG_TAG, "Syncing reading list against " + endpointString);
+    Logger.info(LOG_TAG, "Syncing reading list against endpoint: " + endpointString);
     final URI endpointURI;
     try {
       endpointURI = new URI(endpointString);
     } catch (URISyntaxException e) {
       // Should never happen.
       Logger.error(LOG_TAG, "Unexpected malformed URI for reading list service: " + endpointString);
+      syncDelegate.handleError(e);
       return;
     }
 
-    final CountDownLatch latch = new CountDownLatch(1);
-    final FxAccountSyncDelegate syncDelegate = new FxAccountSyncDelegate(latch, syncResult);
-
     final AccountManager accountManager = AccountManager.get(context);
     // If we have an auth failure that requires user intervention, FxA will show system
     // notifications prompting the user to re-connect as it advances the internal account state.
     // true causes the auth token fetch to return null on failure immediately, rather than doing
     // Mysterious Internal Work to try to get the token.
     final boolean notifyAuthFailure = true;
     try {
       final SharedPreferences sharedPrefs = fxAccount.getReadingListPrefs();
-      final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "storage");
 
-      // TODO: allow overriding based on flags.
-      final long delayMilliseconds = storageBackoffHandler.delayMilliseconds();
-      if (delayMilliseconds > 0) {
-        Logger.warn(LOG_TAG, "Not syncing: storage requested additional backoff: " + delayMilliseconds + " milliseconds.");
+      final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "storage");
+      final long storageBackoffDelayMilliseconds = storageBackoffHandler.delayMilliseconds();
+      if (!syncHints.contains(SyncHint.SCHEDULE_NOW) && !syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF) && storageBackoffDelayMilliseconds > 0) {
+        Logger.warn(LOG_TAG, "Not syncing: storage requested additional backoff: " + storageBackoffDelayMilliseconds + " milliseconds.");
+        syncDelegate.rejectSync();
+        return;
+      }
+
+      final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate");
+      final long rateLimitBackoffDelayMilliseconds = rateLimitBackoffHandler.delayMilliseconds();
+      if (!syncHints.contains(SyncHint.SCHEDULE_NOW) && !syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT) && rateLimitBackoffDelayMilliseconds > 0) {
+        Logger.warn(LOG_TAG, "Not syncing: local rate limiting for another: " + rateLimitBackoffDelayMilliseconds + " milliseconds.");
+        syncDelegate.rejectSync();
         return;
       }
 
       final String authToken = accountManager.blockingGetAuthToken(account, ReadingListConstants.AUTH_TOKEN_TYPE, notifyAuthFailure);
       if (authToken == null) {
         throw new RuntimeException("Couldn't get oauth token!  Aborting sync.");
       }
 
       final ReadingListBackoffObserver observer = new ReadingListBackoffObserver(endpointURI.getHost());
       BaseResource.addHttpResponseObserver(observer);
       try {
         syncWithAuthorization(context, endpointURI, syncResult, syncDelegate, authToken, sharedPrefs, extras);
-        latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        result = latch.poll(TIMEOUT_SECONDS, TimeUnit.SECONDS);
       } finally {
+        BaseResource.removeHttpResponseObserver(observer);
         long backoffInSeconds = observer.largestBackoffObservedInSeconds.get();
-        BaseResource.removeHttpResponseObserver(observer);
         if (backoffInSeconds > 0) {
-          Logger.warn(LOG_TAG, "Observed " + backoffInSeconds + " second backoff request.");
+          Logger.warn(LOG_TAG, "Observed " + backoffInSeconds + "-second backoff request.");
           storageBackoffHandler.extendEarliestNextRequest(System.currentTimeMillis() + 1000 * backoffInSeconds);
         }
       }
 
+      switch (result) {
+      case Success:
+        requestPeriodicSync(account, ReadingListSyncAdapter.AFTER_SUCCESS_SYNC_DELAY_SECONDS);
+        break;
+      case Error:
+        requestPeriodicSync(account, ReadingListSyncAdapter.AFTER_ERROR_SYNC_DELAY_SECONDS);
+        break;
+      case Postponed:
+        break;
+      case Rejected:
+        break;
+      }
+
       Logger.info(LOG_TAG, "Reading list sync done.");
     } catch (Exception e) {
       // We can get lots of exceptions here; handle them uniformly.
       Logger.error(LOG_TAG, "Got error syncing.", e);
       syncDelegate.handleError(e);
     }
 
     /*
@@ -251,19 +287,29 @@ public class ReadingListSyncAdapter exte
      */
 
     /*
      * TODO:
      * * Auth.
      * * Server URI lookup.
      * * Syncing.
      * * Error handling.
-     * * Sync scheduling.
      * * Forcing syncs/interactive use.
      */
   }
 
   private ContentProviderClient getContentProviderClient(Context context) {
     final ContentResolver contentResolver = context.getContentResolver();
     final ContentProviderClient client = contentResolver.acquireContentProviderClient(ReadingListItems.CONTENT_URI);
     return client;
   }
+
+  /**
+   * Updates the existing system periodic sync interval to the specified duration.
+   *
+   * @param intervalSeconds the requested period, which Android will vary by up to 4%.
+   */
+  protected void requestPeriodicSync(final Account account, final long intervalSeconds) {
+    final String authority = BrowserContract.AUTHORITY;
+    Logger.info(LOG_TAG, "Scheduling periodic sync for " + intervalSeconds + ".");
+    ContentResolver.addPeriodicSync(account, authority, Bundle.EMPTY, intervalSeconds);
+  }
 }