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
    187             // time grow too large.
    188             if (moratoriumTimeMillis > 0 && backoff > moratoriumTimeMillis) {
    189                 backoff = moratoriumTimeMillis;
    190             }
    191 
    192             time = Math.max(time, lastErrorTimeMillis + backoff);
    193         }
    194         return time;
    195     }
    196 
    197     /**
    198      * Return the last time the operation completed.  Does not modify any state.
    199      *
    200      * @return the wall clock time when {@link #onSuccess()} was last called.
    201      */
    202     public long getLastSuccessTimeMillis() {
    203         return mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0);
    204     }
    205 
    206     /**
    207      * Return the last time the operation was attempted.  Does not modify any state.
    208      *
    209      * @return the wall clock time when {@link #onSuccess()} or {@link
    210      * #onTransientError()} was last called.
    211      */
    212     public long getLastAttemptTimeMillis() {
    213         return Math.max(
    214                 mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0),
    215                 mStorage.getLong(PREFIX + "lastErrorTimeMillis", 0));
    216     }
    217 
    218     /**
    219      * Fetch a {@link SharedPreferences} property, but force it to be before
    220      * a certain time, updating the value if necessary.  This is to recover
    221      * gracefully from clock rollbacks which could otherwise strand our timers.
    222      *
    223      * @param name of SharedPreferences key
    224      * @param max time to allow in result
    225      * @return current value attached to key (default 0), limited by max
    226      */
    227     private long getTimeBefore(String name, long max) {
    228         long time = mStorage.getLong(name, 0);
    229         if (time > max) {
    230             time = max;
    231             SharedPreferencesCompat.apply(mStorage.edit().putLong(name, time));
    232         }
    233         return time;
    234     }
    235 
    236     /**
    237      * Request an operation to be performed at a certain time.  The actual
    238      * scheduled time may be affected by error backoff logic and defined
    239      * minimum intervals.  Use {@link Long#MAX_VALUE} to disable triggering.
    240      *
    241      * @param millis wall clock time ({@link System#currentTimeMillis()}) to
    242      * trigger another operation; 0 to trigger immediately
    243      */
    244     public void setTriggerTimeMillis(long millis) {
    245         SharedPreferencesCompat.apply(
    246                 mStorage.edit().putLong(PREFIX + "triggerTimeMillis", millis));
    247     }
    248 
    249     /**
    250      * Forbid any operations until after a certain (absolute) time.
    251      * Limited by {@link Options#maxMoratoriumMillis}.
    252      *
    253      * @param millis wall clock time ({@link System#currentTimeMillis()})
    254      * when operations should be allowed again; 0 to remove moratorium
    255      */
    256     public void setMoratoriumTimeMillis(long millis) {
    257         SharedPreferencesCompat.apply(mStorage.edit()
    258                    .putLong(PREFIX + "moratoriumTimeMillis", millis)
    259                    .putLong(PREFIX + "moratoriumSetTimeMillis", currentTimeMillis()));
    260     }
    261 
    262     /**
    263      * Forbid any operations until after a certain time, as specified in
    264      * the format used by the HTTP "Retry-After" header.
    265      * Limited by {@link Options#maxMoratoriumMillis}.
    266      *
    267      * @param retryAfter moratorium time in HTTP format
    268      * @return true if a time was successfully parsed
    269      */
    270     public boolean setMoratoriumTimeHttp(String retryAfter) {
    271         try {
    272             long ms = Long.parseLong(retryAfter) * 1000;
    273             setMoratoriumTimeMillis(ms + currentTimeMillis());
    274             return true;
    275         } catch (NumberFormatException nfe) {
    276             try {
    277                 setMoratoriumTimeMillis(LegacyHttpDateTime.parse(retryAfter));
    278                 return true;
    279             } catch (IllegalArgumentException iae) {
    280                 return false;
    281             }
    282         }
    283     }
    284 
    285     /**
    286      * Enable or disable all operations.  When disabled, all calls to
    287      * {@link #getNextTimeMillis} return {@link Long#MAX_VALUE}.
    288      * Commonly used when data network availability goes up and down.
    289      *
    290      * @param enabled if operations can be performed
    291      */
    292     public void setEnabledState(boolean enabled) {
    293         SharedPreferencesCompat.apply(
    294                 mStorage.edit().putBoolean(PREFIX + "enabledState", enabled));
    295     }
    296 
    297     /**
    298      * Report successful completion of an operation.  Resets all error
    299      * counters, clears any trigger directives, and records the success.
    300      */
    301     public void onSuccess() {
    302         resetTransientError();
    303         resetPermanentError();
    304         SharedPreferencesCompat.apply(mStorage.edit()
    305                 .remove(PREFIX + "errorCount")
    306                 .remove(PREFIX + "lastErrorTimeMillis")
    307                 .remove(PREFIX + "permanentError")
    308                 .remove(PREFIX + "triggerTimeMillis")
    309                 .putLong(PREFIX + "lastSuccessTimeMillis", currentTimeMillis()));
    310     }
    311 
    312     /**
    313      * Report a transient error (usually a network failure).  Increments
    314      * the error count and records the time of the latest error for backoff
    315      * purposes.
    316      */
    317     public void onTransientError() {
    318         SharedPreferences.Editor editor = mStorage.edit();
    319         editor.putLong(PREFIX + "lastErrorTimeMillis", currentTimeMillis());
    320         editor.putInt(PREFIX + "errorCount",
    321                 mStorage.getInt(PREFIX + "errorCount", 0) + 1);
    322         SharedPreferencesCompat.apply(editor);
    323     }
    324 
    325     /**
    326      * Reset all transient error counts, allowing the next operation to proceed
    327      * immediately without backoff.  Commonly used on network state changes, when
    328      * partial progress occurs (some data received), and in other circumstances
    329      * where there is reason to hope things might start working better.
    330      */
    331     public void resetTransientError() {
    332         SharedPreferencesCompat.apply(mStorage.edit().remove(PREFIX + "errorCount"));
    333     }
    334 
    335     /**
    336      * Report a permanent error that will not go away until further notice.
    337      * No operation will be scheduled until {@link #resetPermanentError()}
    338      * is called.  Commonly used for authentication failures (which are reset
    339      * when the accounts database is updated).
    340      */
    341     public void onPermanentError() {
    342         SharedPreferencesCompat.apply(mStorage.edit().putBoolean(PREFIX + "permanentError", true));
    343     }
    344 
    345     /**
    346      * Reset any permanent error status set by {@link #onPermanentError},
    347      * allowing operations to be scheduled as normal.
    348      */
    349     public void resetPermanentError() {
    350         SharedPreferencesCompat.apply(mStorage.edit().remove(PREFIX + "permanentError"));
    351     }
    352 
    353     /**
    354      * Return a string description of the scheduler state for debugging.
    355      */
    356     public String toString() {
    357         StringBuilder out = new StringBuilder("[OperationScheduler:");
    358         TreeMap<String, Object> copy = new TreeMap<String, Object>(mStorage.getAll());  // Sort keys
    359         for (Map.Entry<String, Object> e : copy.entrySet()) {
    360             String key = e.getKey();
    361             if (key.startsWith(PREFIX)) {
    362                 if (key.endsWith("TimeMillis")) {
    363                     Time time = new Time();
    364                     time.set((Long) e.getValue());
    365                     out.append(" ").append(key.substring(PREFIX.length(), key.length() - 10));
    366                     out.append("=").append(time.format("%Y-%m-%d/%H:%M:%S"));
    367                 } else {
    368                     out.append(" ").append(key.substring(PREFIX.length()));
    369                     Object v = e.getValue();
    370                     if (v == null) {
    371                         out.append("=(null)");
    372                     } else {
    373                         out.append("=").append(v.toString());
    374                     }
    375                 }
    376             }
    377         }
    378         return out.append("]").toString();
    379     }
    380 
    381     /**
    382      * Gets the current time.  Can be overridden for unit testing.
    383      *
    384      * @return {@link System#currentTimeMillis()}
    385      */
    386     protected long currentTimeMillis() {
    387         return System.currentTimeMillis();
    388     }
    389 }
    390