Fix periodic job scheduling.

If a periodic job was run outside of its expected window (eg: because of
Doze mode or its constraints were unmet for a long time), then
JobSchedulerService would schedule the next one with a window starting
right now, which could cause the job to run again immediately.
This change addresses the issue by:
1. Switching to having consistent windows based on when the job was
scheduled. This also helps developers know when to expect jobs to run.
2. If a job runs in a different window, its run counts toward that
window instead.
3. For jobs with a custom flex setting, the next window is skipped if
the job is run to close to the next window.

Bug: 123248627
Test: atest com.android.server.job.JobSchedulerServiceTest
Test: atest CtsJobSchedulerTestCases
Change-Id: Ic18929e9d03e704ec43aa50fe37c64cd6807cf68
This commit is contained in:
Kweku Adams
2019-04-30 11:54:48 -07:00
parent ebc8747546
commit c9793092e3
3 changed files with 488 additions and 12 deletions

View File

@@ -18,6 +18,7 @@ package com.android.server.job;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
@@ -1801,7 +1802,8 @@ public class JobSchedulerService extends com.android.server.SystemService
*
* @see #maybeQueueReadyJobsForExecutionLocked
*/
private JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) {
@VisibleForTesting
JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) {
final long elapsedNowMillis = sElapsedRealtimeClock.millis();
final JobInfo job = failureToReschedule.getJob();
@@ -1848,6 +1850,10 @@ public class JobSchedulerService extends com.android.server.SystemService
elapsedNowMillis + delayMillis,
JobStatus.NO_LATEST_RUNTIME, backoffAttempts,
failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis());
if (job.isPeriodic()) {
newJob.setOriginalLatestRunTimeElapsed(
failureToReschedule.getOriginalLatestRunTimeElapsed());
}
for (int ic=0; ic<mControllers.size(); ic++) {
StateController controller = mControllers.get(ic);
controller.rescheduleForFailureLocked(newJob, failureToReschedule);
@@ -1868,23 +1874,41 @@ public class JobSchedulerService extends com.android.server.SystemService
* @return A new job representing the execution criteria for this instantiation of the
* recurring job.
*/
private JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
@VisibleForTesting
JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
final long elapsedNow = sElapsedRealtimeClock.millis();
// Compute how much of the period is remaining.
long runEarly = 0L;
final long newLatestRuntimeElapsed;
final long period = periodicToReschedule.getJob().getIntervalMillis();
final long latestRunTimeElapsed = periodicToReschedule.getOriginalLatestRunTimeElapsed();
final long flex = periodicToReschedule.getJob().getFlexMillis();
// If this periodic was rescheduled it won't have a deadline.
if (periodicToReschedule.hasDeadlineConstraint()) {
runEarly = Math.max(periodicToReschedule.getLatestRunTimeElapsed() - elapsedNow, 0L);
if (elapsedNow > latestRunTimeElapsed) {
// The job ran past its expected run window. Have it count towards the current window
// and schedule a new job for the next window.
if (DEBUG) {
Slog.i(TAG, "Periodic job ran after its intended window.");
}
final long diffMs = (elapsedNow - latestRunTimeElapsed);
int numSkippedWindows = (int) (diffMs / period) + 1; // +1 to include original window
if (period != flex && diffMs > Math.min(30 * MINUTE_IN_MILLIS, (period - flex) / 2)) {
if (DEBUG) {
Slog.d(TAG, "Custom flex job ran too close to next window.");
}
// For custom flex periods, if the job was run too close to the next window,
// skip the next window and schedule for the following one.
numSkippedWindows += 1;
}
newLatestRuntimeElapsed = latestRunTimeElapsed + (period * numSkippedWindows);
} else {
newLatestRuntimeElapsed = latestRunTimeElapsed + period;
}
long flex = periodicToReschedule.getJob().getFlexMillis();
long period = periodicToReschedule.getJob().getIntervalMillis();
long newLatestRuntimeElapsed = elapsedNow + runEarly + period;
long newEarliestRunTimeElapsed = newLatestRuntimeElapsed - flex;
final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed - flex;
if (DEBUG) {
Slog.v(TAG, "Rescheduling executed periodic. New execution window [" +
newEarliestRunTimeElapsed/1000 + ", " + newLatestRuntimeElapsed/1000 + "]s");
newEarliestRunTimeElapsed / 1000 + ", " + newLatestRuntimeElapsed / 1000
+ "]s");
}
return new JobStatus(periodicToReschedule, getCurrentHeartbeat(),
newEarliestRunTimeElapsed, newLatestRuntimeElapsed,

View File

@@ -161,6 +161,12 @@ public final class JobStatus {
*/
private final long latestRunTimeElapsedMillis;
/**
* Valid only for periodic jobs. The original latest point in the future at which this
* job was expected to run.
*/
private long mOriginalLatestRunTimeElapsedMillis;
/** How many times this job has failed, used to compute back-off. */
private final int numFailures;
@@ -394,6 +400,7 @@ public final class JobStatus {
this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
this.numFailures = numFailures;
int requiredConstraints = job.getConstraintFlags();
@@ -871,6 +878,14 @@ public final class JobStatus {
return latestRunTimeElapsedMillis;
}
public long getOriginalLatestRunTimeElapsed() {
return mOriginalLatestRunTimeElapsedMillis;
}
public void setOriginalLatestRunTimeElapsed(long latestRunTimeElapsed) {
mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed;
}
/**
* Return the fractional position of "now" within the "run time" window of
* this job.

View File

@@ -0,0 +1,437 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.job;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.IActivityManager;
import android.app.job.JobInfo;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkPolicyManager;
import android.os.BatteryManagerInternal;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import com.android.server.AppStateTracker;
import com.android.server.DeviceIdleController;
import com.android.server.LocalServices;
import com.android.server.job.controllers.JobStatus;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.time.Clock;
import java.time.Duration;
import java.time.ZoneOffset;
public class JobSchedulerServiceTest {
private JobSchedulerService mService;
private MockitoSession mMockingSession;
@Mock
private ActivityManagerInternal mActivityMangerInternal;
@Mock
private Context mContext;
private class TestJobSchedulerService extends JobSchedulerService {
TestJobSchedulerService(Context context) {
super(context);
}
@Override
public boolean isChainedAttributionEnabled() {
return false;
}
}
@Before
public void setUp() {
mMockingSession = mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
.mockStatic(LocalServices.class)
.startMocking();
// Called in JobSchedulerService constructor.
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
doReturn(mActivityMangerInternal)
.when(() -> LocalServices.getService(ActivityManagerInternal.class));
doReturn(mock(UsageStatsManagerInternal.class))
.when(() -> LocalServices.getService(UsageStatsManagerInternal.class));
// Called in BackgroundJobsController constructor.
doReturn(mock(AppStateTracker.class))
.when(() -> LocalServices.getService(AppStateTracker.class));
// Called in BatteryController constructor.
doReturn(mock(BatteryManagerInternal.class))
.when(() -> LocalServices.getService(BatteryManagerInternal.class));
// Called in ConnectivityController constructor.
when(mContext.getSystemService(ConnectivityManager.class))
.thenReturn(mock(ConnectivityManager.class));
when(mContext.getSystemService(NetworkPolicyManager.class))
.thenReturn(mock(NetworkPolicyManager.class));
// Called in DeviceIdleJobsController constructor.
doReturn(mock(DeviceIdleController.LocalService.class))
.when(() -> LocalServices.getService(DeviceIdleController.LocalService.class));
// Used in JobStatus.
doReturn(mock(PackageManagerInternal.class))
.when(() -> LocalServices.getService(PackageManagerInternal.class));
// Called via IdleController constructor.
when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
when(mContext.getResources()).thenReturn(mock(Resources.class));
// Called in QuotaController constructor.
IActivityManager activityManager = ActivityManager.getService();
spyOn(activityManager);
try {
doNothing().when(activityManager).registerUidObserver(any(), anyInt(), anyInt(), any());
} catch (RemoteException e) {
fail("registerUidObserver threw exception: " + e.getMessage());
}
JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
mService = new TestJobSchedulerService(mContext);
}
@After
public void tearDown() {
if (mMockingSession != null) {
mMockingSession.finishMocking();
}
}
private Clock getAdvancedClock(Clock clock, long incrementMs) {
return Clock.offset(clock, Duration.ofMillis(incrementMs));
}
private void advanceElapsedClock(long incrementMs) {
JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
JobSchedulerService.sElapsedRealtimeClock, incrementMs);
}
private static JobInfo.Builder createJobInfo() {
return new JobInfo.Builder(351, new ComponentName("foo", "bar"));
}
private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder) {
return JobStatus.createFromJobInfo(
jobInfoBuilder.build(), 1234, "com.android.test", 0, testTag);
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is completed and
* rescheduled while run in its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_insideWindow() {
final long now = sElapsedRealtimeClock.millis();
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
final long nextWindowStartTime = now + HOUR_IN_MILLIS;
final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(45 * MINUTE_IN_MILLIS); // now + 55 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 59 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job with a custom flex
* setting is completed and rescheduled while run in its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_insideWindow_flex() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_flex",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
// First window starts 30 minutes from now.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
final long now = sElapsedRealtimeClock.millis();
final long nextWindowStartTime = now + HOUR_IN_MILLIS;
final long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(15 * MINUTE_IN_MILLIS); // now + 25 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 29 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job failed but then ran
* successfully and was rescheduled while run in its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_insideWindow_failedJob() {
final long now = sElapsedRealtimeClock.millis();
final long nextWindowStartTime = now + HOUR_IN_MILLIS;
final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_failedJob",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 5 minutes
failedJob = mService.getRescheduleJobForFailureLocked(job);
advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 10 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(35 * MINUTE_IN_MILLIS); // now + 45 minutes
failedJob = mService.getRescheduleJobForFailureLocked(job);
advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes
failedJob = mService.getRescheduleJobForFailureLocked(job);
advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is completed and
* rescheduled when run after its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job with a custom flex
* setting is completed and rescheduled when run after its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow_flex() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_flex",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
// First window starts 30 minutes from now.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
advanceElapsedClock(31 * MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// 5 minutes before the start of the next window. It's too close to the next window, so the
// returned job should be for the window after.
advanceElapsedClock(24 * MINUTE_IN_MILLIS);
nextWindowStartTime += HOUR_IN_MILLIS;
nextWindowEndTime += HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS + 10 * MINUTE_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job failed but then ran
* successfully and was rescheduled when run after its expected running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow_failedJob() {
JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_failedJob",
createJobInfo().setPeriodic(HOUR_IN_MILLIS));
JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job with a custom flex
* setting failed but then ran successfully and was rescheduled when run after its expected
* running window.
*/
@Test
public void testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob() {
JobStatus job = createJobStatus(
"testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob",
createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
// First window starts 30 minutes from now.
advanceElapsedClock(30 * MINUTE_IN_MILLIS);
long now = sElapsedRealtimeClock.millis();
long nextWindowStartTime = now + HOUR_IN_MILLIS;
long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
advanceElapsedClock(31 * MINUTE_IN_MILLIS);
// Say the job ran at the very end of its previous window. The intended JSS behavior is to
// have consistent windows, so the new window should start as soon as the previous window
// ended and end PERIOD time after the previous window ended.
JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
// 5 minutes before the start of the next window. It's too close to the next window, so the
// returned job should be for the window after.
advanceElapsedClock(24 * MINUTE_IN_MILLIS);
nextWindowStartTime += HOUR_IN_MILLIS;
nextWindowEndTime += HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(job);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
advanceElapsedClock(2 * HOUR_IN_MILLIS);
// Say that the job ran at this point, possibly due to device idle.
// The next window should be consistent (start and end at the time it would have had the job
// run normally in previous windows).
nextWindowStartTime += 2 * HOUR_IN_MILLIS;
nextWindowEndTime += 2 * HOUR_IN_MILLIS;
rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
}