Merge "Fix JobScheduler race condition" into lmp-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
85b1df187f
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 java.util.List;
|
||||
|
||||
import com.android.server.job.controllers.JobStatus;
|
||||
|
||||
/**
|
||||
* Callback definition for I/O thread to let the JobManagerService know when
|
||||
* I/O read has completed. Done this way so we don't stall the main thread on
|
||||
* boot.
|
||||
*/
|
||||
public interface JobMapReadFinishedListener {
|
||||
|
||||
/**
|
||||
* Called by the {@link JobStore} at boot, when the disk read is finished.
|
||||
*/
|
||||
public void onJobMapReadFinished(List<JobStatus> jobs);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ import com.android.server.job.controllers.TimeController;
|
||||
* @hide
|
||||
*/
|
||||
public class JobSchedulerService extends com.android.server.SystemService
|
||||
implements StateChangedListener, JobCompletedListener, JobMapReadFinishedListener {
|
||||
implements StateChangedListener, JobCompletedListener {
|
||||
// TODO: Switch this off for final version.
|
||||
static final boolean DEBUG = true;
|
||||
/** The number of concurrent jobs we run at one time. */
|
||||
@@ -487,28 +487,6 @@ public class JobSchedulerService extends com.android.server.SystemService
|
||||
mHandler.obtainMessage(MSG_JOB_EXPIRED, jobStatus).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disk I/O is finished, take the list of jobs we read from disk and add them to our
|
||||
* {@link JobStore}.
|
||||
* This is run on the {@link com.android.server.IoThread} instance, which is a separate thread,
|
||||
* and is called once at boot.
|
||||
*/
|
||||
@Override
|
||||
public void onJobMapReadFinished(List<JobStatus> jobs) {
|
||||
synchronized (mJobs) {
|
||||
for (int i=0; i<jobs.size(); i++) {
|
||||
JobStatus js = jobs.get(i);
|
||||
if (mJobs.containsJobIdForUid(js.getJobId(), js.getUid())) {
|
||||
// An app with BOOT_COMPLETED *might* have decided to reschedule their job, in
|
||||
// the same amount of time it took us to read it from disk. If this is the case
|
||||
// we leave it be.
|
||||
continue;
|
||||
}
|
||||
startTrackingJob(js);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class JobHandler extends Handler {
|
||||
|
||||
public JobHandler(Looper looper) {
|
||||
|
||||
@@ -88,19 +88,26 @@ public class JobStore {
|
||||
synchronized (sSingletonLock) {
|
||||
if (sSingleton == null) {
|
||||
sSingleton = new JobStore(jobManagerService.getContext(),
|
||||
Environment.getDataDirectory(), jobManagerService);
|
||||
Environment.getDataDirectory());
|
||||
}
|
||||
return sSingleton;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A freshly initialized job store object, with no loaded jobs.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static JobStore initAndGetForTesting(Context context, File dataDir,
|
||||
JobMapReadFinishedListener callback) {
|
||||
return new JobStore(context, dataDir, callback);
|
||||
public static JobStore initAndGetForTesting(Context context, File dataDir) {
|
||||
JobStore jobStoreUnderTest = new JobStore(context, dataDir);
|
||||
jobStoreUnderTest.clear();
|
||||
return jobStoreUnderTest;
|
||||
}
|
||||
|
||||
private JobStore(Context context, File dataDir, JobMapReadFinishedListener callback) {
|
||||
/**
|
||||
* Construct the instance of the job store. This results in a blocking read from disk.
|
||||
*/
|
||||
private JobStore(Context context, File dataDir) {
|
||||
mContext = context;
|
||||
mDirtyOperations = 0;
|
||||
|
||||
@@ -111,7 +118,7 @@ public class JobStore {
|
||||
|
||||
mJobSet = new ArraySet<JobStatus>();
|
||||
|
||||
readJobMapFromDiskAsync(callback);
|
||||
readJobMapFromDisk(mJobSet);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,12 +256,9 @@ public class JobStore {
|
||||
}
|
||||
}
|
||||
|
||||
private void readJobMapFromDiskAsync(JobMapReadFinishedListener callback) {
|
||||
mIoHandler.post(new ReadJobMapFromDiskRunnable(callback));
|
||||
}
|
||||
|
||||
public void readJobMapFromDisk(JobMapReadFinishedListener callback) {
|
||||
new ReadJobMapFromDiskRunnable(callback).run();
|
||||
@VisibleForTesting
|
||||
public void readJobMapFromDisk(ArraySet<JobStatus> jobSet) {
|
||||
new ReadJobMapFromDiskRunnable(jobSet).run();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,13 +402,18 @@ public class JobStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable that reads list of persisted job from xml.
|
||||
* NOTE: This Runnable locks on JobStore.this
|
||||
* Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
|
||||
* need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
|
||||
*/
|
||||
private class ReadJobMapFromDiskRunnable implements Runnable {
|
||||
private JobMapReadFinishedListener mCallback;
|
||||
public ReadJobMapFromDiskRunnable(JobMapReadFinishedListener callback) {
|
||||
mCallback = callback;
|
||||
private final ArraySet<JobStatus> jobSet;
|
||||
|
||||
/**
|
||||
* @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
|
||||
* so that after disk read we can populate it directly.
|
||||
*/
|
||||
ReadJobMapFromDiskRunnable(ArraySet<JobStatus> jobSet) {
|
||||
this.jobSet = jobSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -414,11 +423,13 @@ public class JobStore {
|
||||
FileInputStream fis = mJobsFile.openRead();
|
||||
synchronized (JobStore.this) {
|
||||
jobs = readJobMapImpl(fis);
|
||||
if (jobs != null) {
|
||||
for (int i=0; i<jobs.size(); i++) {
|
||||
this.jobSet.add(jobs.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
fis.close();
|
||||
if (jobs != null) {
|
||||
mCallback.onJobMapReadFinished(jobs);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
if (JobSchedulerService.DEBUG) {
|
||||
Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
|
||||
@@ -673,4 +684,4 @@ public class JobStore {
|
||||
return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.android.server.task;
|
||||
package com.android.server.job;
|
||||
|
||||
|
||||
import android.content.ComponentName;
|
||||
@@ -9,40 +9,32 @@ import android.os.PersistableBundle;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.RenamingDelegatingContext;
|
||||
import android.util.Log;
|
||||
import android.util.ArraySet;
|
||||
|
||||
import com.android.server.job.JobMapReadFinishedListener;
|
||||
import com.android.server.job.JobStore;
|
||||
import com.android.server.job.controllers.JobStatus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Test reading and writing correctly from file.
|
||||
*/
|
||||
public class TaskStoreTest extends AndroidTestCase {
|
||||
public class JobStoreTest extends AndroidTestCase {
|
||||
private static final String TAG = "TaskStoreTest";
|
||||
private static final String TEST_PREFIX = "_test_";
|
||||
// private static final int USER_NON_0 = 3;
|
||||
|
||||
private static final int SOME_UID = 34234;
|
||||
private ComponentName mComponent;
|
||||
private static final long IO_WAIT = 600L;
|
||||
private static final long IO_WAIT = 1000L;
|
||||
|
||||
JobStore mTaskStoreUnderTest;
|
||||
Context mTestContext;
|
||||
JobMapReadFinishedListener mTaskMapReadFinishedListenerStub =
|
||||
new JobMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onJobMapReadFinished(List<JobStatus> tasks) {
|
||||
// do nothing.
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
mTestContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
|
||||
Log.d(TAG, "Saving tasks to '" + mTestContext.getFilesDir() + "'");
|
||||
mTaskStoreUnderTest = JobStore.initAndGetForTesting(mTestContext,
|
||||
mTestContext.getFilesDir(), mTaskMapReadFinishedListenerStub);
|
||||
mTaskStoreUnderTest =
|
||||
JobStore.initAndGetForTesting(mTestContext, mTestContext.getFilesDir());
|
||||
mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName());
|
||||
}
|
||||
|
||||
@@ -69,19 +61,17 @@ public class TaskStoreTest extends AndroidTestCase {
|
||||
mTaskStoreUnderTest.add(ts);
|
||||
Thread.sleep(IO_WAIT);
|
||||
// Manually load tasks from xml file.
|
||||
mTaskStoreUnderTest.readJobMapFromDisk(new JobMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onJobMapReadFinished(List<JobStatus> tasks) {
|
||||
assertEquals("Didn't get expected number of persisted tasks.", 1, tasks.size());
|
||||
JobStatus loadedTaskStatus = tasks.get(0);
|
||||
assertTasksEqual(task, loadedTaskStatus.getJob());
|
||||
assertEquals("Different uids.", SOME_UID, tasks.get(0).getUid());
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed());
|
||||
}
|
||||
});
|
||||
final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
|
||||
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
|
||||
|
||||
assertEquals("Didn't get expected number of persisted tasks.", 1, jobStatusSet.size());
|
||||
final JobStatus loadedTaskStatus = jobStatusSet.iterator().next();
|
||||
assertTasksEqual(task, loadedTaskStatus.getJob());
|
||||
assertEquals("Different uids.", SOME_UID, loadedTaskStatus.getUid());
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed());
|
||||
|
||||
}
|
||||
|
||||
@@ -104,26 +94,25 @@ public class TaskStoreTest extends AndroidTestCase {
|
||||
mTaskStoreUnderTest.add(taskStatus1);
|
||||
mTaskStoreUnderTest.add(taskStatus2);
|
||||
Thread.sleep(IO_WAIT);
|
||||
mTaskStoreUnderTest.readJobMapFromDisk(new JobMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onJobMapReadFinished(List<JobStatus> tasks) {
|
||||
assertEquals("Incorrect # of persisted tasks.", 2, tasks.size());
|
||||
JobStatus loaded1 = tasks.get(0);
|
||||
JobStatus loaded2 = tasks.get(1);
|
||||
assertTasksEqual(task1, loaded1.getJob());
|
||||
assertTasksEqual(task2, loaded2.getJob());
|
||||
|
||||
// Check that the loaded task has the correct runtimes.
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed());
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed());
|
||||
}
|
||||
});
|
||||
final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
|
||||
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
|
||||
assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size());
|
||||
Iterator<JobStatus> it = jobStatusSet.iterator();
|
||||
JobStatus loaded1 = it.next();
|
||||
JobStatus loaded2 = it.next();
|
||||
assertTasksEqual(task1, loaded1.getJob());
|
||||
assertTasksEqual(task2, loaded2.getJob());
|
||||
|
||||
// Check that the loaded task has the correct runtimes.
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed());
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed());
|
||||
|
||||
}
|
||||
|
||||
@@ -144,15 +133,12 @@ public class TaskStoreTest extends AndroidTestCase {
|
||||
|
||||
mTaskStoreUnderTest.add(taskStatus);
|
||||
Thread.sleep(IO_WAIT);
|
||||
mTaskStoreUnderTest.readJobMapFromDisk(new JobMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onJobMapReadFinished(List<JobStatus> tasks) {
|
||||
assertEquals("Incorrect # of persisted tasks.", 1, tasks.size());
|
||||
JobStatus loaded = tasks.get(0);
|
||||
assertTasksEqual(task, loaded.getJob());
|
||||
}
|
||||
});
|
||||
|
||||
final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
|
||||
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
|
||||
assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
|
||||
JobStatus loaded = jobStatusSet.iterator().next();
|
||||
assertTasksEqual(task, loaded.getJob());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,4 +187,4 @@ public class TaskStoreTest extends AndroidTestCase {
|
||||
|
||||
private static class StubClass {}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user