Home | History | Annotate | Download | only in common
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.common;
     18 
     19 import android.content.SharedPreferences;
     20 import android.text.format.Time;
     21 
     22 import java.util.Map;
     23 import java.util.TreeMap;
     24 
     25 /**
     26  * Tracks the success/failure history of a particular network operation in
     27  * persistent storage and computes retry strategy accordingly.  Handles
     28  * exponential backoff, periodic rescheduling, event-driven triggering,
     29  * retry-after moratorium intervals, etc. based on caller-specified parameters.
     30  *
     31  * <p>This class does not directly perform or invoke any operations,
     32  * it only keeps track of the schedule.  Somebody else needs to call
     33  * {@link #getNextTimeMillis} as appropriate and do the actual work.
     34  */
     35 public class OperationScheduler {
     36     /** Tunable parameter options for {@link #getNextTimeMillis}. */
     37     public static class Options {
     38         /** Wait this long after every error before retrying. */
     39         public long backoffFixedMillis = 0;
     40 
     41         /** Wait this long times the number of consecutive errors so far before retrying. */
     42         public long backoffIncrementalMillis = 5000;
     43 
     44         /** Wait this long times 2^(number of consecutive errors so far) before retrying. */
     45         public int backoffExponentialMillis = 0;
     46 
     47         /** Maximum duration of moratorium to honor.  Mostly an issue for clock rollbacks. */
     48         public long maxMoratoriumMillis = 24 * 3600 * 1000;
     49 
     50         /** Minimum duration after success to wait before allowing another trigger. */
     51         public long minTriggerMillis = 0;
     52 
     53         /** Automatically trigger this long after the last success. */
     54         public long periodicIntervalMillis = 0;
     55 
     56         @Override
     57         public String toString() {
     58             if (backoffExponentialMillis > 0) {
     59                 return String.format(
     60                     "OperationScheduler.Options[backoff=%.1f+%.1f+%.1f max=%.1f min=%.1f period=%.1f]",
     61                     backoffFixedMillis / 1000.0, backoffIncrementalMillis / 1000.0,
     62                     backoffExponentialMillis / 1000.0,
     63                     maxMoratoriumMillis / 1000.0, minTriggerMillis / 1000.0,
     64                     periodicIntervalMillis / 1000.0);
     65             } else {
     66                 return String.format(
     67                     "OperationScheduler.Options[backoff=%.1f+%.1f max=%.1f min=%.1f period=%.1f]",
     68                     backoffFixedMillis / 1000.0, backoffIncrementalMillis / 1000.0,
     69                     maxMoratoriumMillis / 1000.0, minTriggerMillis / 1000.0,
     70                     periodicIntervalMillis / 1000.0);
     71             }
     72         }
     73     }
     74 
     75     private static final String PREFIX = "OperationScheduler_";
     76     private final SharedPreferences mStorage;
     77 
     78     /**
     79      * Initialize the scheduler state.
     80      * @param storage to use for recording the state of operations across restarts/reboots
     81      */
     82     public OperationScheduler(SharedPreferences storage) {
     83         mStorage = storage;
     84     }
     85 
     86     /**
     87      * Parse scheduler options supplied in this string form:
     88      *
     89      * <pre>
     90      * backoff=(fixed)+(incremental)[+(exponential)] max=(maxmoratorium) min=(mintrigger) [period=](interval)
     91      * </pre>
     92      *
     93      * All values are times in (possibly fractional) <em>seconds</em> (not milliseconds).
     94      * Omitted settings are left at whatever existing default value was passed in.
     95      *
     96      * <p>
     97      * The default options: <code>backoff=0+5 max=86400 min=0 period=0</code><br>
     98      * Fractions are OK: <code>backoff=+2.5 period=10.0</code><br>
     99      * The "period=" can be omitted: <code>3600</code><br>
    100      *
    101      * @param spec describing some or all scheduler options.
    102      * @param options to update with parsed values.
    103      * @return the options passed in (for convenience)
    104      * @throws IllegalArgumentException if the syntax is invalid
    105      */
    106     public static Options parseOptions(String spec, Options options)
    107             throws IllegalArgumentException {
    108         for (String param : spec.split(" +")) {
    109             if (param.length() == 0) continue;
    110             if (param.startsWith("backoff=")) {
    111                 String[] pieces = param.substring(8).split("\\+");
    112                 if (pieces.length > 3) {
    113                     throw new IllegalArgumentException("bad value for backoff: [" + spec + "]");
    114                 }
    115                 if (pieces.length > 0 && pieces[0].length() > 0) {
    116                     options.backoffFixedMillis = parseSeconds(pieces[0]);
    117                 }
    118                 if (pieces.length > 1 && pieces[1].length() > 0) {
    119                     options.backoffIncrementalMillis = parseSeconds(pieces[1]);
    120                 }
    121                 if (pieces.length > 2 && pieces[2].length() > 0) {
    122                     options.backoffExponentialMillis = (int)parseSeconds(pieces[2]);
    123                 }
    124             } else if (param.startsWith("max=")) {
    125                 options.maxMoratoriumMillis = parseSeconds(param.substring(4));
    126             } else if (param.startsWith("min=")) {
    127                 options.minTriggerMillis = parseSeconds(param.substring(4));
    128             } else if (param.startsWith("period=")) {
    129                 options.periodicIntervalMillis = parseSeconds(param.substring(7));
    130             } else {
    131                 options.periodicIntervalMillis = parseSeconds(param);
    132             }
    133         }
    134         return options;
    135     }
    136 
    137     private static long parseSeconds(String param) throws NumberFormatException {
    138         return (long) (Float.parseFloat(param) * 1000);
    139     }
    140 
    141     /**
    142      * Compute the time of the next operation.  Does not modify any state
    143      * (unless the clock rolls backwards, in which case timers are reset).
    144      *
    145      * @param options to use for this computation.
    146      * @return the wall clock time ({@link System#currentTimeMillis()}) when the
    147      * next operation should be attempted -- immediately, if the return value is
    148      * before the current time.
    149      */
    150     public long getNextTimeMillis(Options options) {
    151         boolean enabledState = mStorage.getBoolean(PREFIX + "enabledState", true);
    152         if (!enabledState) return Long.MAX_VALUE;
    153 
    154         boolean permanentError = mStorage.getBoolean(PREFIX + "permanentError", false);
    155         if (permanentError) return Long.MAX_VALUE;
    156 
    157         // We do quite a bit of limiting to prevent a clock rollback from totally
    158         // hosing the scheduler.  Times which are supposed to be in the past are
    159         // clipped to the current time so we don't languish forever.
    160 
    161         int errorCount = mStorage.getInt(PREFIX + "errorCount", 0);
    162         long now = currentTimeMillis();
    163         long lastSuccessTimeMillis = getTimeBefore(PREFIX + "lastSuccessTimeMillis", now);
    164         long lastErrorTimeMillis = getTimeBefore(PREFIX + "lastErrorTimeMillis", now);
    165         long triggerTimeMillis = mStorage.getLong(PREFIX + "triggerTimeMillis", Long.MAX_VALUE);
    166         long moratoriumSetMillis = getTimeBefore(PREFIX + "moratoriumSetTimeMillis", now);
    167         long moratoriumTimeMillis = getTimeBefore(PREFIX + "moratoriumTimeMillis",
    168                 moratoriumSetMillis + options.maxMoratoriumMillis);
    169 
    170         long time = triggerTimeMillis;
    171         if (options.periodicIntervalMillis > 0) {
    172             time = Math.min(time, lastSuccessTimeMillis + options.periodicIntervalMillis);
    173         }
    174 
    175         time = Math.max(time, moratoriumTimeMillis);
    176         time = Math.max(time, lastSuccessTimeMillis + options.minTriggerMillis);
    177         if (errorCount > 0) {
    178             int shift = errorCount-1;
    179             // backoffExponentialMillis is an int, so we can safely
    180             // double it 30 times without overflowing a long.
    181             if (shift > 30) shift = 30;
    182             long backoff = options.backoffFixedMillis +
    183                 (options.backoffIncrementalMillis * errorCount) +
    184                 (((long)options.backoffExponentialMillis) << shift);
    185 
    186             // Treat backoff like a moratorium: don't let the backoff time grow too large.
    187             backoff = Math.min(backoff, options.maxMoratoriumMillis);
    188 
    189             time = Math.max(time, lastErrorTimeMillis + backoff);
    190         }
    191         return time;
    192     }
    193 
    194     /**
    195      * Return the last time the operation completed.  Does not modify any state.
    196      *
    197      * @return the wall clock time when {@link #onSuccess()} was last called.
    198      */
    199     public long getLastSuccessTimeMillis() {
    200         return mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0);
    201     }
    202 
    203     /**
    204      * Return the last time the operation was attempted.  Does not modify any state.
    205      *
    206      * @return the wall clock time when {@link #onSuccess()} or {@link
    207      * #onTransientError()} was last called.
    208      */
    209     public long getLastAttemptTimeMillis() {
    210         return Math.max(
    211                 mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0),
    212                 mStorage.getLong(PREFIX + "lastErrorTimeMillis", 0));
    213     }
    214 
    215     /**
    216      * Fetch a {@link SharedPreferences} property, but force it to be before
    217      * a certain time, updating the value if necessary.  This is to recover
    218      * gracefully from clock rollbacks which could otherwise strand our timers.
    219      *
    220      * @param name of SharedPreferences key
    221      * @param max time to allow in result
    222      * @return current value attached to key (default 0), limited by max
    223      */
    224     private long getTimeBefore(String name, long max) {
    225         long time = mStorage.getLong(name, 0);
    226         if (time > max) {
    227             time = max;
    228             SharedPreferencesCompat.apply(mStorage.edit().putLong(name, time));
    229         }
    230         return time;
    231     }
    232 
    233     /**
    234      * Request an operation to be performed at a certain time.  The actual
    235      * scheduled time may be affected by error backoff logic and defined
    236      * minimum intervals.  Use {@link Long#MAX_VALUE} to disable triggering.
    237      *
    238      * @param millis wall clock time ({@link System#currentTimeMillis()}) to
    239      * trigger another operation; 0 to trigger immediately
    240      */
    241     public void setTriggerTimeMillis(long millis) {
    242         SharedPreferencesCompat.apply(
    243                 mStorage.edit().putLong(PREFIX + "triggerTimeMillis", millis));
    244     }
    245 
    246     /**
    247      * Forbid any operations until after a certain (absolute) time.
    248      * Limited by {@link Options#maxMoratoriumMillis}.
    249      *
    250      * @param millis wall clock time ({@link System#currentTimeMillis()})
    251      * when operations should be allowed again; 0 to remove moratorium
    252      */
    253     public void setMoratoriumTimeMillis(long millis) {
    254         SharedPreferencesCompat.apply(mStorage.edit()
    255                    .putLong(PREFIX + "moratoriumTimeMillis", millis)
    256                    .putLong(PREFIX + "moratoriumSetTimeMillis", currentTimeMillis()));
    257     }
    258 
    259     /**
    260      * Forbid any operations until after a certain time, as specified in
    261      * the format used by the HTTP "Retry-After" header.
    262      * Limited by {@link Options#maxMoratoriumMillis}.
    263      *
    264      * @param retryAfter moratorium time in HTTP format
    265      * @return true if a time was successfully parsed
    266      */
    267     public boolean setMoratoriumTimeHttp(String retryAfter) {
    268         try {
    269             long ms = Long.parseLong(retryAfter) * 1000;
    270             setMoratoriumTimeMillis(ms + currentTimeMillis());
    271             return true;
    272         } catch (NumberFormatException nfe) {
    273             try {
    274                 setMoratoriumTimeMillis(LegacyHttpDateTime.parse(retryAfter));
    275                 return true;
    276             } catch (IllegalArgumentException iae) {
    277                 return false;
    278             }
    279         }
    280     }
    281 
    282     /**
    283      * Enable or disable all operations.  When disabled, all calls to
    284      * {@link #getNextTimeMillis} return {@link Long#MAX_VALUE}.
    285      * Commonly used when data network availability goes up and down.
    286      *
    287      * @param enabled if operations can be performed
    288      */
    289     public void setEnabledState(boolean enabled) {
    290         SharedPreferencesCompat.apply(
    291                 mStorage.edit().putBoolean(PREFIX + "enabledState", enabled));
    292     }
    293 
    294     /**
    295      * Report successful completion of an operation.  Resets all error
    296      * counters, clears any trigger directives, and records the success.
    297      */
    298     public void onSuccess() {
    299         resetTransientError();
    300         resetPermanentError();
    301         SharedPreferencesCompat.apply(mStorage.edit()
    302                 .remove(PREFIX + "errorCount")
    303                 .remove(PREFIX + "lastErrorTimeMillis")
    304                 .remove(PREFIX + "permanentError")
    305                 .remove(PREFIX + "triggerTimeMillis")
    306                 .putLong(PREFIX + "lastSuccessTimeMillis", currentTimeMillis()));
    307     }
    308 
    309     /**
    310      * Report a transient error (usually a network failure).  Increments
    311      * the error count and records the time of the latest error for backoff
    312      * purposes.
    313      */
    314     public void onTransientError() {
    315         SharedPreferences.Editor editor = mStorage.edit();
    316         editor.putLong(PREFIX + "lastErrorTimeMillis", currentTimeMillis());
    317         editor.putInt(PREFIX + "errorCount",
    318                 mStorage.getInt(PREFIX + "errorCount", 0) + 1);
    319         SharedPreferencesCompat.apply(editor);
    320     }
    321 
    322     /**
    323      * Reset all transient error counts, allowing the next operation to proceed
    324      * immediately without backoff.  Commonly used on network state changes, when
    325      * partial progress occurs (some data received), and in other circumstances
    326      * where there is reason to hope things might start working better.
    327      */
    328     public void resetTransientError() {
    329         SharedPreferencesCompat.apply(mStorage.edit().remove(PREFIX + "errorCount"));
    330     }
    331 
    332     /**
    333      * Report a permanent error that will not go away until further notice.
    334      * No operation will be scheduled until {@link #resetPermanentError()}
    335      * is called.  Commonly used for authentication failures (which are reset
    336      * when the accounts database is updated).
    337      */
    338     public void onPermanentError() {
    339         SharedPreferencesCompat.apply(mStorage.edit().putBoolean(PREFIX + "permanentError", true));
    340     }
    341 
    342     /**
    343      * Reset any permanent error status set by {@link #onPermanentError},
    344      * allowing operations to be scheduled as normal.
    345      */
    346     public void resetPermanentError() {
    347         SharedPreferencesCompat.apply(mStorage.edit().remove(PREFIX + "permanentError"));
    348     }
    349 
    350     /**
    351      * Return a string description of the scheduler state for debugging.
    352      */
    353     public String toString() {
    354         StringBuilder out = new StringBuilder("[OperationScheduler:");
    355         TreeMap<String, Object> copy = new TreeMap<String, Object>(mStorage.getAll());  // Sort keys
    356         for (Map.Entry<String, Object> e : copy.entrySet()) {
    357             String key = e.getKey();
    358             if (key.startsWith(PREFIX)) {
    359                 if (key.endsWith("TimeMillis")) {
    360                     Time time = new Time();
    361                     time.set((Long) e.getValue());
    362                     out.append(" ").append(key.substring(PREFIX.length(), key.length() - 10));
    363                     out.append("=").append(time.format("%Y-%m-%d/%H:%M:%S"));
    364                 } else {
    365                     out.append(" ").append(key.substring(PREFIX.length()));
    366                     Object v = e.getValue();
    367                     if (v == null) {
    368                         out.append("=(null)");
    369                     } else {
    370                         out.append("=").append(v.toString());
    371                     }
    372                 }
    373             }
    374         }
    375         return out.append("]").toString();
    376     }
    377 
    378     /**
    379      * Gets the current time.  Can be overridden for unit testing.
    380      *
    381      * @return {@link System#currentTimeMillis()}
    382      */
    383     protected long currentTimeMillis() {
    384         return System.currentTimeMillis();
    385     }
    386 }
    387