OSDN Git Service

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