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

Commit 56f9ce0e authored by Kweku Adams's avatar Kweku Adams Committed by Android (Google) Code Review
Browse files

Merge "Don't count proxied jobs toward scheduling limit."

parents 1ea50d2c 0a2ccf30
Loading
Loading
Loading
Loading
+18 −6
Original line number Diff line number Diff line
@@ -59,9 +59,9 @@ import java.util.List;
 *
 * <p class="caution"><strong>Note:</strong> Beginning with API 30
 * ({@link android.os.Build.VERSION_CODES#R}), JobScheduler will throttle runaway applications.
 * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency is indicative
 * of an app bug and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin
 * to throttle apps that show buggy behavior, regardless of target SDK version.
 * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency can have a
 * high cost and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin
 * to throttle apps, regardless of target SDK version.
 */
@SystemService(Context.JOB_SCHEDULER_SERVICE)
public abstract class JobScheduler {
@@ -74,9 +74,16 @@ public abstract class JobScheduler {
    public @interface Result {}

    /**
     * Returned from {@link #schedule(JobInfo)} when an invalid parameter was supplied. This can occur
     * if the run-time for your job is too short, or perhaps the system can't resolve the
     * requisite {@link JobService} in your package.
     * Returned from {@link #schedule(JobInfo)} if a job wasn't scheduled successfully. Scheduling
     * can fail for a variety of reasons, including, but not limited to:
     * <ul>
     * <li>an invalid parameter was supplied (eg. the run-time for your job is too short, or the
     * system can't resolve the requisite {@link JobService} in your package)</li>
     * <li>the app has too many jobs scheduled</li>
     * <li>the app has tried to schedule too many jobs in a short amount of time</li>
     * </ul>
     * Attempting to schedule the job again immediately after receiving this result will not
     * guarantee a successful schedule.
     */
    public static final int RESULT_FAILURE = 0;
    /**
@@ -89,6 +96,11 @@ public abstract class JobScheduler {
     * ID with the new information in the {@link JobInfo}.  If a job with the given ID is currently
     * running, it will be stopped.
     *
     * <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's
     * rescheduling the same job and the job didn't execute, especially on platform versions before
     * version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to
     * this API if calls are made too frequently in a short amount of time.
     *
     * @param job The job you wish scheduled. See
     * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs
     * you can schedule.
+57 −13
Original line number Diff line number Diff line
@@ -254,6 +254,18 @@ public class JobSchedulerService extends com.android.server.SystemService

    private final CountQuotaTracker mQuotaTracker;
    private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
    private static final String QUOTA_TRACKER_SCHEDULE_LOGGED =
            ".schedulePersisted out-of-quota logged";
    private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED = new Category(
            ".schedulePersisted()");
    private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED = new Category(
            ".schedulePersisted out-of-quota logged");
    private static final Categorizer QUOTA_CATEGORIZER = (userId, packageName, tag) -> {
        if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) {
            return QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED;
        }
        return QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED;
    };

    /**
     * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
@@ -270,6 +282,7 @@ public class JobSchedulerService extends com.android.server.SystemService
    ActivityManagerInternal mActivityManagerInternal;
    IBatteryStats mBatteryStats;
    DeviceIdleInternal mLocalDeviceIdleController;
    @VisibleForTesting
    AppStateTracker mAppStateTracker;
    final UsageStatsManagerInternal mUsageStats;
    private final AppStandbyInternal mAppStandbyInternal;
@@ -342,10 +355,7 @@ public class JobSchedulerService extends com.android.server.SystemService
                        final StateController sc = mControllers.get(controller);
                        sc.onConstantsUpdatedLocked();
                    }
                    mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS);
                    mQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY,
                            mConstants.API_QUOTA_SCHEDULE_COUNT,
                            mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
                    updateQuotaTracker();
                } catch (IllegalArgumentException e) {
                    // Failed to parse the settings string, log this and move on
                    // with defaults.
@@ -355,6 +365,14 @@ public class JobSchedulerService extends com.android.server.SystemService
        }
    }

    @VisibleForTesting
    void updateQuotaTracker() {
        mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS);
        mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED,
                mConstants.API_QUOTA_SCHEDULE_COUNT,
                mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
    }

    static class MaxJobCounts {
        private final KeyValueListParser.IntValue mTotal;
        private final KeyValueListParser.IntValue mMaxBg;
@@ -507,6 +525,8 @@ public class JobSchedulerService extends com.android.server.SystemService
        private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms";
        private static final String KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION =
                "aq_schedule_throw_exception";
        private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
                "aq_schedule_return_failure";

        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;
@@ -520,6 +540,7 @@ public class JobSchedulerService extends com.android.server.SystemService
        private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250;
        private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS;
        private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true;
        private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false;

        /**
         * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early.
@@ -623,6 +644,11 @@ public class JobSchedulerService extends com.android.server.SystemService
         */
        public boolean API_QUOTA_SCHEDULE_THROW_EXCEPTION =
                DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION;
        /**
         * Whether or not to return a failure result when an app hits its schedule quota limit.
         */
        public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
                DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT;

        private final KeyValueListParser mParser = new KeyValueListParser(',');

@@ -678,6 +704,9 @@ public class JobSchedulerService extends com.android.server.SystemService
            API_QUOTA_SCHEDULE_THROW_EXCEPTION = mParser.getBoolean(
                    KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION,
                    DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION);
            API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = mParser.getBoolean(
                    KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
                    DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT);
        }

        void dump(IndentingPrintWriter pw) {
@@ -712,6 +741,8 @@ public class JobSchedulerService extends com.android.server.SystemService
            pw.print(KEY_API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS).println();
            pw.print(KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION,
                    API_QUOTA_SCHEDULE_THROW_EXCEPTION).println();
            pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
                    API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println();

            pw.decreaseIndent();
        }
@@ -740,6 +771,8 @@ public class JobSchedulerService extends com.android.server.SystemService
            proto.write(ConstantsProto.API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS);
            proto.write(ConstantsProto.API_QUOTA_SCHEDULE_THROW_EXCEPTION,
                    API_QUOTA_SCHEDULE_THROW_EXCEPTION);
            proto.write(ConstantsProto.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
                    API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT);
        }
    }

@@ -973,12 +1006,17 @@ public class JobSchedulerService extends com.android.server.SystemService

    public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
            int userId, String tag) {
        if (job.isPersisted()) {
            // Only limit schedule calls for persisted jobs.
        final String servicePkg = job.getService().getPackageName();
        if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) {
            // Only limit schedule calls for persisted jobs scheduled by the app itself.
            final String pkg =
                    packageName == null ? job.getService().getPackageName() : packageName;
            if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) {
                Slog.e(TAG, userId + "-" + pkg + " has called schedule() too many times");
                if (mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED)) {
                    // Don't log too frequently
                    Slog.wtf(TAG, userId + "-" + pkg + " has called schedule() too many times");
                    mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED);
                }
                mAppStandbyInternal.restrictApp(
                        pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
                if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION) {
@@ -1004,14 +1042,18 @@ public class JobSchedulerService extends com.android.server.SystemService
                        // Only throw the exception for debuggable apps.
                        throw new LimitExceededException(
                                "schedule()/enqueue() called more than "
                                        + mQuotaTracker.getLimit(Category.SINGLE_CATEGORY)
                                        + mQuotaTracker.getLimit(
                                        QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED)
                                        + " times in the past "
                                        + mQuotaTracker.getWindowSizeMs(Category.SINGLE_CATEGORY)
                                        + "ms");
                                        + mQuotaTracker.getWindowSizeMs(
                                        QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED)
                                        + "ms. See the documentation for more information.");
                    }
                }
                if (mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT) {
                    return JobScheduler.RESULT_FAILURE;
                }
            }
            mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG);
        }

@@ -1371,10 +1413,12 @@ public class JobSchedulerService extends com.android.server.SystemService
        // Set up the app standby bucketing tracker
        mStandbyTracker = new StandbyTracker();
        mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
        mQuotaTracker = new CountQuotaTracker(context, Categorizer.SINGLE_CATEGORIZER);
        mQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY,
        mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER);
        mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED,
                mConstants.API_QUOTA_SCHEDULE_COUNT,
                mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
        // Log at most once per minute.
        mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000);

        mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
        mAppStandbyInternal.addListener(mStandbyTracker);
+3 −1
Original line number Diff line number Diff line
@@ -236,6 +236,8 @@ message ConstantsProto {
    optional int64 api_quota_schedule_window_ms = 33;
    // Whether or not to throw an exception when an app hits its schedule quota limit.
    optional bool api_quota_schedule_throw_exception = 34;
    // Whether or not to return a failure result when an app hits its schedule quota limit.
    optional bool api_quota_schedule_return_failure_result = 35;

    message QuotaController {
        option (.android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -335,7 +337,7 @@ message ConstantsProto {
    // In this time after screen turns on, we increase job concurrency.
    optional int32 screen_off_job_concurrency_increase_delay_ms = 28;

    // Next tag: 35
    // Next tag: 36
}

// Next tag: 4
+92 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.IActivityManager;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
@@ -56,6 +57,7 @@ import android.os.SystemClock;
import com.android.server.AppStateTracker;
import com.android.server.DeviceIdleInternal;
import com.android.server.LocalServices;
import com.android.server.SystemServiceManager;
import com.android.server.job.controllers.JobStatus;
import com.android.server.usage.AppStandbyInternal;

@@ -82,6 +84,7 @@ public class JobSchedulerServiceTest {
    private class TestJobSchedulerService extends JobSchedulerService {
        TestJobSchedulerService(Context context) {
            super(context);
            mAppStateTracker = mock(AppStateTracker.class);
        }

        @Override
@@ -136,6 +139,9 @@ public class JobSchedulerServiceTest {
        } catch (RemoteException e) {
            fail("registerUidObserver threw exception: " + e.getMessage());
        }
        // Called by QuotaTracker
        doReturn(mock(SystemServiceManager.class))
                .when(() -> LocalServices.getService(SystemServiceManager.class));

        JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
        JobSchedulerService.sElapsedRealtimeClock =
@@ -750,4 +756,90 @@ public class JobSchedulerServiceTest {
        maybeQueueFunctor.postProcess();
        assertEquals(3, mService.mPendingJobs.size());
    }

    /** Tests that jobs scheduled by the app itself are counted towards scheduling limits. */
    @Test
    public void testScheduleLimiting_RegularSchedule_Blocked() {
        mService.mConstants.ENABLE_API_QUOTAS = true;
        mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
        mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
        mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
        mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true;
        mService.updateQuotaTracker();

        final JobInfo job = createJobInfo().setPersisted(true).build();
        for (int i = 0; i < 500; ++i) {
            final int expected =
                    i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE;
            assertEquals("Got unexpected result for schedule #" + (i + 1),
                    expected,
                    mService.scheduleAsPackage(job, null, 10123, null, 0, ""));
        }
    }

    /**
     * Tests that jobs scheduled by the app itself succeed even if the app is above the scheduling
     * limit.
     */
    @Test
    public void testScheduleLimiting_RegularSchedule_Allowed() {
        mService.mConstants.ENABLE_API_QUOTAS = true;
        mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
        mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
        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();
        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, ""));
        }
    }

    /**
     * Tests that jobs scheduled through a proxy (eg. system server) don't count towards scheduling
     * limits.
     */
    @Test
    public void testScheduleLimiting_Proxy() {
        mService.mConstants.ENABLE_API_QUOTAS = true;
        mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
        mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
        mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
        mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true;
        mService.updateQuotaTracker();

        final JobInfo job = createJobInfo().setPersisted(true).build();
        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, ""));
        }
    }

    /**
     * Tests that jobs scheduled by an app for itself as if through a proxy are counted towards
     * scheduling limits.
     */
    @Test
    public void testScheduleLimiting_SelfProxy() {
        mService.mConstants.ENABLE_API_QUOTAS = true;
        mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
        mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
        mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
        mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true;
        mService.updateQuotaTracker();

        final JobInfo job = createJobInfo().setPersisted(true).build();
        for (int i = 0; i < 500; ++i) {
            final int expected =
                    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(),
                            0, ""));
        }
    }
}