mobile/android/base/background/healthreport/upload/SubmissionPolicy.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.background.healthreport.upload;
     7 import java.net.MalformedURLException;
     8 import java.net.SocketException;
     9 import java.net.UnknownHostException;
    10 import java.util.Collection;
    12 import org.mozilla.gecko.background.common.log.Logger;
    13 import org.mozilla.gecko.background.healthreport.HealthReportConstants;
    14 import org.mozilla.gecko.background.healthreport.HealthReportUtils;
    15 import org.mozilla.gecko.background.healthreport.upload.SubmissionClient.Delegate;
    17 import android.content.SharedPreferences;
    19 /**
    20  * Manages scheduling of Firefox Health Report data submission.
    21  *
    22  * The rules of data submission are as follows:
    23  *
    24  * 1. Do not submit data more than once every 24 hours.
    25  *
    26  * 2. Try to submit as close to 24 hours apart as possible.
    27  *
    28  * 3. Do not submit too soon after application startup so as to not negatively
    29  * impact performance at startup.
    30  *
    31  * 4. Before first ever data submission, the user should be notified about data
    32  * collection practices.
    33  *
    34  * 5. User should have opportunity to react to this notification before data
    35  * submission.
    36  *
    37  * 6. Display of notification without any explicit user action constitutes
    38  * implicit consent after a certain duration of time.
    39  *
    40  * 7. If data submission fails, try at most 2 additional times before giving up
    41  * on that day's submission.
    42  *
    43  * On Android, items 4, 5, and 6 are addressed by displaying an Android
    44  * notification on first run.
    45  */
    46 public class SubmissionPolicy {
    47   public static final String LOG_TAG = SubmissionPolicy.class.getSimpleName();
    49   protected final SharedPreferences sharedPreferences;
    50   protected final SubmissionClient client;
    51   protected final boolean uploadEnabled;
    52   protected final ObsoleteDocumentTracker tracker;
    54   public SubmissionPolicy(final SharedPreferences sharedPreferences,
    55       final SubmissionClient client,
    56       final ObsoleteDocumentTracker tracker,
    57       boolean uploadEnabled) {
    58     if (sharedPreferences == null) {
    59       throw new IllegalArgumentException("sharedPreferences must not be null");
    60     }
    61     this.sharedPreferences = sharedPreferences;
    62     this.client = client;
    63     this.tracker = tracker;
    64     this.uploadEnabled = uploadEnabled;
    65   }
    67   /**
    68    * Check what action must happen, advance counters and timestamps, and
    69    * possibly spawn a request to the server.
    70    *
    71    * @param localTime now.
    72    * @return true if a request was spawned; false otherwise.
    73    */
    74   public boolean tick(final long localTime) {
    75     final long nextUpload = getNextSubmission();
    77     // If the system clock were ever set to a time in the distant future,
    78     // it's possible our next schedule date is far out as well. We know
    79     // we shouldn't schedule for more than a day out, so we reset the next
    80     // scheduled date appropriately. 3 days was chosen to match desktop's
    81     // arbitrary choice.
    82     if (nextUpload >= localTime + 3 * getMinimumTimeBetweenUploads()) {
    83       Logger.warn(LOG_TAG, "Next upload scheduled far in the future; system clock reset? " + nextUpload + " > " + localTime);
    84       // Things are strange, we want to start again but we don't want to stampede.
    85       editor()
    86         .setNextSubmission(localTime + getMinimumTimeBetweenUploads())
    87         .commit();
    88       return false;
    89     }
    91     // Don't upload unless an interval has elapsed.
    92     if (localTime < nextUpload) {
    93       Logger.debug(LOG_TAG, "We uploaded less than an interval ago; skipping. " + nextUpload + " > " + localTime);
    94       return false;
    95     }
    97     if (!uploadEnabled) {
    98       // We only delete (rather than mark as obsolete during upload) when
    99       // uploading is disabled. We try to delete aggressively, since the volume
   100       // of deletes should be very low. But we don't want to send too many
   101       // delete requests at the same time, so we process these one at a time. In
   102       // the future (Bug 872756), we will be able to delete multiple documents
   103       // with one request.
   104       final String obsoleteId = tracker.getNextObsoleteId();
   105       if (obsoleteId == null) {
   106         return false;
   107       }
   109       Editor editor = editor();
   110       editor.setLastDeleteRequested(localTime); // Write committed by delegate.
   111       client.delete(localTime, obsoleteId, new DeleteDelegate(editor));
   112       return true;
   113     }
   115     long firstRun = getFirstRunLocalTime();
   116     if (firstRun < 0) {
   117       firstRun = localTime;
   118       // Make sure we start clean and as soon as possible.
   119       editor()
   120         .setFirstRunLocalTime(firstRun)
   121         .setNextSubmission(localTime + getMinimumTimeBeforeFirstSubmission())
   122         .setCurrentDayFailureCount(0)
   123         .commit();
   124     }
   126     // This case will occur if the nextSubmission time is not set (== -1) but firstRun is.
   127     if (localTime < firstRun + getMinimumTimeBeforeFirstSubmission()) {
   128       Logger.info(LOG_TAG, "Need to wait " + getMinimumTimeBeforeFirstSubmission() + " before first upload.");
   129       return false;
   130     }
   132     // The first upload attempt for a given document submission begins a 24-hour period in which
   133     // the upload will retry upon a soft failure. At the end of this period, the submission
   134     // failure count is reset, ensuring each day's first submission attempt has a zeroed failure
   135     // count. A period may also end on upload success or hard failure.
   136     if (localTime >= getCurrentDayResetTime()) {
   137       editor()
   138         .setCurrentDayResetTime(localTime + getMinimumTimeBetweenUploads())
   139         .setCurrentDayFailureCount(0)
   140         .commit();
   141     }
   143     String id = HealthReportUtils.generateDocumentId();
   144     Collection<String> oldIds = tracker.getBatchOfObsoleteIds();
   145     tracker.addObsoleteId(id);
   147     Editor editor = editor();
   148     editor.setLastUploadRequested(localTime); // Write committed by delegate.
   149     client.upload(localTime, id, oldIds, new UploadDelegate(editor, oldIds));
   150     return true;
   151   }
   153   /**
   154    * Return true if the upload that produced <code>e</code> definitely did not
   155    * produce a new record on the remote server.
   156    *
   157    * @param e
   158    *          <code>Exception</code> that upload produced.
   159    * @return true if the server could not have a new record.
   160    */
   161   protected boolean isLocalException(Exception e) {
   162     return (e instanceof MalformedURLException) ||
   163            (e instanceof SocketException) ||
   164            (e instanceof UnknownHostException);
   165   }
   167   protected class UploadDelegate implements Delegate {
   168     protected final Editor editor;
   169     protected final Collection<String> oldIds;
   171     public UploadDelegate(Editor editor, Collection<String> oldIds) {
   172       this.editor = editor;
   173       this.oldIds = oldIds;
   174     }
   176     @Override
   177     public void onSuccess(long localTime, String id) {
   178       long next = localTime + getMinimumTimeBetweenUploads();
   179       tracker.markIdAsUploaded(id);
   180       tracker.purgeObsoleteIds(oldIds);
   181       editor
   182         .setNextSubmission(next)
   183         .setLastUploadSucceeded(localTime)
   184         .setCurrentDayFailureCount(0)
   185         .clearCurrentDayResetTime() // Set again on the next submission's first upload attempt.
   186         .commit();
   187       if (Logger.LOG_PERSONAL_INFORMATION) {
   188         Logger.pii(LOG_TAG, "Successful upload with id " + id + " obsoleting "
   189             + oldIds.size() + " old records reported at " + localTime + "; next upload at " + next + ".");
   190       } else {
   191         Logger.info(LOG_TAG, "Successful upload obsoleting " + oldIds.size()
   192             + " old records reported at " + localTime + "; next upload at " + next + ".");
   193       }
   194     }
   196     @Override
   197     public void onHardFailure(long localTime, String id, String reason, Exception e) {
   198       long next = localTime + getMinimumTimeBetweenUploads();
   199       if (isLocalException(e)) {
   200         Logger.info(LOG_TAG, "Hard failure caused by local exception; not tracking id and not decrementing attempts.");
   201         tracker.removeObsoleteId(id);
   202       } else {
   203         tracker.decrementObsoleteIdAttempts(oldIds);
   204       }
   205       editor
   206         .setNextSubmission(next)
   207         .setLastUploadFailed(localTime)
   208         .setCurrentDayFailureCount(0)
   209         .clearCurrentDayResetTime() // Set again on the next submission's first upload attempt.
   210         .commit();
   211       Logger.warn(LOG_TAG, "Hard failure reported at " + localTime + ": " + reason + " Next upload at " + next + ".", e);
   212     }
   214     @Override
   215     public void onSoftFailure(long localTime, String id, String reason, Exception e) {
   216       int failuresToday = getCurrentDayFailureCount();
   217       Logger.warn(LOG_TAG, "Soft failure reported at " + localTime + ": " + reason + " Previously failed " + failuresToday + " time(s) today.");
   219       if (failuresToday >= getMaximumFailuresPerDay()) {
   220         onHardFailure(localTime, id, "Reached the limit of daily upload attempts: " + failuresToday, e);
   221         return;
   222       }
   224       long next = localTime + getMinimumTimeAfterFailure();
   225       if (isLocalException(e)) {
   226         Logger.info(LOG_TAG, "Soft failure caused by local exception; not tracking id and not decrementing attempts.");
   227         tracker.removeObsoleteId(id);
   228       } else {
   229         tracker.decrementObsoleteIdAttempts(oldIds);
   230       }
   231       editor
   232         .setNextSubmission(next)
   233         .setLastUploadFailed(localTime)
   234         .setCurrentDayFailureCount(failuresToday + 1)
   235         .commit();
   236       Logger.info(LOG_TAG, "Retrying upload at " + next + ".");
   237     }
   238   }
   240   protected class DeleteDelegate implements Delegate {
   241     protected final Editor editor;
   243     public DeleteDelegate(Editor editor) {
   244       this.editor = editor;
   245     }
   247     @Override
   248     public void onSoftFailure(final long localTime, String id, String reason, Exception e) {
   249       long next = localTime + getMinimumTimeBetweenDeletes();
   250       if (isLocalException(e)) {
   251         Logger.info(LOG_TAG, "Soft failure caused by local exception; not decrementing attempts.");
   252       } else {
   253         tracker.decrementObsoleteIdAttempts(id);
   254       }
   255       editor
   256         .setNextSubmission(next)
   257         .setLastDeleteFailed(localTime)
   258         .commit();
   260       if (Logger.LOG_PERSONAL_INFORMATION) {
   261         Logger.info(LOG_TAG, "Got soft failure at " + localTime + " deleting obsolete document with id " + id + ": " + reason + " Trying again later.");
   262       } else {
   263         Logger.info(LOG_TAG, "Got soft failure at " + localTime + " deleting obsolete document: " + reason + " Trying again later.");
   264       }
   265     }
   267     @Override
   268     public void onHardFailure(final long localTime, String id, String reason, Exception e) {
   269       // We're never going to be able to delete this id, so don't keep trying.
   270       long next = localTime + getMinimumTimeBetweenDeletes();
   271       tracker.removeObsoleteId(id);
   272       editor
   273         .setNextSubmission(next)
   274         .setLastDeleteFailed(localTime)
   275         .commit();
   277       if (Logger.LOG_PERSONAL_INFORMATION) {
   278         Logger.warn(LOG_TAG, "Got hard failure at " + localTime + " deleting obsolete document with id " + id + ": " + reason + " Abandoning delete request.", e);
   279       } else {
   280         Logger.warn(LOG_TAG, "Got hard failure at " + localTime + " deleting obsolete document: " + reason + " Abandoning delete request.", e);
   281       }
   282     }
   284     @Override
   285     public void onSuccess(final long localTime, String id) {
   286       long next = localTime + getMinimumTimeBetweenDeletes();
   287       tracker.removeObsoleteId(id);
   288       editor
   289         .setNextSubmission(next)
   290         .setLastDeleteSucceeded(localTime)
   291         .commit();
   293       if (Logger.LOG_PERSONAL_INFORMATION) {
   294         Logger.pii(LOG_TAG, "Deleted an obsolete document with id " + id + " at " + localTime + ".");
   295       } else {
   296         Logger.info(LOG_TAG, "Deleted an obsolete document at " + localTime + ".");
   297       }
   298     }
   299   }
   301   public SharedPreferences getSharedPreferences() {
   302     return this.sharedPreferences;
   303   }
   305   public long getMinimumTimeBetweenUploads() {
   306     return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_UPLOADS, HealthReportConstants.DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS);
   307   }
   309   public long getMinimumTimeBeforeFirstSubmission() {
   310     return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION, HealthReportConstants.DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION);
   311   }
   313   public long getMinimumTimeAfterFailure() {
   314     return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_AFTER_FAILURE, HealthReportConstants.DEFAULT_MINIMUM_TIME_AFTER_FAILURE);
   315   }
   317   public long getMaximumFailuresPerDay() {
   318     return getSharedPreferences().getLong(HealthReportConstants.PREF_MAXIMUM_FAILURES_PER_DAY, HealthReportConstants.DEFAULT_MAXIMUM_FAILURES_PER_DAY);
   319   }
   321   // Authoritative.
   322   public long getFirstRunLocalTime() {
   323     return getSharedPreferences().getLong(HealthReportConstants.PREF_FIRST_RUN, -1);
   324   }
   326   // Authoritative.
   327   public long getNextSubmission() {
   328     return getSharedPreferences().getLong(HealthReportConstants.PREF_NEXT_SUBMISSION, -1);
   329   }
   331   // Authoritative.
   332   public int getCurrentDayFailureCount() {
   333     return getSharedPreferences().getInt(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, 0);
   334   }
   336   // Authoritative.
   337   public long getCurrentDayResetTime() {
   338     return getSharedPreferences().getLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, -1);
   339   }
   341   /**
   342    * To avoid writing to disk multiple times, we encapsulate writes in a
   343    * helper class. Be sure to call <code>commit</code> to flush to disk!
   344    */
   345   protected Editor editor() {
   346     return new Editor(getSharedPreferences().edit());
   347   }
   349   protected static class Editor {
   350     protected final SharedPreferences.Editor editor;
   352     public Editor(SharedPreferences.Editor editor) {
   353       this.editor = editor;
   354     }
   356     public void commit() {
   357       editor.commit();
   358     }
   360     // Authoritative.
   361     public Editor setFirstRunLocalTime(long localTime) {
   362       editor.putLong(HealthReportConstants.PREF_FIRST_RUN, localTime);
   363       return this;
   364     }
   366     // Authoritative.
   367     public Editor setNextSubmission(long localTime) {
   368       editor.putLong(HealthReportConstants.PREF_NEXT_SUBMISSION, localTime);
   369       return this;
   370     }
   372     // Authoritative.
   373     public Editor setCurrentDayFailureCount(int failureCount) {
   374       editor.putInt(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, failureCount);
   375       return this;
   376     }
   378     // Authoritative.
   379     public Editor setCurrentDayResetTime(long resetTime) {
   380       editor.putLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, resetTime);
   381       return this;
   382     }
   384     // Authoritative.
   385     public Editor clearCurrentDayResetTime() {
   386       editor.putLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, -1);
   387       return this;
   388     }
   390     // Authoritative.
   391     public Editor setLastUploadRequested(long localTime) {
   392       editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED, localTime);
   393       return this;
   394     }
   396     // Forensics only.
   397     public Editor setLastUploadSucceeded(long localTime) {
   398       editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_SUCCEEDED, localTime);
   399       return this;
   400     }
   402     // Forensics only.
   403     public Editor setLastUploadFailed(long localTime) {
   404       editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_FAILED, localTime);
   405       return this;
   406     }
   408     // Forensics only.
   409     public Editor setLastDeleteRequested(long localTime) {
   410       editor.putLong(HealthReportConstants.PREF_LAST_DELETE_REQUESTED, localTime);
   411       return this;
   412     }
   414     // Forensics only.
   415     public Editor setLastDeleteSucceeded(long localTime) {
   416       editor.putLong(HealthReportConstants.PREF_LAST_DELETE_SUCCEEDED, localTime);
   417       return this;
   418     }
   420     // Forensics only.
   421     public Editor setLastDeleteFailed(long localTime) {
   422       editor.putLong(HealthReportConstants.PREF_LAST_DELETE_FAILED, localTime);
   423       return this;
   424     }
   425   }
   427   // Authoritative.
   428   public long getLastUploadRequested() {
   429     return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED, -1);
   430   }
   432   // Forensics only.
   433   public long getLastUploadSucceeded() {
   434     return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_SUCCEEDED, -1);
   435   }
   437   // Forensics only.
   438   public long getLastUploadFailed() {
   439     return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_FAILED, -1);
   440   }
   442   // Forensics only.
   443   public long getLastDeleteRequested() {
   444     return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_REQUESTED, -1);
   445   }
   447   // Forensics only.
   448   public long getLastDeleteSucceeded() {
   449     return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_SUCCEEDED, -1);
   450   }
   452   // Forensics only.
   453   public long getLastDeleteFailed() {
   454     return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_FAILED, -1);
   455   }
   457   public long getMinimumTimeBetweenDeletes() {
   458     return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_DELETES, HealthReportConstants.DEFAULT_MINIMUM_TIME_BETWEEN_DELETES);
   459   }
   460 }

mercurial