Loading services/core/java/com/android/server/job/JobStore.java +58 −42 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.BitUtils; Loading Loading @@ -85,9 +86,10 @@ public final class JobStore { private static final boolean DEBUG = JobSchedulerService.DEBUG; /** Threshold to adjust how often we want to write to the db. */ private static final int MAX_OPS_BEFORE_WRITE = 1; private static final long JOB_PERSIST_DELAY = 2000L; final Object mLock; final Object mWriteScheduleLock; // used solely for invariants around write scheduling final JobSet mJobSet; // per-caller-uid and per-source-uid tracking final Context mContext; Loading @@ -95,7 +97,11 @@ public final class JobStore { private final long mXmlTimestamp; private boolean mRtcGood; private int mDirtyOperations; @GuardedBy("mWriteScheduleLock") private boolean mWriteScheduled; @GuardedBy("mWriteScheduleLock") private boolean mWriteInProgress; private static final Object sSingletonLock = new Object(); private final AtomicFile mJobsFile; Loading Loading @@ -131,8 +137,8 @@ public final class JobStore { */ private JobStore(Context context, Object lock, File dataDir) { mLock = lock; mWriteScheduleLock = new Object(); mContext = context; mDirtyOperations = 0; File systemDir = new File(dataDir, "system"); File jobDir = new File(systemDir, "job"); Loading Loading @@ -322,13 +328,14 @@ public final class JobStore { * track incremental changes. */ private void maybeWriteStatusToDiskAsync() { mDirtyOperations++; if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) { synchronized (mWriteScheduleLock) { if (!mWriteScheduled) { if (DEBUG) { Slog.v(TAG, "Writing jobs to disk."); Slog.v(TAG, "Scheduling persist of jobs to disk."); } mIoHandler.postDelayed(mWriteRunnable, JOB_PERSIST_DELAY); mWriteScheduled = mWriteInProgress = true; } mIoHandler.removeCallbacks(mWriteRunnable); mIoHandler.post(mWriteRunnable); } } Loading @@ -337,6 +344,34 @@ public final class JobStore { new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run(); } /** * Wait for any pending write to the persistent store to clear * @param maxWaitMillis Maximum time from present to wait * @return {@code true} if I/O cleared as expected, {@code false} if the wait * timed out before the pending write completed. */ @VisibleForTesting public boolean waitForWriteToCompleteForTesting(long maxWaitMillis) { final long start = SystemClock.uptimeMillis(); final long end = start + maxWaitMillis; synchronized (mWriteScheduleLock) { while (mWriteInProgress) { final long now = SystemClock.uptimeMillis(); if (now >= end) { // still not done and we've hit the end; failure return false; } try { mWriteScheduleLock.wait(now - start + maxWaitMillis); } catch (InterruptedException e) { // Spurious; keep waiting break; } } } return true; } /** * Runnable that writes {@link #mJobSet} out to xml. * NOTE: This Runnable locks on mLock Loading @@ -346,6 +381,16 @@ public final class JobStore { public void run() { final long startElapsed = sElapsedRealtimeClock.millis(); final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); // Intentionally allow new scheduling of a write operation *before* we clone // the job set. If we reset it to false after cloning, there's a window in // which no new write will be scheduled but mLock is not held, i.e. a new // job might appear and fail to be recognized as needing a persist. The // potential cost is one redundant write of an identical set of jobs in the // rare case of that specific race, but by doing it this way we avoid quite // a bit of lock contention. synchronized (mWriteScheduleLock) { mWriteScheduled = false; } synchronized (mLock) { // Clone the jobs so we can release the lock before writing. mJobSet.forEachJob(null, (job) -> { Loading @@ -359,6 +404,10 @@ public final class JobStore { Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis() - startElapsed) + "ms"); } synchronized (mWriteScheduleLock) { mWriteInProgress = false; mWriteScheduleLock.notifyAll(); } } private void writeJobsMapImpl(List<JobStatus> jobList) { Loading Loading @@ -402,7 +451,6 @@ public final class JobStore { FileOutputStream fos = mJobsFile.startWrite(startTime); fos.write(baos.toByteArray()); mJobsFile.finishWrite(fos); mDirtyOperations = 0; } catch (IOException e) { if (DEBUG) { Slog.v(TAG, "Error writing out job data.", e); Loading Loading @@ -979,38 +1027,6 @@ public final class JobStore { : JobStatus.NO_LATEST_RUNTIME; return Pair.create(earliestRunTimeRtc, latestRunTimeRtc); } /** * Convenience function to read out and convert deadline and delay from xml into elapsed real * time. * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime * and the second is the latest elapsed runtime. */ private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser) throws NumberFormatException { // Pull out execution time data. final long nowWallclock = sSystemClock.millis(); final long nowElapsed = sElapsedRealtimeClock.millis(); long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME; long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME; String val = parser.getAttributeValue(null, "deadline"); if (val != null) { long latestRuntimeWallclock = Long.parseLong(val); long maxDelayElapsed = Math.max(latestRuntimeWallclock - nowWallclock, 0); latestRunTimeElapsed = nowElapsed + maxDelayElapsed; } val = parser.getAttributeValue(null, "delay"); if (val != null) { long earliestRuntimeWallclock = Long.parseLong(val); long minDelayElapsed = Math.max(earliestRuntimeWallclock - nowWallclock, 0); earliestRunTimeElapsed = nowElapsed + minDelayElapsed; } return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); } } static final class JobSet { Loading services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +3 −8 Original line number Diff line number Diff line Loading @@ -31,7 +31,6 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.HexDump; import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.job.JobStore.JobSet; import com.android.server.job.controllers.JobStatus; Loading @@ -45,8 +44,6 @@ import java.time.Clock; import java.time.ZoneOffset; import java.util.Arrays; import java.util.Iterator; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Test reading and writing correctly from file. Loading Loading @@ -96,14 +93,12 @@ public class JobStoreTest { @After public void tearDown() throws Exception { mTaskStoreUnderTest.clear(); mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L); } private void waitForPendingIo() throws Exception { final CountDownLatch latch = new CountDownLatch(1); IoThread.getHandler().post(() -> { latch.countDown(); }); latch.await(10, TimeUnit.SECONDS); assertTrue("Timed out waiting for persistence I/O to complete", mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L)); } @Test Loading Loading
services/core/java/com/android/server/job/JobStore.java +58 −42 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.BitUtils; Loading Loading @@ -85,9 +86,10 @@ public final class JobStore { private static final boolean DEBUG = JobSchedulerService.DEBUG; /** Threshold to adjust how often we want to write to the db. */ private static final int MAX_OPS_BEFORE_WRITE = 1; private static final long JOB_PERSIST_DELAY = 2000L; final Object mLock; final Object mWriteScheduleLock; // used solely for invariants around write scheduling final JobSet mJobSet; // per-caller-uid and per-source-uid tracking final Context mContext; Loading @@ -95,7 +97,11 @@ public final class JobStore { private final long mXmlTimestamp; private boolean mRtcGood; private int mDirtyOperations; @GuardedBy("mWriteScheduleLock") private boolean mWriteScheduled; @GuardedBy("mWriteScheduleLock") private boolean mWriteInProgress; private static final Object sSingletonLock = new Object(); private final AtomicFile mJobsFile; Loading Loading @@ -131,8 +137,8 @@ public final class JobStore { */ private JobStore(Context context, Object lock, File dataDir) { mLock = lock; mWriteScheduleLock = new Object(); mContext = context; mDirtyOperations = 0; File systemDir = new File(dataDir, "system"); File jobDir = new File(systemDir, "job"); Loading Loading @@ -322,13 +328,14 @@ public final class JobStore { * track incremental changes. */ private void maybeWriteStatusToDiskAsync() { mDirtyOperations++; if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) { synchronized (mWriteScheduleLock) { if (!mWriteScheduled) { if (DEBUG) { Slog.v(TAG, "Writing jobs to disk."); Slog.v(TAG, "Scheduling persist of jobs to disk."); } mIoHandler.postDelayed(mWriteRunnable, JOB_PERSIST_DELAY); mWriteScheduled = mWriteInProgress = true; } mIoHandler.removeCallbacks(mWriteRunnable); mIoHandler.post(mWriteRunnable); } } Loading @@ -337,6 +344,34 @@ public final class JobStore { new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run(); } /** * Wait for any pending write to the persistent store to clear * @param maxWaitMillis Maximum time from present to wait * @return {@code true} if I/O cleared as expected, {@code false} if the wait * timed out before the pending write completed. */ @VisibleForTesting public boolean waitForWriteToCompleteForTesting(long maxWaitMillis) { final long start = SystemClock.uptimeMillis(); final long end = start + maxWaitMillis; synchronized (mWriteScheduleLock) { while (mWriteInProgress) { final long now = SystemClock.uptimeMillis(); if (now >= end) { // still not done and we've hit the end; failure return false; } try { mWriteScheduleLock.wait(now - start + maxWaitMillis); } catch (InterruptedException e) { // Spurious; keep waiting break; } } } return true; } /** * Runnable that writes {@link #mJobSet} out to xml. * NOTE: This Runnable locks on mLock Loading @@ -346,6 +381,16 @@ public final class JobStore { public void run() { final long startElapsed = sElapsedRealtimeClock.millis(); final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); // Intentionally allow new scheduling of a write operation *before* we clone // the job set. If we reset it to false after cloning, there's a window in // which no new write will be scheduled but mLock is not held, i.e. a new // job might appear and fail to be recognized as needing a persist. The // potential cost is one redundant write of an identical set of jobs in the // rare case of that specific race, but by doing it this way we avoid quite // a bit of lock contention. synchronized (mWriteScheduleLock) { mWriteScheduled = false; } synchronized (mLock) { // Clone the jobs so we can release the lock before writing. mJobSet.forEachJob(null, (job) -> { Loading @@ -359,6 +404,10 @@ public final class JobStore { Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis() - startElapsed) + "ms"); } synchronized (mWriteScheduleLock) { mWriteInProgress = false; mWriteScheduleLock.notifyAll(); } } private void writeJobsMapImpl(List<JobStatus> jobList) { Loading Loading @@ -402,7 +451,6 @@ public final class JobStore { FileOutputStream fos = mJobsFile.startWrite(startTime); fos.write(baos.toByteArray()); mJobsFile.finishWrite(fos); mDirtyOperations = 0; } catch (IOException e) { if (DEBUG) { Slog.v(TAG, "Error writing out job data.", e); Loading Loading @@ -979,38 +1027,6 @@ public final class JobStore { : JobStatus.NO_LATEST_RUNTIME; return Pair.create(earliestRunTimeRtc, latestRunTimeRtc); } /** * Convenience function to read out and convert deadline and delay from xml into elapsed real * time. * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime * and the second is the latest elapsed runtime. */ private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser) throws NumberFormatException { // Pull out execution time data. final long nowWallclock = sSystemClock.millis(); final long nowElapsed = sElapsedRealtimeClock.millis(); long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME; long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME; String val = parser.getAttributeValue(null, "deadline"); if (val != null) { long latestRuntimeWallclock = Long.parseLong(val); long maxDelayElapsed = Math.max(latestRuntimeWallclock - nowWallclock, 0); latestRunTimeElapsed = nowElapsed + maxDelayElapsed; } val = parser.getAttributeValue(null, "delay"); if (val != null) { long earliestRuntimeWallclock = Long.parseLong(val); long minDelayElapsed = Math.max(earliestRuntimeWallclock - nowWallclock, 0); earliestRunTimeElapsed = nowElapsed + minDelayElapsed; } return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); } } static final class JobSet { Loading
services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +3 −8 Original line number Diff line number Diff line Loading @@ -31,7 +31,6 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.HexDump; import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.job.JobStore.JobSet; import com.android.server.job.controllers.JobStatus; Loading @@ -45,8 +44,6 @@ import java.time.Clock; import java.time.ZoneOffset; import java.util.Arrays; import java.util.Iterator; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Test reading and writing correctly from file. Loading Loading @@ -96,14 +93,12 @@ public class JobStoreTest { @After public void tearDown() throws Exception { mTaskStoreUnderTest.clear(); mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L); } private void waitForPendingIo() throws Exception { final CountDownLatch latch = new CountDownLatch(1); IoThread.getHandler().post(() -> { latch.countDown(); }); latch.await(10, TimeUnit.SECONDS); assertTrue("Timed out waiting for persistence I/O to complete", mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L)); } @Test Loading