FileDocCategorySizeDatePackage
OperationScheduler.javaAPI DocAndroid 5.1 API16342Thu Mar 12 22:22:48 GMT 2015com.android.common

OperationScheduler

public class OperationScheduler extends Object
Tracks the success/failure history of a particular network operation in persistent storage and computes retry strategy accordingly. Handles exponential backoff, periodic rescheduling, event-driven triggering, retry-after moratorium intervals, etc. based on caller-specified parameters.

This class does not directly perform or invoke any operations, it only keeps track of the schedule. Somebody else needs to call {@link #getNextTimeMillis} as appropriate and do the actual work.

Fields Summary
private static final String
PREFIX
private final android.content.SharedPreferences
mStorage
Constructors Summary
public OperationScheduler(android.content.SharedPreferences storage)
Initialize the scheduler state.

param
storage to use for recording the state of operations across restarts/reboots


                         
       
        mStorage = storage;
    
Methods Summary
protected longcurrentTimeMillis()
Gets the current time. Can be overridden for unit testing.

return
{@link System#currentTimeMillis()}

        return System.currentTimeMillis();
    
public longgetLastAttemptTimeMillis()
Return the last time the operation was attempted. Does not modify any state.

return
the wall clock time when {@link #onSuccess()} or {@link #onTransientError()} was last called.

        return Math.max(
                mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0),
                mStorage.getLong(PREFIX + "lastErrorTimeMillis", 0));
    
public longgetLastSuccessTimeMillis()
Return the last time the operation completed. Does not modify any state.

return
the wall clock time when {@link #onSuccess()} was last called.

        return mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0);
    
public longgetNextTimeMillis(com.android.common.OperationScheduler$Options options)
Compute the time of the next operation. Does not modify any state (unless the clock rolls backwards, in which case timers are reset).

param
options to use for this computation.
return
the wall clock time ({@link System#currentTimeMillis()}) when the next operation should be attempted -- immediately, if the return value is before the current time.

        boolean enabledState = mStorage.getBoolean(PREFIX + "enabledState", true);
        if (!enabledState) return Long.MAX_VALUE;

        boolean permanentError = mStorage.getBoolean(PREFIX + "permanentError", false);
        if (permanentError) return Long.MAX_VALUE;

        // We do quite a bit of limiting to prevent a clock rollback from totally
        // hosing the scheduler.  Times which are supposed to be in the past are
        // clipped to the current time so we don't languish forever.

        int errorCount = mStorage.getInt(PREFIX + "errorCount", 0);
        long now = currentTimeMillis();
        long lastSuccessTimeMillis = getTimeBefore(PREFIX + "lastSuccessTimeMillis", now);
        long lastErrorTimeMillis = getTimeBefore(PREFIX + "lastErrorTimeMillis", now);
        long triggerTimeMillis = mStorage.getLong(PREFIX + "triggerTimeMillis", Long.MAX_VALUE);
        long moratoriumSetMillis = getTimeBefore(PREFIX + "moratoriumSetTimeMillis", now);
        long moratoriumTimeMillis = getTimeBefore(PREFIX + "moratoriumTimeMillis",
                moratoriumSetMillis + options.maxMoratoriumMillis);

        long time = triggerTimeMillis;
        if (options.periodicIntervalMillis > 0) {
            time = Math.min(time, lastSuccessTimeMillis + options.periodicIntervalMillis);
        }

        time = Math.max(time, moratoriumTimeMillis);
        time = Math.max(time, lastSuccessTimeMillis + options.minTriggerMillis);
        if (errorCount > 0) {
            int shift = errorCount-1;
            // backoffExponentialMillis is an int, so we can safely
            // double it 30 times without overflowing a long.
            if (shift > 30) shift = 30;
            long backoff = options.backoffFixedMillis +
                (options.backoffIncrementalMillis * errorCount) +
                (((long)options.backoffExponentialMillis) << shift);

            // Treat backoff like a moratorium: don't let the backoff
            // time grow too large.
            if (moratoriumTimeMillis > 0 && backoff > moratoriumTimeMillis) {
                backoff = moratoriumTimeMillis;
            }

            time = Math.max(time, lastErrorTimeMillis + backoff);
        }
        return time;
    
private longgetTimeBefore(java.lang.String name, long max)
Fetch a {@link SharedPreferences} property, but force it to be before a certain time, updating the value if necessary. This is to recover gracefully from clock rollbacks which could otherwise strand our timers.

param
name of SharedPreferences key
param
max time to allow in result
return
current value attached to key (default 0), limited by max

        long time = mStorage.getLong(name, 0);
        if (time > max) {
            time = max;
            SharedPreferencesCompat.apply(mStorage.edit().putLong(name, time));
        }
        return time;
    
public voidonPermanentError()
Report a permanent error that will not go away until further notice. No operation will be scheduled until {@link #resetPermanentError()} is called. Commonly used for authentication failures (which are reset when the accounts database is updated).

        SharedPreferencesCompat.apply(mStorage.edit().putBoolean(PREFIX + "permanentError", true));
    
public voidonSuccess()
Report successful completion of an operation. Resets all error counters, clears any trigger directives, and records the success.

        resetTransientError();
        resetPermanentError();
        SharedPreferencesCompat.apply(mStorage.edit()
                .remove(PREFIX + "errorCount")
                .remove(PREFIX + "lastErrorTimeMillis")
                .remove(PREFIX + "permanentError")
                .remove(PREFIX + "triggerTimeMillis")
                .putLong(PREFIX + "lastSuccessTimeMillis", currentTimeMillis()));
    
public voidonTransientError()
Report a transient error (usually a network failure). Increments the error count and records the time of the latest error for backoff purposes.

        SharedPreferences.Editor editor = mStorage.edit();
        editor.putLong(PREFIX + "lastErrorTimeMillis", currentTimeMillis());
        editor.putInt(PREFIX + "errorCount",
                mStorage.getInt(PREFIX + "errorCount", 0) + 1);
        SharedPreferencesCompat.apply(editor);
    
public static com.android.common.OperationScheduler$OptionsparseOptions(java.lang.String spec, com.android.common.OperationScheduler$Options options)
Parse scheduler options supplied in this string form:
backoff=(fixed)+(incremental)[+(exponential)] max=(maxmoratorium) min=(mintrigger) [period=](interval)
All values are times in (possibly fractional) seconds (not milliseconds). Omitted settings are left at whatever existing default value was passed in.

The default options: backoff=0+5 max=86400 min=0 period=0
Fractions are OK: backoff=+2.5 period=10.0
The "period=" can be omitted: 3600

param
spec describing some or all scheduler options.
param
options to update with parsed values.
return
the options passed in (for convenience)
throws
IllegalArgumentException if the syntax is invalid

        for (String param : spec.split(" +")) {
            if (param.length() == 0) continue;
            if (param.startsWith("backoff=")) {
                String[] pieces = param.substring(8).split("\\+");
                if (pieces.length > 3) {
                    throw new IllegalArgumentException("bad value for backoff: [" + spec + "]");
                }
                if (pieces.length > 0 && pieces[0].length() > 0) {
                    options.backoffFixedMillis = parseSeconds(pieces[0]);
                }
                if (pieces.length > 1 && pieces[1].length() > 0) {
                    options.backoffIncrementalMillis = parseSeconds(pieces[1]);
                }
                if (pieces.length > 2 && pieces[2].length() > 0) {
                    options.backoffExponentialMillis = (int)parseSeconds(pieces[2]);
                }
            } else if (param.startsWith("max=")) {
                options.maxMoratoriumMillis = parseSeconds(param.substring(4));
            } else if (param.startsWith("min=")) {
                options.minTriggerMillis = parseSeconds(param.substring(4));
            } else if (param.startsWith("period=")) {
                options.periodicIntervalMillis = parseSeconds(param.substring(7));
            } else {
                options.periodicIntervalMillis = parseSeconds(param);
            }
        }
        return options;
    
private static longparseSeconds(java.lang.String param)

        return (long) (Float.parseFloat(param) * 1000);
    
public voidresetPermanentError()
Reset any permanent error status set by {@link #onPermanentError}, allowing operations to be scheduled as normal.

        SharedPreferencesCompat.apply(mStorage.edit().remove(PREFIX + "permanentError"));
    
public voidresetTransientError()
Reset all transient error counts, allowing the next operation to proceed immediately without backoff. Commonly used on network state changes, when partial progress occurs (some data received), and in other circumstances where there is reason to hope things might start working better.

        SharedPreferencesCompat.apply(mStorage.edit().remove(PREFIX + "errorCount"));
    
public voidsetEnabledState(boolean enabled)
Enable or disable all operations. When disabled, all calls to {@link #getNextTimeMillis} return {@link Long#MAX_VALUE}. Commonly used when data network availability goes up and down.

param
enabled if operations can be performed

        SharedPreferencesCompat.apply(
                mStorage.edit().putBoolean(PREFIX + "enabledState", enabled));
    
public booleansetMoratoriumTimeHttp(java.lang.String retryAfter)
Forbid any operations until after a certain time, as specified in the format used by the HTTP "Retry-After" header. Limited by {@link Options#maxMoratoriumMillis}.

param
retryAfter moratorium time in HTTP format
return
true if a time was successfully parsed

        try {
            long ms = Long.valueOf(retryAfter) * 1000;
            setMoratoriumTimeMillis(ms + currentTimeMillis());
            return true;
        } catch (NumberFormatException nfe) {
            try {
                setMoratoriumTimeMillis(AndroidHttpClient.parseDate(retryAfter));
                return true;
            } catch (IllegalArgumentException iae) {
                return false;
            }
        }
    
public voidsetMoratoriumTimeMillis(long millis)
Forbid any operations until after a certain (absolute) time. Limited by {@link Options#maxMoratoriumMillis}.

param
millis wall clock time ({@link System#currentTimeMillis()}) when operations should be allowed again; 0 to remove moratorium

        SharedPreferencesCompat.apply(mStorage.edit()
                   .putLong(PREFIX + "moratoriumTimeMillis", millis)
                   .putLong(PREFIX + "moratoriumSetTimeMillis", currentTimeMillis()));
    
public voidsetTriggerTimeMillis(long millis)
Request an operation to be performed at a certain time. The actual scheduled time may be affected by error backoff logic and defined minimum intervals. Use {@link Long#MAX_VALUE} to disable triggering.

param
millis wall clock time ({@link System#currentTimeMillis()}) to trigger another operation; 0 to trigger immediately

        SharedPreferencesCompat.apply(
                mStorage.edit().putLong(PREFIX + "triggerTimeMillis", millis));
    
public java.lang.StringtoString()
Return a string description of the scheduler state for debugging.

        StringBuilder out = new StringBuilder("[OperationScheduler:");
        TreeMap<String, Object> copy = new TreeMap<String, Object>(mStorage.getAll());  // Sort keys
        for (Map.Entry<String, Object> e : copy.entrySet()) {
            String key = e.getKey();
            if (key.startsWith(PREFIX)) {
                if (key.endsWith("TimeMillis")) {
                    Time time = new Time();
                    time.set((Long) e.getValue());
                    out.append(" ").append(key.substring(PREFIX.length(), key.length() - 10));
                    out.append("=").append(time.format("%Y-%m-%d/%H:%M:%S"));
                } else {
                    out.append(" ").append(key.substring(PREFIX.length()));
                    Object v = e.getValue();
                    if (v == null) {
                        out.append("=(null)");
                    } else {
                        out.append("=").append(v.toString());
                    }
                }
            }
        }
        return out.append("]").toString();