Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 46f31feb authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Limit the number of persistable JobWorkItems." into udc-dev

parents 182d1ac5 f035fca7
Loading
Loading
Loading
Loading
+61 −2
Original line number Diff line number Diff line
@@ -429,6 +429,7 @@ public class JobSchedulerService extends com.android.server.SystemService
        public void onPropertiesChanged(DeviceConfig.Properties properties) {
            boolean apiQuotaScheduleUpdated = false;
            boolean concurrencyUpdated = false;
            boolean persistenceUpdated = false;
            boolean runtimeUpdated = false;
            for (int controller = 0; controller < mControllers.size(); controller++) {
                final StateController sc = mControllers.get(controller);
@@ -488,9 +489,13 @@ public class JobSchedulerService extends com.android.server.SystemService
                                runtimeUpdated = true;
                            }
                            break;
                        case Constants.KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS:
                        case Constants.KEY_PERSIST_IN_SPLIT_FILES:
                            if (!persistenceUpdated) {
                                mConstants.updatePersistingConstantsLocked();
                                mJobs.setUseSplitFiles(mConstants.PERSIST_IN_SPLIT_FILES);
                                persistenceUpdated = true;
                            }
                            break;
                        default:
                            if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY)
@@ -583,6 +588,9 @@ public class JobSchedulerService extends com.android.server.SystemService

        private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files";

        private static final String KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS =
                "max_num_persisted_job_work_items";

        private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
        private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
        private static final float DEFAULT_HEAVY_USE_FACTOR = .9f;
@@ -618,6 +626,7 @@ public class JobSchedulerService extends com.android.server.SystemService
        public static final long DEFAULT_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS =
                Math.min(Long.MAX_VALUE, DEFAULT_RUNTIME_UI_LIMIT_MS);
        static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true;
        static final int DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS = 100_000;

        /**
         * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early.
@@ -760,6 +769,11 @@ public class JobSchedulerService extends com.android.server.SystemService
         */
        public boolean PERSIST_IN_SPLIT_FILES = DEFAULT_PERSIST_IN_SPLIT_FILES;

        /**
         * The maximum number of {@link JobWorkItem JobWorkItems} that can be persisted per job.
         */
        public int MAX_NUM_PERSISTED_JOB_WORK_ITEMS = DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS;

        /**
         * If true, use TARE policy for job limiting. If false, use quotas.
         */
@@ -822,6 +836,10 @@ public class JobSchedulerService extends com.android.server.SystemService
        private void updatePersistingConstantsLocked() {
            PERSIST_IN_SPLIT_FILES = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
                    KEY_PERSIST_IN_SPLIT_FILES, DEFAULT_PERSIST_IN_SPLIT_FILES);
            MAX_NUM_PERSISTED_JOB_WORK_ITEMS = DeviceConfig.getInt(
                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
                    KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS,
                    DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS);
        }

        private void updatePrefetchConstantsLocked() {
@@ -961,6 +979,8 @@ public class JobSchedulerService extends com.android.server.SystemService
                    RUNTIME_UI_DATA_TRANSFER_LIMIT_MS).println();

            pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println();
            pw.print(KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS, MAX_NUM_PERSISTED_JOB_WORK_ITEMS)
                    .println();

            pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY).println();

@@ -1344,6 +1364,25 @@ public class JobSchedulerService extends com.android.server.SystemService
                // Fast path: we are adding work to an existing job, and the JobInfo is not
                // changing.  We can just directly enqueue this work in to the job.
                if (toCancel.getJob().equals(job)) {
                    // On T and below, JobWorkItem count was unlimited but they could not be
                    // persisted. Now in U and above, we allow persisting them. In both cases,
                    // there is a danger of apps adding too many JobWorkItems and causing the
                    // system to OOM since we keep everything in memory. The persisting danger
                    // is greater because it could technically lead to a boot loop if the system
                    // keeps trying to load all the JobWorkItems that led to the initial OOM.
                    // Therefore, for now (partly for app compatibility), we tackle the latter
                    // and limit the number of JobWorkItems that can be persisted.
                    // Moving forward, we should look into two things:
                    //   1. Limiting the number of unpersisted JobWorkItems
                    //   2. Offloading some state to disk so we don't keep everything in memory
                    // TODO(273758274): improve JobScheduler's resilience and memory management
                    if (toCancel.getWorkCount() >= mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS
                            && toCancel.isPersisted()) {
                        Slog.w(TAG, "Too many JWIs for uid " + uId);
                        throw new IllegalStateException("Apps may not persist more than "
                                + mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS
                                + " JobWorkItems per job");
                    }

                    toCancel.enqueueWorkLocked(work);
                    mJobs.touchJob(toCancel);
@@ -1388,6 +1427,26 @@ public class JobSchedulerService extends com.android.server.SystemService
            jobStatus.prepareLocked();

            if (toCancel != null) {
                // On T and below, JobWorkItem count was unlimited but they could not be
                // persisted. Now in U and above, we allow persisting them. In both cases,
                // there is a danger of apps adding too many JobWorkItems and causing the
                // system to OOM since we keep everything in memory. The persisting danger
                // is greater because it could technically lead to a boot loop if the system
                // keeps trying to load all the JobWorkItems that led to the initial OOM.
                // Therefore, for now (partly for app compatibility), we tackle the latter
                // and limit the number of JobWorkItems that can be persisted.
                // Moving forward, we should look into two things:
                //   1. Limiting the number of unpersisted JobWorkItems
                //   2. Offloading some state to disk so we don't keep everything in memory
                // TODO(273758274): improve JobScheduler's resilience and memory management
                if (work != null && toCancel.isPersisted()
                        && toCancel.getWorkCount() >= mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS) {
                    Slog.w(TAG, "Too many JWIs for uid " + uId);
                    throw new IllegalStateException("Apps may not persist more than "
                            + mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS
                            + " JobWorkItems per job");
                }

                // Implicitly replaces the existing job record with the new instance
                cancelJobImplLocked(toCancel, jobStatus, JobParameters.STOP_REASON_CANCELLED_BY_APP,
                        JobParameters.INTERNAL_STOP_REASON_CANCELED, "job rescheduled by app");
+7 −0
Original line number Diff line number Diff line
@@ -814,6 +814,13 @@ public final class JobStatus {
        return null;
    }

    /** Returns the number of {@link JobWorkItem JobWorkItems} attached to this job. */
    public int getWorkCount() {
        final int pendingCount = pendingWork == null ? 0 : pendingWork.size();
        final int executingCount = executingWork == null ? 0 : executingWork.size();
        return pendingCount + executingCount;
    }

    public boolean hasWorkLocked() {
        return (pendingWork != null && pendingWork.size() > 0) || hasExecutingWorkLocked();
    }
+61 −4
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import android.app.UiModeManager;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobWorkItem;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
@@ -91,6 +92,7 @@ import java.time.ZoneOffset;

public class JobSchedulerServiceTest {
    private static final String TAG = JobSchedulerServiceTest.class.getSimpleName();
    private static final int TEST_UID = 10123;

    private JobSchedulerService mService;

@@ -177,6 +179,9 @@ public class JobSchedulerServiceTest {
        if (mMockingSession != null) {
            mMockingSession.finishMocking();
        }
        mService.cancelJobsForUid(TEST_UID, true,
                JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_UNKNOWN,
                "test cleanup");
    }

    private Clock getAdvancedClock(Clock clock, long incrementMs) {
@@ -1170,7 +1175,7 @@ public class JobSchedulerServiceTest {
                    i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE;
            assertEquals("Got unexpected result for schedule #" + (i + 1),
                    expected,
                    mService.scheduleAsPackage(job, null, 10123, null, 0, "JSSTest", ""));
                    mService.scheduleAsPackage(job, null, TEST_UID, null, 0, "JSSTest", ""));
        }
    }

@@ -1191,7 +1196,7 @@ public class JobSchedulerServiceTest {
        for (int i = 0; i < 500; ++i) {
            assertEquals("Got unexpected result for schedule #" + (i + 1),
                    JobScheduler.RESULT_SUCCESS,
                    mService.scheduleAsPackage(job, null, 10123, null, 0, "JSSTest", ""));
                    mService.scheduleAsPackage(job, null, TEST_UID, null, 0, "JSSTest", ""));
        }
    }

@@ -1212,7 +1217,7 @@ public class JobSchedulerServiceTest {
        for (int i = 0; i < 500; ++i) {
            assertEquals("Got unexpected result for schedule #" + (i + 1),
                    JobScheduler.RESULT_SUCCESS,
                    mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, "JSSTest",
                    mService.scheduleAsPackage(job, null, TEST_UID, "proxied.package", 0, "JSSTest",
                            ""));
        }
    }
@@ -1236,11 +1241,63 @@ public class JobSchedulerServiceTest {
                    i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE;
            assertEquals("Got unexpected result for schedule #" + (i + 1),
                    expected,
                    mService.scheduleAsPackage(job, null, 10123, job.getService().getPackageName(),
                    mService.scheduleAsPackage(job, null, TEST_UID,
                            job.getService().getPackageName(),
                            0, "JSSTest", ""));
        }
    }

    /**
     * Tests that the number of persisted JobWorkItems is capped.
     */
    @Test
    public void testScheduleLimiting_JobWorkItems_Nonpersisted() {
        mService.mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS = 500;
        mService.mConstants.ENABLE_API_QUOTAS = false;
        mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
        mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false;
        mService.updateQuotaTracker();

        final JobInfo job = createJobInfo().setPersisted(false).build();
        final JobWorkItem item = new JobWorkItem.Builder().build();
        for (int i = 0; i < 1000; ++i) {
            assertEquals("Got unexpected result for schedule #" + (i + 1),
                    JobScheduler.RESULT_SUCCESS,
                    mService.scheduleAsPackage(job, item, TEST_UID,
                            job.getService().getPackageName(),
                            0, "JSSTest", ""));
        }
    }

    /**
     * Tests that the number of persisted JobWorkItems is capped.
     */
    @Test
    public void testScheduleLimiting_JobWorkItems_Persisted() {
        mService.mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS = 500;
        mService.mConstants.ENABLE_API_QUOTAS = false;
        mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
        mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false;
        mService.updateQuotaTracker();

        final JobInfo job = createJobInfo().setPersisted(true).build();
        final JobWorkItem item = new JobWorkItem.Builder().build();
        for (int i = 0; i < 500; ++i) {
            assertEquals("Got unexpected result for schedule #" + (i + 1),
                    JobScheduler.RESULT_SUCCESS,
                    mService.scheduleAsPackage(job, item, TEST_UID,
                            job.getService().getPackageName(),
                            0, "JSSTest", ""));
        }
        try {
            mService.scheduleAsPackage(job, item, TEST_UID, job.getService().getPackageName(),
                    0, "JSSTest", "");
            fail("Added more items than allowed");
        } catch (IllegalStateException expected) {
            // Success
        }
    }

    /** Tests that jobs are removed from the pending list if the user stops the app. */
    @Test
    public void testUserStopRemovesPending() {