Merge "Fix OperationScheduler moratorium calculation for clock rollback case." into froyo

This commit is contained in:
Dan Egnor
2010-04-08 17:38:28 -07:00
committed by Android (Google) Code Review
2 changed files with 114 additions and 55 deletions

View File

@@ -124,7 +124,8 @@ public class OperationScheduler {
} }
/** /**
* Compute the time of the next operation. Does not modify any state. * 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. * @param options to use for this computation.
* @return the wall clock time ({@link System#currentTimeMillis()}) when the * @return the wall clock time ({@link System#currentTimeMillis()}) when the
@@ -143,11 +144,11 @@ public class OperationScheduler {
// clipped to the current time so we don't languish forever. // clipped to the current time so we don't languish forever.
int errorCount = mStorage.getInt(PREFIX + "errorCount", 0); int errorCount = mStorage.getInt(PREFIX + "errorCount", 0);
long now = System.currentTimeMillis(); long now = currentTimeMillis();
long lastSuccessTimeMillis = getTimeBefore(PREFIX + "lastSuccessTimeMillis", now); long lastSuccessTimeMillis = getTimeBefore(PREFIX + "lastSuccessTimeMillis", now);
long lastErrorTimeMillis = getTimeBefore(PREFIX + "lastErrorTimeMillis", now); long lastErrorTimeMillis = getTimeBefore(PREFIX + "lastErrorTimeMillis", now);
long triggerTimeMillis = mStorage.getLong(PREFIX + "triggerTimeMillis", Long.MAX_VALUE); long triggerTimeMillis = mStorage.getLong(PREFIX + "triggerTimeMillis", Long.MAX_VALUE);
long moratoriumSetMillis = mStorage.getLong(PREFIX + "moratoriumSetTimeMillis", 0); long moratoriumSetMillis = getTimeBefore(PREFIX + "moratoriumSetTimeMillis", now);
long moratoriumTimeMillis = getTimeBefore(PREFIX + "moratoriumTimeMillis", long moratoriumTimeMillis = getTimeBefore(PREFIX + "moratoriumTimeMillis",
moratoriumSetMillis + options.maxMoratoriumMillis); moratoriumSetMillis + options.maxMoratoriumMillis);
@@ -155,9 +156,8 @@ public class OperationScheduler {
if (options.periodicIntervalMillis > 0) { if (options.periodicIntervalMillis > 0) {
time = Math.min(time, lastSuccessTimeMillis + options.periodicIntervalMillis); time = Math.min(time, lastSuccessTimeMillis + options.periodicIntervalMillis);
} }
if (time >= moratoriumTimeMillis - options.maxMoratoriumMillis) {
time = Math.max(time, moratoriumTimeMillis); time = Math.max(time, moratoriumTimeMillis);
}
time = Math.max(time, lastSuccessTimeMillis + options.minTriggerMillis); time = Math.max(time, lastSuccessTimeMillis + options.minTriggerMillis);
if (errorCount > 0) { if (errorCount > 0) {
time = Math.max(time, lastErrorTimeMillis + options.backoffFixedMillis + time = Math.max(time, lastErrorTimeMillis + options.backoffFixedMillis +
@@ -205,7 +205,7 @@ public class OperationScheduler {
/** /**
* Request an operation to be performed at a certain time. The actual * Request an operation to be performed at a certain time. The actual
* scheduled time may be affected by error backoff logic and defined * scheduled time may be affected by error backoff logic and defined
* minimum intervals. * minimum intervals. Use {@link Long#MAX_VALUE} to disable triggering.
* *
* @param millis wall clock time ({@link System#currentTimeMillis()}) to * @param millis wall clock time ({@link System#currentTimeMillis()}) to
* trigger another operation; 0 to trigger immediately * trigger another operation; 0 to trigger immediately
@@ -218,13 +218,13 @@ public class OperationScheduler {
* Forbid any operations until after a certain (absolute) time. * Forbid any operations until after a certain (absolute) time.
* Limited by {@link #Options.maxMoratoriumMillis}. * Limited by {@link #Options.maxMoratoriumMillis}.
* *
* @param millis wall clock time ({@link System#currentTimeMillis()}) to * @param millis wall clock time ({@link System#currentTimeMillis()})
* wait before attempting any more operations; 0 to remove moratorium * when operations should be allowed again; 0 to remove moratorium
*/ */
public void setMoratoriumTimeMillis(long millis) { public void setMoratoriumTimeMillis(long millis) {
mStorage.edit() mStorage.edit()
.putLong(PREFIX + "moratoriumTimeMillis", millis) .putLong(PREFIX + "moratoriumTimeMillis", millis)
.putLong(PREFIX + "moratoriumSetTimeMillis", System.currentTimeMillis()) .putLong(PREFIX + "moratoriumSetTimeMillis", currentTimeMillis())
.commit(); .commit();
} }
@@ -239,7 +239,7 @@ public class OperationScheduler {
public boolean setMoratoriumTimeHttp(String retryAfter) { public boolean setMoratoriumTimeHttp(String retryAfter) {
try { try {
long ms = Long.valueOf(retryAfter) * 1000; long ms = Long.valueOf(retryAfter) * 1000;
setMoratoriumTimeMillis(ms + System.currentTimeMillis()); setMoratoriumTimeMillis(ms + currentTimeMillis());
return true; return true;
} catch (NumberFormatException nfe) { } catch (NumberFormatException nfe) {
try { try {
@@ -269,13 +269,12 @@ public class OperationScheduler {
public void onSuccess() { public void onSuccess() {
resetTransientError(); resetTransientError();
resetPermanentError(); resetPermanentError();
long now = System.currentTimeMillis();
mStorage.edit() mStorage.edit()
.remove(PREFIX + "errorCount") .remove(PREFIX + "errorCount")
.remove(PREFIX + "lastErrorTimeMillis") .remove(PREFIX + "lastErrorTimeMillis")
.remove(PREFIX + "permanentError") .remove(PREFIX + "permanentError")
.remove(PREFIX + "triggerTimeMillis") .remove(PREFIX + "triggerTimeMillis")
.putLong(PREFIX + "lastSuccessTimeMillis", now).commit(); .putLong(PREFIX + "lastSuccessTimeMillis", currentTimeMillis()).commit();
} }
/** /**
@@ -284,8 +283,7 @@ public class OperationScheduler {
* purposes. * purposes.
*/ */
public void onTransientError() { public void onTransientError() {
long now = System.currentTimeMillis(); mStorage.edit().putLong(PREFIX + "lastErrorTimeMillis", currentTimeMillis()).commit();
mStorage.edit().putLong(PREFIX + "lastErrorTimeMillis", now).commit();
mStorage.edit().putInt(PREFIX + "errorCount", mStorage.edit().putInt(PREFIX + "errorCount",
mStorage.getInt(PREFIX + "errorCount", 0) + 1).commit(); mStorage.getInt(PREFIX + "errorCount", 0) + 1).commit();
} }
@@ -338,4 +336,13 @@ public class OperationScheduler {
} }
return out.append("]").toString(); return out.append("]").toString();
} }
/**
* Gets the current time. Can be overridden for unit testing.
*
* @return {@link System#currentTimeMillis()}
*/
protected long currentTimeMillis() {
return System.currentTimeMillis();
}
} }

View File

@@ -22,19 +22,34 @@ import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
public class OperationSchedulerTest extends AndroidTestCase { public class OperationSchedulerTest extends AndroidTestCase {
/**
* OperationScheduler subclass which uses an artificial time.
* Set {@link #timeMillis} to whatever value you like.
*/
private class TimeTravelScheduler extends OperationScheduler {
static final long DEFAULT_TIME = 1250146800000L; // 13-Aug-2009, 12:00:00 am
public long timeMillis = DEFAULT_TIME;
@Override
protected long currentTimeMillis() { return timeMillis; }
public TimeTravelScheduler() { super(getFreshStorage()); }
}
private SharedPreferences getFreshStorage() {
SharedPreferences sp = getContext().getSharedPreferences("OperationSchedulerTest", 0);
sp.edit().clear().commit();
return sp;
}
@MediumTest @MediumTest
public void testScheduler() throws Exception { public void testScheduler() throws Exception {
String name = "OperationSchedulerTest.testScheduler"; TimeTravelScheduler scheduler = new TimeTravelScheduler();
SharedPreferences storage = getContext().getSharedPreferences(name, 0);
storage.edit().clear().commit();
OperationScheduler scheduler = new OperationScheduler(storage);
OperationScheduler.Options options = new OperationScheduler.Options(); OperationScheduler.Options options = new OperationScheduler.Options();
assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options)); assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options));
assertEquals(0, scheduler.getLastSuccessTimeMillis()); assertEquals(0, scheduler.getLastSuccessTimeMillis());
assertEquals(0, scheduler.getLastAttemptTimeMillis()); assertEquals(0, scheduler.getLastAttemptTimeMillis());
long beforeTrigger = System.currentTimeMillis(); long beforeTrigger = scheduler.timeMillis;
scheduler.setTriggerTimeMillis(beforeTrigger + 1000000); scheduler.setTriggerTimeMillis(beforeTrigger + 1000000);
assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options));
@@ -51,33 +66,26 @@ public class OperationSchedulerTest extends AndroidTestCase {
assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options));
// Backoff interval after an error // Backoff interval after an error
long beforeError = System.currentTimeMillis(); long beforeError = (scheduler.timeMillis += 100);
scheduler.onTransientError(); scheduler.onTransientError();
long afterError = System.currentTimeMillis();
assertEquals(0, scheduler.getLastSuccessTimeMillis()); assertEquals(0, scheduler.getLastSuccessTimeMillis());
assertTrue(beforeError <= scheduler.getLastAttemptTimeMillis()); assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
assertTrue(afterError >= scheduler.getLastAttemptTimeMillis());
assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options));
options.backoffFixedMillis = 1000000; options.backoffFixedMillis = 1000000;
options.backoffIncrementalMillis = 500000; options.backoffIncrementalMillis = 500000;
assertTrue(beforeError + 1500000 <= scheduler.getNextTimeMillis(options)); assertEquals(beforeError + 1500000, scheduler.getNextTimeMillis(options));
assertTrue(afterError + 1500000 >= scheduler.getNextTimeMillis(options));
// Two errors: backoff interval increases // Two errors: backoff interval increases
beforeError = System.currentTimeMillis(); beforeError = (scheduler.timeMillis += 100);
scheduler.onTransientError(); scheduler.onTransientError();
afterError = System.currentTimeMillis(); assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
assertTrue(beforeError <= scheduler.getLastAttemptTimeMillis()); assertEquals(beforeError + 2000000, scheduler.getNextTimeMillis(options));
assertTrue(afterError >= scheduler.getLastAttemptTimeMillis());
assertTrue(beforeError + 2000000 <= scheduler.getNextTimeMillis(options));
assertTrue(afterError + 2000000 >= scheduler.getNextTimeMillis(options));
// Reset transient error: no backoff interval // Reset transient error: no backoff interval
scheduler.resetTransientError(); scheduler.resetTransientError();
assertEquals(0, scheduler.getLastSuccessTimeMillis()); assertEquals(0, scheduler.getLastSuccessTimeMillis());
assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options));
assertTrue(beforeError <= scheduler.getLastAttemptTimeMillis()); assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
assertTrue(afterError >= scheduler.getLastAttemptTimeMillis());
// Permanent error holds true even if transient errors are reset // Permanent error holds true even if transient errors are reset
// However, we remember that the transient error was reset... // However, we remember that the transient error was reset...
@@ -89,30 +97,26 @@ public class OperationSchedulerTest extends AndroidTestCase {
assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options));
// Success resets the trigger // Success resets the trigger
long beforeSuccess = System.currentTimeMillis(); long beforeSuccess = (scheduler.timeMillis += 100);
scheduler.onSuccess(); scheduler.onSuccess();
long afterSuccess = System.currentTimeMillis(); assertEquals(beforeSuccess, scheduler.getLastAttemptTimeMillis());
assertTrue(beforeSuccess <= scheduler.getLastAttemptTimeMillis()); assertEquals(beforeSuccess, scheduler.getLastSuccessTimeMillis());
assertTrue(afterSuccess >= scheduler.getLastAttemptTimeMillis());
assertTrue(beforeSuccess <= scheduler.getLastSuccessTimeMillis());
assertTrue(afterSuccess >= scheduler.getLastSuccessTimeMillis());
assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options)); assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options));
// The moratorium is not reset by success! // The moratorium is not reset by success!
scheduler.setTriggerTimeMillis(beforeSuccess + 500000); scheduler.setTriggerTimeMillis(0);
assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options));
scheduler.setMoratoriumTimeMillis(0); scheduler.setMoratoriumTimeMillis(0);
assertEquals(beforeSuccess + 500000, scheduler.getNextTimeMillis(options)); assertEquals(beforeSuccess, scheduler.getNextTimeMillis(options));
// Periodic interval after success // Periodic interval after success
options.periodicIntervalMillis = 250000; options.periodicIntervalMillis = 250000;
assertTrue(beforeSuccess + 250000 <= scheduler.getNextTimeMillis(options)); scheduler.setTriggerTimeMillis(Long.MAX_VALUE);
assertTrue(afterSuccess + 250000 >= scheduler.getNextTimeMillis(options)); assertEquals(beforeSuccess + 250000, scheduler.getNextTimeMillis(options));
// Trigger minimum is also since the last success // Trigger minimum is also since the last success
options.minTriggerMillis = 1000000; options.minTriggerMillis = 1000000;
assertTrue(beforeSuccess + 1000000 <= scheduler.getNextTimeMillis(options)); assertEquals(beforeSuccess + 1000000, scheduler.getNextTimeMillis(options));
assertTrue(afterSuccess + 1000000 >= scheduler.getNextTimeMillis(options));
} }
@SmallTest @SmallTest
@@ -138,23 +142,19 @@ public class OperationSchedulerTest extends AndroidTestCase {
@SmallTest @SmallTest
public void testMoratoriumWithHttpDate() throws Exception { public void testMoratoriumWithHttpDate() throws Exception {
String name = "OperationSchedulerTest.testMoratoriumWithHttpDate"; TimeTravelScheduler scheduler = new TimeTravelScheduler();
SharedPreferences storage = getContext().getSharedPreferences(name, 0);
storage.edit().clear().commit();
OperationScheduler scheduler = new OperationScheduler(storage);
OperationScheduler.Options options = new OperationScheduler.Options(); OperationScheduler.Options options = new OperationScheduler.Options();
long beforeTrigger = System.currentTimeMillis(); long beforeTrigger = scheduler.timeMillis;
scheduler.setTriggerTimeMillis(beforeTrigger + 1000000); scheduler.setTriggerTimeMillis(beforeTrigger + 1000000);
assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options));
scheduler.setMoratoriumTimeMillis(beforeTrigger + 2000000); scheduler.setMoratoriumTimeMillis(beforeTrigger + 2000000);
assertEquals(beforeTrigger + 2000000, scheduler.getNextTimeMillis(options)); assertEquals(beforeTrigger + 2000000, scheduler.getNextTimeMillis(options));
long beforeMoratorium = System.currentTimeMillis(); long beforeMoratorium = scheduler.timeMillis;
assertTrue(scheduler.setMoratoriumTimeHttp("3000")); assertTrue(scheduler.setMoratoriumTimeHttp("3000"));
long afterMoratorium = System.currentTimeMillis(); long afterMoratorium = scheduler.timeMillis;
assertTrue(beforeMoratorium + 3000000 <= scheduler.getNextTimeMillis(options)); assertTrue(beforeMoratorium + 3000000 <= scheduler.getNextTimeMillis(options));
assertTrue(afterMoratorium + 3000000 >= scheduler.getNextTimeMillis(options)); assertTrue(afterMoratorium + 3000000 >= scheduler.getNextTimeMillis(options));
@@ -164,4 +164,56 @@ public class OperationSchedulerTest extends AndroidTestCase {
assertFalse(scheduler.setMoratoriumTimeHttp("not actually a date")); assertFalse(scheduler.setMoratoriumTimeHttp("not actually a date"));
} }
@SmallTest
public void testClockRollbackScenario() throws Exception {
TimeTravelScheduler scheduler = new TimeTravelScheduler();
OperationScheduler.Options options = new OperationScheduler.Options();
options.minTriggerMillis = 2000;
// First, set up a scheduler with reasons to wait: a transient
// error with backoff and a moratorium for a few minutes.
long beforeTrigger = scheduler.timeMillis;
long triggerTime = beforeTrigger - 10000000;
scheduler.setTriggerTimeMillis(triggerTime);
assertEquals(triggerTime, scheduler.getNextTimeMillis(options));
assertEquals(0, scheduler.getLastAttemptTimeMillis());
long beforeSuccess = (scheduler.timeMillis += 100);
scheduler.onSuccess();
scheduler.setTriggerTimeMillis(triggerTime);
assertEquals(beforeSuccess, scheduler.getLastAttemptTimeMillis());
assertEquals(beforeSuccess + 2000, scheduler.getNextTimeMillis(options));
long beforeError = (scheduler.timeMillis += 100);
scheduler.onTransientError();
assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
assertEquals(beforeError + 5000, scheduler.getNextTimeMillis(options));
long beforeMoratorium = (scheduler.timeMillis += 100);
scheduler.setMoratoriumTimeMillis(beforeTrigger + 1000000);
assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options));
// Now set the time back a few seconds.
// The moratorium time should still be honored.
long beforeRollback = (scheduler.timeMillis = beforeTrigger - 10000);
assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options));
// The rollback also moved the last-attempt clock back to the rollback time.
assertEquals(scheduler.timeMillis, scheduler.getLastAttemptTimeMillis());
// But if we set the time back more than a day, the moratorium
// resets to the maximum moratorium (a day, by default), exposing
// the original trigger time.
beforeRollback = (scheduler.timeMillis = beforeTrigger - 100000000);
assertEquals(triggerTime, scheduler.getNextTimeMillis(options));
assertEquals(beforeRollback, scheduler.getLastAttemptTimeMillis());
// If we roll forward until after the re-set moratorium, then it expires.
scheduler.timeMillis = triggerTime + 5000000;
assertEquals(triggerTime, scheduler.getNextTimeMillis(options));
assertEquals(beforeRollback, scheduler.getLastAttemptTimeMillis());
assertEquals(beforeRollback, scheduler.getLastSuccessTimeMillis());
}
} }