diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 3f8ddff5327c1..28733798b9ad5 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -273,9 +273,11 @@ message ConstantsProto { // The maximum number of jobs an app can run within this particular standby bucket's // window size. optional int32 max_job_count_rare = 11; + // The period of time used to rate limit recently run jobs. + optional int32 rate_limiting_window_ms = 19; // The maximum number of jobs that should be allowed to run in the past - // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}. - optional int32 max_job_count_per_allowed_time = 12; + // rate_limiting_window_ms. + optional int32 max_job_count_per_rate_limiting_window = 12; // The maximum number of timing sessions an app can run within this particular standby // bucket's window size. optional int32 max_session_count_active = 13; @@ -289,8 +291,8 @@ message ConstantsProto { // bucket's window size. optional int32 max_session_count_rare = 16; // The maximum number of timing sessions that should be allowed to run in the past - // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}. - optional int32 max_session_count_per_allowed_time = 17; + // rate_limiting_window_ms. + optional int32 max_session_count_per_rate_limiting_window = 17; // Treat two distinct {@link TimingSession}s as the same if they start and end within this // amount of time of each other. optional int64 timing_session_coalescing_duration_ms = 18; @@ -517,63 +519,56 @@ message StateControllerProto { optional int64 expiration_time_elapsed = 2; optional int64 window_size_ms = 3; - /** The total amount of time the app ran in its respective bucket window size. */ + optional int32 job_count_limit = 14; + optional int32 session_count_limit = 15; + + // The total amount of time the app ran in its respective bucket window size. optional int64 execution_time_in_window_ms = 4; optional int32 bg_job_count_in_window = 5; - /** - * The total amount of time the app ran in the last - * {@link QuotaController#MAX_PERIOD_MS}. - */ + // The total amount of time the app ran in the last + // {@link QuotaController#MAX_PERIOD_MS}. optional int64 execution_time_in_max_period_ms = 6; optional int32 bg_job_count_in_max_period = 7; - /** - * The number of {@link TimingSession}s within the bucket window size. This will include - * sessions that started before the window as long as they end within the window. - */ + // The number of {@link TimingSession}s within the bucket window size. This will include + // sessions that started before the window as long as they end within the window. optional int32 session_count_in_window = 11; - /** - * The time after which the sum of all the app's sessions plus - * ConstantsProto.QuotaController.in_quota_buffer_ms equals the quota. This is only - * valid if - * execution_time_in_window_ms >= - * ConstantsProto.QuotaController.allowed_time_per_period_ms - * or - * execution_time_in_max_period_ms >= - * ConstantsProto.QuotaController.max_execution_time_ms. - */ - optional int64 quota_cutoff_time_elapsed = 8; + // The time after which the app will be under the bucket quota. This is only valid if + // execution_time_in_window_ms >= + // ConstantsProto.QuotaController.allowed_time_per_period_ms + // or + // execution_time_in_max_period_ms >= + // ConstantsProto.QuotaController.max_execution_time_ms + // or + // bg_job_count_in_window >= job_count_limit + // or + // session_count_in_window >= session_count_limit. + optional int64 in_quota_time_elapsed = 8; - /** - * The time after which job_count_in_allowed_time should be considered invalid, in the - * elapsed realtime timebase. - */ + // The time after which job_count_in_rate_limiting_window should be considered invalid, + // in the elapsed realtime timebase. optional int64 job_count_expiration_time_elapsed = 9; - /** - * The number of jobs that ran in at least the last - * ConstantsProto.QuotaController.allowed_time_per_period_ms. - * It may contain a few stale entries since cleanup won't happen exactly every - * ConstantsProto.QuotaController.allowed_time_per_period_ms. - */ - optional int32 job_count_in_allowed_time = 10; + // The number of jobs that ran in at least the last + // ConstantsProto.QuotaController.rate_limiting_window_ms. + // It may contain a few stale entries since cleanup won't happen exactly every + // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be + // considered valid before elapsed realtime has reached + // job_count_expiration_time_elapsed. + optional int32 job_count_in_rate_limiting_window = 10; - /** - * The time after which {@link #timingSessionCountInAllowedTime} should be considered - * invalid, in the elapsed realtime timebase. - */ + // The time after which {@link #timingSessionCountInAllowedTime} should be considered + // invalid, in the elapsed realtime timebase. optional int64 session_count_expiration_time_elapsed = 12; - /** - * The number of {@link TimingSession}s that ran in at least the last - * {@link #mAllowedTimePerPeriodMs}. It may contain a few stale entries since cleanup won't - * happen exactly every {@link #mAllowedTimePerPeriodMs}. This should only be considered - * valid before elapsed realtime has reached - * {@link #timingSessionCountExpirationTimeElapsed}. - */ - optional int32 session_count_in_allowed_time = 13; + // The number of {@link TimingSession}s that ran in at least the last + // ConstantsProto.QuotaController.rate_limiting_window_ms. It may contain a few stale + // entries since cleanup won't happen exactly every + // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be considered + // valid before elapsed realtime has reached session_count_expiration_time_elapsed. + optional int32 session_count_in_rate_limiting_window = 13; } message Package { diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java index f560d69310a3a..ef6944e180de6 100644 --- a/services/core/java/com/android/server/job/controllers/QuotaController.java +++ b/services/core/java/com/android/server/job/controllers/QuotaController.java @@ -16,6 +16,10 @@ package com.android.server.job.controllers; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; + import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; import static com.android.server.job.JobSchedulerService.NEVER_INDEX; @@ -75,17 +79,28 @@ import java.util.function.Predicate; /** * Controller that tracks whether an app has exceeded its standby bucket quota. * - * Each job in each bucket is given 10 minutes to run within its respective time window. Active - * jobs can run indefinitely, working set jobs can run for 10 minutes within a 2 hour window, - * frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run 10 minutes in - * a 24 hour window. The windows are rolling, so as soon as a job would have some quota based on its - * bucket, it will be eligible to run. When a job's bucket changes, its new quota is immediately - * applied to it. + * With initial defaults, each app in each bucket is given 10 minutes to run within its respective + * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a + * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run + * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some + * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new + * quota is immediately applied to it. + * + * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on + * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will + * not be allowed to run more than 20 jobs within the past 10 minutes. * * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run * freely when an app enters the foreground state and are restricted when the app leaves the - * foreground state. However, jobs that are started while the app is in the TOP state are not - * restricted regardless of the app's state change. + * foreground state. However, jobs that are started while the app is in the TOP state do not count + * towards any quota and are not restricted regardless of the app's state change. + * + * Jobs will not be throttled when the device is charging. The device is considered to be charging + * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast. + * + * Note: all limits are enforced per bucket window unless explicitly stated otherwise. + * All stated values are configurable and subject to change. See {@link QcConstants} for current + * defaults. * * Test: atest com.android.server.job.controllers.QuotaControllerTest */ @@ -94,8 +109,6 @@ public final class QuotaController extends StateController { private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); - private static final long MINUTE_IN_MILLIS = 60 * 1000L; - private static final String ALARM_TAG_CLEANUP = "*job.cleanup*"; private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*"; @@ -244,6 +257,8 @@ public final class QuotaController extends StateController { public long expirationTimeElapsed; public long windowSizeMs; + public int jobCountLimit; + public int sessionCountLimit; /** The total amount of time the app ran in its respective bucket window size. */ public long executionTimeInWindowMs; @@ -260,54 +275,58 @@ public final class QuotaController extends StateController { public int sessionCountInWindow; /** - * The time after which the sum of all the app's sessions plus {@link #mQuotaBufferMs} - * equals the quota. This is only valid if - * executionTimeInWindowMs >= {@link #mAllowedTimePerPeriodMs} or - * executionTimeInMaxPeriodMs >= {@link #mMaxExecutionTimeMs}. + * The time after which the app will be under the bucket quota and can start running jobs + * again. This is only valid if + * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs}, + * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs}, + * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or + * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}. */ - public long quotaCutoffTimeElapsed; + public long inQuotaTimeElapsed; /** - * The time after which {@link #jobCountInAllowedTime} should be considered invalid, in the - * elapsed realtime timebase. + * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid, + * in the elapsed realtime timebase. */ - public long jobCountExpirationTimeElapsed; + public long jobRateLimitExpirationTimeElapsed; /** - * The number of jobs that ran in at least the last {@link #mAllowedTimePerPeriodMs}. + * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}. * It may contain a few stale entries since cleanup won't happen exactly every - * {@link #mAllowedTimePerPeriodMs}. + * {@link #mRateLimitingWindowMs}. */ - public int jobCountInAllowedTime; + public int jobCountInRateLimitingWindow; /** - * The time after which {@link #sessionCountInAllowedTime} should be considered + * The time after which {@link #sessionCountInRateLimitingWindow} should be considered * invalid, in the elapsed realtime timebase. */ - public long sessionCountExpirationTimeElapsed; + public long sessionRateLimitExpirationTimeElapsed; /** * The number of {@link TimingSession}s that ran in at least the last - * {@link #mAllowedTimePerPeriodMs}. It may contain a few stale entries since cleanup won't - * happen exactly every {@link #mAllowedTimePerPeriodMs}. This should only be considered - * valid before elapsed realtime has reached {@link #sessionCountExpirationTimeElapsed}. + * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't + * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered + * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}. */ - public int sessionCountInAllowedTime; + public int sessionCountInRateLimitingWindow; @Override public String toString() { return "expirationTime=" + expirationTimeElapsed + ", " - + "windowSize=" + windowSizeMs + ", " + + "windowSizeMs=" + windowSizeMs + ", " + + "jobCountLimit=" + jobCountLimit + ", " + + "sessionCountLimit=" + sessionCountLimit + ", " + "executionTimeInWindow=" + executionTimeInWindowMs + ", " + "bgJobCountInWindow=" + bgJobCountInWindow + ", " + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", " + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", " + "sessionCountInWindow=" + sessionCountInWindow + ", " - + "quotaCutoffTime=" + quotaCutoffTimeElapsed + ", " - + "jobCountExpirationTime=" + jobCountExpirationTimeElapsed + ", " - + "jobCountInAllowedTime=" + jobCountInAllowedTime + ", " - + "sessionCountExpirationTime=" + sessionCountExpirationTimeElapsed + ", " - + "sessionCountInAllowedTime=" + sessionCountInAllowedTime; + + "inQuotaTime=" + inQuotaTimeElapsed + ", " + + "jobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", " + + "jobCountInRateLimitingWindow=" + jobCountInRateLimitingWindow + ", " + + "sessionCountExpirationTime=" + sessionRateLimitExpirationTimeElapsed + ", " + + "sessionCountInRateLimitingWindow=" + sessionCountInRateLimitingWindow; } @Override @@ -316,18 +335,21 @@ public final class QuotaController extends StateController { ExecutionStats other = (ExecutionStats) obj; return this.expirationTimeElapsed == other.expirationTimeElapsed && this.windowSizeMs == other.windowSizeMs + && this.jobCountLimit == other.jobCountLimit + && this.sessionCountLimit == other.sessionCountLimit && this.executionTimeInWindowMs == other.executionTimeInWindowMs && this.bgJobCountInWindow == other.bgJobCountInWindow && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs && this.sessionCountInWindow == other.sessionCountInWindow && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod - && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed - && this.jobCountExpirationTimeElapsed == other.jobCountExpirationTimeElapsed - && this.jobCountInAllowedTime == other.jobCountInAllowedTime - && this.sessionCountExpirationTimeElapsed - == other.sessionCountExpirationTimeElapsed - && this.sessionCountInAllowedTime - == other.sessionCountInAllowedTime; + && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed + && this.jobRateLimitExpirationTimeElapsed + == other.jobRateLimitExpirationTimeElapsed + && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow + && this.sessionRateLimitExpirationTimeElapsed + == other.sessionRateLimitExpirationTimeElapsed + && this.sessionCountInRateLimitingWindow + == other.sessionCountInRateLimitingWindow; } else { return false; } @@ -338,16 +360,18 @@ public final class QuotaController extends StateController { int result = 0; result = 31 * result + hashLong(expirationTimeElapsed); result = 31 * result + hashLong(windowSizeMs); + result = 31 * result + hashLong(jobCountLimit); + result = 31 * result + hashLong(sessionCountLimit); result = 31 * result + hashLong(executionTimeInWindowMs); result = 31 * result + bgJobCountInWindow; result = 31 * result + hashLong(executionTimeInMaxPeriodMs); result = 31 * result + bgJobCountInMaxPeriod; result = 31 * result + sessionCountInWindow; - result = 31 * result + hashLong(quotaCutoffTimeElapsed); - result = 31 * result + hashLong(jobCountExpirationTimeElapsed); - result = 31 * result + jobCountInAllowedTime; - result = 31 * result + hashLong(sessionCountExpirationTimeElapsed); - result = 31 * result + sessionCountInAllowedTime; + result = 31 * result + hashLong(inQuotaTimeElapsed); + result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed); + result = 31 * result + jobCountInRateLimitingWindow; + result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed); + result = 31 * result + sessionCountInRateLimitingWindow; return result; } } @@ -399,19 +423,19 @@ public final class QuotaController extends StateController { private boolean mShouldThrottle; /** How much time each app will have to run jobs within their standby bucket window. */ - private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS; + private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; /** * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS} * window. */ - private long mMaxExecutionTimeMs = 4 * 60 * MINUTE_IN_MILLIS; + private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS; /** * How much time the app should have before transitioning from out-of-quota to in-quota. * This should not affect processing if the app is already in-quota. */ - private long mQuotaBufferMs = 30 * 1000L; // 30 seconds + private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS; /** * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine @@ -425,14 +449,19 @@ public final class QuotaController extends StateController { */ private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; - /** The maximum number of jobs that can run within the past {@link #mAllowedTimePerPeriodMs}. */ - private int mMaxJobCountPerAllowedTime = 20; + /** The period of time used to rate limit recently run jobs. */ + private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS; + + /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */ + private int mMaxJobCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; /** * The maximum number of {@link TimingSession}s that can run within the past {@link - * #mAllowedTimePerPeriodMs}. + * #mRateLimitingWindowMs}. */ - private int mMaxSessionCountPerAllowedTime = 20; + private int mMaxSessionCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; private long mNextCleanupTimeElapsed = 0; private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener = @@ -486,11 +515,11 @@ public final class QuotaController extends StateController { * The rolling window size for each standby bucket. Within each window, an app will have 10 * minutes to run its jobs. */ - private final long[] mBucketPeriodsMs = new long[] { - 10 * MINUTE_IN_MILLIS, // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time - 2 * 60 * MINUTE_IN_MILLIS, // 2 hours for WORKING - 8 * 60 * MINUTE_IN_MILLIS, // 8 hours for FREQUENT - 24 * 60 * MINUTE_IN_MILLIS // 24 hours for RARE + private final long[] mBucketPeriodsMs = new long[]{ + QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS, + QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS, + QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS, + QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS }; /** The maximum period any bucket can have. */ @@ -503,16 +532,13 @@ public final class QuotaController extends StateController { * * @see #mBucketPeriodsMs */ - private final int[] mMaxBucketJobCounts = new int[] { - 200, // ACTIVE -- 1200/hr - 1200, // WORKING -- 600/hr - 1800, // FREQUENT -- 225/hr - 2400 // RARE -- 100/hr + private final int[] mMaxBucketJobCounts = new int[]{ + QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE, + QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING, + QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT, + QcConstants.DEFAULT_MAX_JOB_COUNT_RARE }; - /** The minimum number of jobs that any bucket will be allowed to run. */ - private static final int MIN_BUCKET_JOB_COUNT = 100; - /** * The maximum number of {@link TimingSession}s based on its standby bucket. For each max value * count in the array, the app will not be allowed to have more than that many number of @@ -527,14 +553,12 @@ public final class QuotaController extends StateController { QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE }; - /** The minimum number of {@link TimingSession}s that any bucket will be allowed to run. */ - private static final int MIN_BUCKET_SESSION_COUNT = 3; - /** * Treat two distinct {@link TimingSession}s as the same if they start and end within this * amount of time of each other. */ - private long mTimingSessionCoalescingDurationMs = 0; + private long mTimingSessionCoalescingDurationMs = + QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; /** An app has reached its quota. The message should contain a {@link Package} object. */ private static final int MSG_REACHED_QUOTA = 0; @@ -761,8 +785,8 @@ public final class QuotaController extends StateController { final int standbyBucket) { final long now = sElapsedRealtimeClock.millis(); final boolean isUnderAllowedTimeQuota = - (stats.jobCountExpirationTimeElapsed <= now - || stats.jobCountInAllowedTime < mMaxJobCountPerAllowedTime); + (stats.jobRateLimitExpirationTimeElapsed <= now + || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow); return isUnderAllowedTimeQuota && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]); } @@ -770,10 +794,8 @@ public final class QuotaController extends StateController { private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, final int standbyBucket) { final long now = sElapsedRealtimeClock.millis(); - final boolean isUnderAllowedTimeQuota = - (stats.sessionCountExpirationTimeElapsed <= now - || stats.sessionCountInAllowedTime - < mMaxSessionCountPerAllowedTime); + final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now + || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow); return isUnderAllowedTimeQuota && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket]; } @@ -924,12 +946,18 @@ public final class QuotaController extends StateController { } if (refreshStatsIfOld) { final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket]; + final int jobCountLimit = mMaxBucketJobCounts[standbyBucket]; + final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket]; Timer timer = mPkgTimers.get(userId, packageName); if ((timer != null && timer.isActive()) || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis() - || stats.windowSizeMs != bucketWindowSizeMs) { + || stats.windowSizeMs != bucketWindowSizeMs + || stats.jobCountLimit != jobCountLimit + || stats.sessionCountLimit != sessionCountLimit) { // The stats are no longer valid. stats.windowSizeMs = bucketWindowSizeMs; + stats.jobCountLimit = jobCountLimit; + stats.sessionCountLimit = sessionCountLimit; updateExecutionStatsLocked(userId, packageName, stats); } } @@ -945,7 +973,7 @@ public final class QuotaController extends StateController { stats.executionTimeInMaxPeriodMs = 0; stats.bgJobCountInMaxPeriod = 0; stats.sessionCountInWindow = 0; - stats.quotaCutoffTimeElapsed = 0; + stats.inQuotaTimeElapsed = 0; Timer timer = mPkgTimers.get(userId, packageName); final long nowElapsed = sElapsedRealtimeClock.millis(); @@ -958,12 +986,12 @@ public final class QuotaController extends StateController { // invalidate now. stats.expirationTimeElapsed = nowElapsed; if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - nowElapsed - mAllowedTimeIntoQuotaMs); + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs); } if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - nowElapsed - mMaxExecutionTimeIntoQuotaMs); + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); } } @@ -985,36 +1013,40 @@ public final class QuotaController extends StateController { TimingSession session = sessions.get(i); // Window management. - if (startWindowElapsed < session.startTimeElapsed) { - stats.executionTimeInWindowMs += session.endTimeElapsed - session.startTimeElapsed; + if (startWindowElapsed < session.endTimeElapsed) { + final long start; + if (startWindowElapsed < session.startTimeElapsed) { + start = session.startTimeElapsed; + emptyTimeMs = + Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed); + } else { + // The session started before the window but ended within the window. Only + // include the portion that was within the window. + start = startWindowElapsed; + emptyTimeMs = 0; + } + + stats.executionTimeInWindowMs += session.endTimeElapsed - start; stats.bgJobCountInWindow += session.bgJobCount; - emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed); if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - session.startTimeElapsed + stats.executionTimeInWindowMs - - mAllowedTimeIntoQuotaMs); + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs + + stats.windowSizeMs); + } + if (stats.bgJobCountInWindow >= stats.jobCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); } if (i == loopStart || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) > mTimingSessionCoalescingDurationMs) { // Coalesce sessions if they are very close to each other in time sessionCountInWindow++; - } - } else if (startWindowElapsed < session.endTimeElapsed) { - // The session started before the window but ended within the window. Only include - // the portion that was within the window. - stats.executionTimeInWindowMs += session.endTimeElapsed - startWindowElapsed; - stats.bgJobCountInWindow += session.bgJobCount; - emptyTimeMs = 0; - if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, - startWindowElapsed + stats.executionTimeInWindowMs - - mAllowedTimeIntoQuotaMs); - } - if (i == loopStart - || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) - > mTimingSessionCoalescingDurationMs) { - sessionCountInWindow++; + + if (sessionCountInWindow >= stats.sessionCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); + } } } @@ -1025,9 +1057,9 @@ public final class QuotaController extends StateController { stats.bgJobCountInMaxPeriod += session.bgJobCount; emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed); if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, session.startTimeElapsed + stats.executionTimeInMaxPeriodMs - - mMaxExecutionTimeIntoQuotaMs); + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); } } else if (startMaxElapsed < session.endTimeElapsed) { // The session started before the window but ended within the window. Only include @@ -1036,9 +1068,9 @@ public final class QuotaController extends StateController { stats.bgJobCountInMaxPeriod += session.bgJobCount; emptyTimeMs = 0; if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { - stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed, + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, startMaxElapsed + stats.executionTimeInMaxPeriodMs - - mMaxExecutionTimeIntoQuotaMs); + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); } } else { // This session ended before the window. No point in going any further. @@ -1094,11 +1126,11 @@ public final class QuotaController extends StateController { stats = new ExecutionStats(); appStats[i] = stats; } - if (stats.jobCountExpirationTimeElapsed <= now) { - stats.jobCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs; - stats.jobCountInAllowedTime = 0; + if (stats.jobRateLimitExpirationTimeElapsed <= now) { + stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.jobCountInRateLimitingWindow = 0; } - stats.jobCountInAllowedTime += count; + stats.jobCountInRateLimitingWindow += count; } } @@ -1115,11 +1147,11 @@ public final class QuotaController extends StateController { stats = new ExecutionStats(); appStats[i] = stats; } - if (stats.sessionCountExpirationTimeElapsed <= now) { - stats.sessionCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs; - stats.sessionCountInAllowedTime = 0; + if (stats.sessionRateLimitExpirationTimeElapsed <= now) { + stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.sessionCountInRateLimitingWindow = 0; } - stats.sessionCountInAllowedTime++; + stats.sessionCountInRateLimitingWindow++; } } @@ -1367,18 +1399,17 @@ public final class QuotaController extends StateController { } // The time this app will have quota again. - long inQuotaTimeElapsed = stats.quotaCutoffTimeElapsed + stats.windowSizeMs; - if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) { + long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; + if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { + // App hit the rate limit. inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, - stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS); + stats.jobRateLimitExpirationTimeElapsed); } - if (!isUnderJobCountQuota) { + if (!isUnderTimingSessionCountQuota + && stats.sessionCountInWindow < stats.sessionCountLimit) { + // App hit the rate limit. inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, - stats.jobCountExpirationTimeElapsed + mAllowedTimePerPeriodMs); - } - if (!isUnderTimingSessionCountQuota) { - inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, - stats.sessionCountExpirationTimeElapsed + mAllowedTimePerPeriodMs); + stats.sessionRateLimitExpirationTimeElapsed); } // Only schedule the alarm if: // 1. There isn't one currently scheduled @@ -1399,7 +1430,7 @@ public final class QuotaController extends StateController { ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler); alarmListener.setTriggerTime(inQuotaTimeElapsed); } else if (DEBUG) { - Slog.d(TAG, "No need to scheduling start alarm for " + pkgString); + Slog.d(TAG, "No need to schedule start alarm for " + pkgString); } } @@ -1968,14 +1999,15 @@ public final class QuotaController extends StateController { private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working"; private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent"; private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare"; - private static final String KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME = - "max_count_per_allowed_time"; + private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms"; + private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + "max_job_count_per_rate_limiting_window"; private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active"; private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working"; private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent"; private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare"; - private static final String KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME = - "max_session_count_per_allowed_time"; + private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + "max_session_count_per_rate_limiting_window"; private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS = "timing_session_coalescing_duration_ms"; @@ -1984,7 +2016,7 @@ public final class QuotaController extends StateController { private static final long DEFAULT_IN_QUOTA_BUFFER_MS = 30 * 1000L; // 30 seconds private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS = - 10 * 60 * 1000L; // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time + DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time private static final long DEFAULT_WINDOW_SIZE_WORKING_MS = 2 * 60 * 60 * 1000L; // 2 hours private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS = @@ -1992,16 +2024,18 @@ public final class QuotaController extends StateController { private static final long DEFAULT_WINDOW_SIZE_RARE_MS = 24 * 60 * 60 * 1000L; // 24 hours private static final long DEFAULT_MAX_EXECUTION_TIME_MS = - 4 * 60 * 60 * 1000L; // 4 hours - private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = - 200; // 1200/hr - private static final int DEFAULT_MAX_JOB_COUNT_WORKING = - 1200; // 600/hr - private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = - 1800; // 225/hr - private static final int DEFAULT_MAX_JOB_COUNT_RARE = - 2400; // 100/hr - private static final int DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME = 20; + 4 * HOUR_IN_MILLIS; + private static final long DEFAULT_RATE_LIMITING_WINDOW_MS = + 10 * MINUTE_IN_MILLIS; + private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = // 20/window = 120/hr = 1/session + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; + private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session + (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session + (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session + (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE = 20; // 120/hr private static final int DEFAULT_MAX_SESSION_COUNT_WORKING = @@ -2010,8 +2044,8 @@ public final class QuotaController extends StateController { 8; // 1/hr private static final int DEFAULT_MAX_SESSION_COUNT_RARE = 3; // .125/hr - private static final int DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME = 20; - private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 0; + private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds /** How much time each app will have to run jobs within their standby bucket window. */ public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; @@ -2079,11 +2113,14 @@ public final class QuotaController extends StateController { */ public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE; + /** The period of time used to rate limit recently run jobs. */ + public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS; + /** - * The maximum number of jobs that can run within the past - * {@link #ALLOWED_TIME_PER_PERIOD_MS}. + * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}. */ - public int MAX_JOB_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME; + public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; /** * The maximum number of {@link TimingSession}s an app can run within this particular @@ -2113,7 +2150,8 @@ public final class QuotaController extends StateController { * The maximum number of {@link TimingSession}s that can run within the past * {@link #ALLOWED_TIME_PER_PERIOD_MS}. */ - public int MAX_SESSION_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME; + public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; /** * Treat two distinct {@link TimingSession}s as the same if they start and end within this @@ -2122,6 +2160,29 @@ public final class QuotaController extends StateController { public long TIMING_SESSION_COALESCING_DURATION_MS = DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; + // Safeguards + + /** The minimum number of jobs that any bucket will be allowed to run within its window. */ + private static final int MIN_BUCKET_JOB_COUNT = 10; + + /** + * The minimum number of {@link TimingSession}s that any bucket will be allowed to run + * within its window. + */ + private static final int MIN_BUCKET_SESSION_COUNT = 1; + + /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */ + private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS; + + /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */ + private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS; + QcConstants(Handler handler) { super(handler); } @@ -2167,8 +2228,11 @@ public final class QuotaController extends StateController { KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT); MAX_JOB_COUNT_RARE = mParser.getInt( KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE); - MAX_JOB_COUNT_PER_ALLOWED_TIME = mParser.getInt( - KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME); + RATE_LIMITING_WINDOW_MS = mParser.getLong( + KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS); + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); MAX_SESSION_COUNT_ACTIVE = mParser.getInt( KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE); MAX_SESSION_COUNT_WORKING = mParser.getInt( @@ -2177,9 +2241,9 @@ public final class QuotaController extends StateController { KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT); MAX_SESSION_COUNT_RARE = mParser.getInt( KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE); - MAX_SESSION_COUNT_PER_ALLOWED_TIME = mParser.getInt( - KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME, - DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME); + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong( KEY_TIMING_SESSION_COALESCING_DURATION_MS, DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS); @@ -2192,7 +2256,14 @@ public final class QuotaController extends StateController { synchronized (mLock) { boolean changed = false; - long newAllowedTimeMs = Math.min(MAX_PERIOD_MS, + long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS, + Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS)); + if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) { + mMaxExecutionTimeMs = newMaxExecutionTimeMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + changed = true; + } + long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs, Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS)); if (mAllowedTimePerPeriodMs != newAllowedTimeMs) { mAllowedTimePerPeriodMs = newAllowedTimeMs; @@ -2231,47 +2302,44 @@ public final class QuotaController extends StateController { mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs; changed = true; } - long newMaxExecutionTimeMs = Math.max(60 * MINUTE_IN_MILLIS, - Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS)); - if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) { - mMaxExecutionTimeMs = newMaxExecutionTimeMs; - mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS, + Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS)); + if (mRateLimitingWindowMs != newRateLimitingWindowMs) { + mRateLimitingWindowMs = newRateLimitingWindowMs; changed = true; } - int newMaxCountPerAllowedPeriod = Math.max(10, - MAX_JOB_COUNT_PER_ALLOWED_TIME); - if (mMaxJobCountPerAllowedTime != newMaxCountPerAllowedPeriod) { - mMaxJobCountPerAllowedTime = newMaxCountPerAllowedPeriod; + int newMaxJobCountPerRateLimitingWindow = Math.max( + MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) { + mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow; changed = true; } - int newActiveMaxJobCount = Math.max(mMaxJobCountPerAllowedTime, - Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE)); + int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE); if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) { mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount; changed = true; } - int newWorkingMaxJobCount = Math.max(mMaxJobCountPerAllowedTime, - Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING)); + int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING); if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) { mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount; changed = true; } - int newFrequentMaxJobCount = Math.max(mMaxJobCountPerAllowedTime, - Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT)); + int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT); if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) { mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount; changed = true; } - int newRareMaxJobCount = Math.max(mMaxJobCountPerAllowedTime, - Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE)); + int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE); if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) { mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount; changed = true; } - int newMaxSessionCountPerAllowedPeriod = Math.max(10, - MAX_SESSION_COUNT_PER_ALLOWED_TIME); - if (mMaxSessionCountPerAllowedTime != newMaxSessionCountPerAllowedPeriod) { - mMaxSessionCountPerAllowedTime = newMaxSessionCountPerAllowedPeriod; + int newMaxSessionCountPerRateLimitPeriod = Math.max( + MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) { + mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod; changed = true; } int newActiveMaxSessionCount = @@ -2332,14 +2400,15 @@ public final class QuotaController extends StateController { pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println(); pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println(); pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println(); - pw.printPair(KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, MAX_JOB_COUNT_PER_ALLOWED_TIME) - .println(); + pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println(); + pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println(); pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println(); pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println(); pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println(); pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println(); - pw.printPair(KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME, MAX_SESSION_COUNT_PER_ALLOWED_TIME) - .println(); + pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println(); pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS, TIMING_SESSION_COALESCING_DURATION_MS).println(); pw.decreaseIndent(); @@ -2365,8 +2434,10 @@ public final class QuotaController extends StateController { proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT); proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE); - proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_ALLOWED_TIME, - MAX_JOB_COUNT_PER_ALLOWED_TIME); + proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS, + RATE_LIMITING_WINDOW_MS); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE); proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING, @@ -2375,8 +2446,8 @@ public final class QuotaController extends StateController { MAX_SESSION_COUNT_FREQUENT); proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE); - proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_ALLOWED_TIME, - MAX_SESSION_COUNT_PER_ALLOWED_TIME); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS, TIMING_SESSION_COALESCING_DURATION_MS); proto.end(qcToken); @@ -2431,8 +2502,18 @@ public final class QuotaController extends StateController { } @VisibleForTesting - int getMaxJobCountPerAllowedTime() { - return mMaxJobCountPerAllowedTime; + int getMaxJobCountPerRateLimitingWindow() { + return mMaxJobCountPerRateLimitingWindow; + } + + @VisibleForTesting + int getMaxSessionCountPerRateLimitingWindow() { + return mMaxSessionCountPerRateLimitingWindow; + } + + @VisibleForTesting + long getRateLimitingWindowMs() { + return mRateLimitingWindowMs; } @VisibleForTesting @@ -2440,11 +2521,6 @@ public final class QuotaController extends StateController { return mTimingSessionCoalescingDurationMs; } - @VisibleForTesting - int getMaxSessionCountPerAllowedTime() { - return mMaxSessionCountPerAllowedTime; - } - @VisibleForTesting @Nullable List getTimingSessions(int userId, String packageName) { @@ -2658,6 +2734,12 @@ public final class QuotaController extends StateController { proto.write( StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS, es.windowSizeMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT, + es.jobCountLimit); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT, + es.sessionCountLimit); proto.write( StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS, es.executionTimeInWindowMs); @@ -2674,20 +2756,20 @@ public final class QuotaController extends StateController { StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW, es.sessionCountInWindow); proto.write( - StateControllerProto.QuotaController.ExecutionStats.QUOTA_CUTOFF_TIME_ELAPSED, - es.quotaCutoffTimeElapsed); + StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED, + es.inQuotaTimeElapsed); proto.write( StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED, - es.jobCountExpirationTimeElapsed); + es.jobRateLimitExpirationTimeElapsed); proto.write( - StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_ALLOWED_TIME, - es.jobCountInAllowedTime); + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW, + es.jobCountInRateLimitingWindow); proto.write( StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED, - es.sessionCountExpirationTimeElapsed); + es.sessionRateLimitExpirationTimeElapsed); proto.write( - StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_ALLOWED_TIME, - es.sessionCountInAllowedTime); + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW, + es.sessionCountInRateLimitingWindow); proto.end(esToken); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 4e893575e9d40..1db6b8e581656 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -386,6 +386,8 @@ public class QuotaControllerTest { ExecutionStats expectedStats = new ExecutionStats(); expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; final int uid = 10001; mQuotaController.onAppRemovedLocked("com.android.test.remove", uid); @@ -424,6 +426,8 @@ public class QuotaControllerTest { ExecutionStats expectedStats = new ExecutionStats(); expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; mQuotaController.onUserRemovedLocked(0); assertNull(mQuotaController.getTimingSessions(0, "com.android.test")); @@ -456,6 +460,8 @@ public class QuotaControllerTest { ExecutionStats inputStats = new ExecutionStats(); inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS; + inputStats.jobCountLimit = expectedStats.jobCountLimit = 100; + inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100; // Invalid time is now +24 hours since there are no sessions at all for the app. expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats); @@ -520,6 +526,7 @@ public class QuotaControllerTest { assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS; + inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 2; // Invalid time is now since the start of the session is at the very edge of the window // cutoff time. expectedStats.expirationTimeElapsed = now; @@ -528,10 +535,13 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 3; + expectedStats.inQuotaTimeElapsed = now + 11 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + inputStats.jobCountLimit = expectedStats.jobCountLimit = 6; + inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100; // Invalid time is now since the session straddles the window cutoff time. expectedStats.expirationTimeElapsed = now; expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS; @@ -539,8 +549,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 4; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) - + mQcConstants.IN_QUOTA_BUFFER_MS; + expectedStats.inQuotaTimeElapsed = now + 5 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -553,8 +562,9 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 4; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) - + mQcConstants.IN_QUOTA_BUFFER_MS; + // App goes under job execution time limit in ~61 minutes, but will be under job count limit + // in 65 minutes. + expectedStats.inQuotaTimeElapsed = now + 65 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -567,8 +577,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 15; expectedStats.sessionCountInWindow = 5; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) - + mQcConstants.IN_QUOTA_BUFFER_MS; + expectedStats.inQuotaTimeElapsed = now + 4 * HOUR_IN_MILLIS + 5 * MINUTE_IN_MILLIS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -577,6 +586,7 @@ public class QuotaControllerTest { .add(0, createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3)); inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + inputStats.jobCountLimit = expectedStats.jobCountLimit = 100; // Invalid time is now +1 hour since the earliest session in the max period is 1 hour // before the end of the max period cutoff time. expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; @@ -585,7 +595,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 18; expectedStats.sessionCountInWindow = 5; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -602,7 +612,7 @@ public class QuotaControllerTest { expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 5; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats); assertEquals(expectedStats, inputStats); @@ -627,6 +637,8 @@ public class QuotaControllerTest { // Active expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE; expectedStats.expirationTimeElapsed = now + 4 * MINUTE_IN_MILLIS; expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 5; @@ -638,44 +650,102 @@ public class QuotaControllerTest { // Working expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING; expectedStats.expirationTimeElapsed = now; expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 10; expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 2; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 3 * MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX)); // Frequent expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 15; expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 3; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX)); // Rare expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInWindow = 20; expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; expectedStats.bgJobCountInMaxPeriod = 20; expectedStats.sessionCountInWindow = 4; - expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS) + expectedStats.inQuotaTimeElapsed = now + 22 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS + mQcConstants.IN_QUOTA_BUFFER_MS; assertEquals(expectedStats, mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX)); } + /** + * Tests that getExecutionStatsLocked returns the correct stats soon after device startup. + */ + @Test + public void testGetExecutionStatsLocked_Values_BeginningOfTime() { + // Set time to 3 minutes after boot. + advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis()); + advanceElapsedClock(3 * MINUTE_IN_MILLIS); + + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2)); + + ExecutionStats expectedStats = new ExecutionStats(); + + // Active + expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE; + expectedStats.expirationTimeElapsed = 11 * MINUTE_IN_MILLIS; + expectedStats.executionTimeInWindowMs = MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 2; + expectedStats.executionTimeInMaxPeriodMs = MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 2; + expectedStats.sessionCountInWindow = 1; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX)); + + // Working + expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING; + expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX)); + + // Frequent + expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT; + expectedStats.expirationTimeElapsed = 8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX)); + + // Rare + expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; + expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX)); + } + /** * Tests that getExecutionStatsLocked returns the correct timing session stats when coalescing. */ @@ -850,13 +920,15 @@ public class QuotaControllerTest { ExecutionStats expectedStats = new ExecutionStats(); expectedStats.windowSizeMs = originalStatsActive.windowSizeMs; + expectedStats.jobCountLimit = originalStatsActive.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsActive.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsActive.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow; expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs; expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod; expectedStats.sessionCountInWindow = originalStatsActive.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsActive.inQuotaTimeElapsed; final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX); // Stats for the same bucket should use the same object. @@ -864,33 +936,39 @@ public class QuotaControllerTest { assertEquals(expectedStats, newStatsActive); expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs; + expectedStats.jobCountLimit = originalStatsWorking.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsWorking.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsWorking.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow; expectedStats.sessionCountInWindow = originalStatsWorking.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsWorking.inQuotaTimeElapsed; final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX); assertTrue(originalStatsWorking == newStatsWorking); assertNotEquals(expectedStats, newStatsWorking); expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs; + expectedStats.jobCountLimit = originalStatsFrequent.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsFrequent.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsFrequent.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow; expectedStats.sessionCountInWindow = originalStatsFrequent.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsFrequent.inQuotaTimeElapsed; final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX); assertTrue(originalStatsFrequent == newStatsFrequent); assertNotEquals(expectedStats, newStatsFrequent); expectedStats.windowSizeMs = originalStatsRare.windowSizeMs; + expectedStats.jobCountLimit = originalStatsRare.jobCountLimit; + expectedStats.sessionCountLimit = originalStatsRare.sessionCountLimit; expectedStats.expirationTimeElapsed = originalStatsRare.expirationTimeElapsed; expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs; expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow; expectedStats.sessionCountInWindow = originalStatsRare.sessionCountInWindow; - expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed; + expectedStats.inQuotaTimeElapsed = originalStatsRare.inQuotaTimeElapsed; final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX); assertTrue(originalStatsRare == newStatsRare); @@ -1065,7 +1143,7 @@ public class QuotaControllerTest { public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; + final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; mQuotaController.saveTimingSession(0, "com.android.test.spam", createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25)); mQuotaController.saveTimingSession(0, "com.android.test.spam", @@ -1100,7 +1178,7 @@ public class QuotaControllerTest { public void testIsWithinQuotaLocked_OverDuration_OverJobCount() { setDischarging(); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; + final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25)); mQuotaController.saveTimingSession(0, "com.android.test", @@ -1141,7 +1219,7 @@ public class QuotaControllerTest { advanceElapsedClock(10 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS); assertEquals(2, mQuotaController.getExecutionStatsLocked( - SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInAllowedTime); + SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInRateLimitingWindow); assertTrue(mQuotaController.isWithinQuotaLocked(jobStatus)); } @@ -1212,9 +1290,9 @@ public class QuotaControllerTest { mQuotaController.getTimingSessions(SOURCE_USER_ID, fgChangerPkgName).size()); for (int i = ACTIVE_INDEX; i < RARE_INDEX; ++i) { assertEquals(42, mQuotaController.getExecutionStatsLocked( - SOURCE_USER_ID, fgChangerPkgName, i).jobCountInAllowedTime); + SOURCE_USER_ID, fgChangerPkgName, i).jobCountInRateLimitingWindow); assertEquals(1, mQuotaController.getExecutionStatsLocked( - SOURCE_USER_ID, unaffectedPkgName, i).jobCountInAllowedTime); + SOURCE_USER_ID, unaffectedPkgName, i).jobCountInRateLimitingWindow); } } @@ -1555,26 +1633,29 @@ public class QuotaControllerTest { } @Test - public void testMaybeScheduleStartAlarmLocked_JobCount_AllowedTime() { + public void testMaybeScheduleStartAlarmLocked_JobCount_RateLimitingWindow() { + // Set rate limiting period different from allowed time to confirm code sets based on + // the former. + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS = 10 * MINUTE_IN_MILLIS; + mQcConstants.RATE_LIMITING_WINDOW_MS = 5 * MINUTE_IN_MILLIS; + mQcConstants.updateConstants(); + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); final int standbyBucket = WORKING_INDEX; ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - stats.jobCountInAllowedTime = - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME + 2; + stats.jobCountInRateLimitingWindow = + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW + 2; // Invalid time in the past, so the count shouldn't be used. - stats.jobCountExpirationTimeElapsed = - now - mQuotaController.getAllowedTimePerPeriodMs() / 2; + stats.jobRateLimitExpirationTimeElapsed = now - 5 * MINUTE_IN_MILLIS / 2; mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - // Invalid time in the future, so the count should be used. - stats.jobCountExpirationTimeElapsed = - now + mQuotaController.getAllowedTimePerPeriodMs() / 2; - final long expectedWorkingAlarmTime = - stats.jobCountExpirationTimeElapsed + mQuotaController.getAllowedTimePerPeriodMs(); + // Valid time in the future, so the count should be used. + stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2; + final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed; mQuotaController.maybeScheduleStartAlarmLocked( SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); verify(mAlarmManager, times(1)) @@ -1732,12 +1813,13 @@ public class QuotaControllerTest { mQcConstants.MAX_JOB_COUNT_WORKING = 4000; mQcConstants.MAX_JOB_COUNT_FREQUENT = 3000; mQcConstants.MAX_JOB_COUNT_RARE = 2000; - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 500; + mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * MINUTE_IN_MILLIS; + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 500; mQcConstants.MAX_SESSION_COUNT_ACTIVE = 500; mQcConstants.MAX_SESSION_COUNT_WORKING = 400; mQcConstants.MAX_SESSION_COUNT_FREQUENT = 300; mQcConstants.MAX_SESSION_COUNT_RARE = 200; - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 50; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 50; mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 10 * SECOND_IN_MILLIS; mQcConstants.updateConstants(); @@ -1750,12 +1832,13 @@ public class QuotaControllerTest { mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]); assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs()); - assertEquals(500, mQuotaController.getMaxJobCountPerAllowedTime()); + assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getRateLimitingWindowMs()); + assertEquals(500, mQuotaController.getMaxJobCountPerRateLimitingWindow()); assertEquals(5000, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]); assertEquals(4000, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]); assertEquals(3000, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]); assertEquals(2000, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]); - assertEquals(50, mQuotaController.getMaxSessionCountPerAllowedTime()); + assertEquals(50, mQuotaController.getMaxSessionCountPerRateLimitingWindow()); assertEquals(500, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]); assertEquals(400, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]); assertEquals(300, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]); @@ -1778,12 +1861,13 @@ public class QuotaControllerTest { mQcConstants.MAX_JOB_COUNT_WORKING = 1; mQcConstants.MAX_JOB_COUNT_FREQUENT = 1; mQcConstants.MAX_JOB_COUNT_RARE = 1; - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 0; + mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * SECOND_IN_MILLIS; + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 0; mQcConstants.MAX_SESSION_COUNT_ACTIVE = -1; - mQcConstants.MAX_SESSION_COUNT_WORKING = 1; - mQcConstants.MAX_SESSION_COUNT_FREQUENT = 2; - mQcConstants.MAX_SESSION_COUNT_RARE = 1; - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 0; + mQcConstants.MAX_SESSION_COUNT_WORKING = 0; + mQcConstants.MAX_SESSION_COUNT_FREQUENT = -3; + mQcConstants.MAX_SESSION_COUNT_RARE = 0; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 0; mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = -1; mQcConstants.updateConstants(); @@ -1795,16 +1879,17 @@ public class QuotaControllerTest { assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]); assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs()); - assertEquals(10, mQuotaController.getMaxJobCountPerAllowedTime()); - assertEquals(100, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]); - assertEquals(100, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]); - assertEquals(100, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]); - assertEquals(100, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]); - assertEquals(10, mQuotaController.getMaxSessionCountPerAllowedTime()); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]); - assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]); + assertEquals(30 * SECOND_IN_MILLIS, mQuotaController.getRateLimitingWindowMs()); + assertEquals(10, mQuotaController.getMaxJobCountPerRateLimitingWindow()); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]); + assertEquals(10, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]); + assertEquals(10, mQuotaController.getMaxSessionCountPerRateLimitingWindow()); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]); + assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]); assertEquals(0, mQuotaController.getTimingSessionCoalescingDurationMs()); // Test larger than a day. Controller should cap at one day. @@ -1815,6 +1900,7 @@ public class QuotaControllerTest { mQcConstants.WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS; mQcConstants.WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS; mQcConstants.MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS; + mQcConstants.RATE_LIMITING_WINDOW_MS = 25 * HOUR_IN_MILLIS; mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 25 * HOUR_IN_MILLIS; mQcConstants.updateConstants(); @@ -1826,6 +1912,7 @@ public class QuotaControllerTest { assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]); assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs()); + assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getRateLimitingWindowMs()); assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getTimingSessionCoalescingDurationMs()); } @@ -2126,7 +2213,7 @@ public class QuotaControllerTest { assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - assertEquals(0, stats.jobCountInAllowedTime); + assertEquals(0, stats.jobCountInRateLimitingWindow); setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); mQuotaController.prepareForExecutionLocked(jobFg1); @@ -2138,7 +2225,7 @@ public class QuotaControllerTest { mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false); assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); - assertEquals(0, stats.jobCountInAllowedTime); + assertEquals(0, stats.jobCountInRateLimitingWindow); } /** @@ -2154,7 +2241,7 @@ public class QuotaControllerTest { ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - assertEquals(0, stats.jobCountInAllowedTime); + assertEquals(0, stats.jobCountInRateLimitingWindow); setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); mQuotaController.prepareForExecutionLocked(jobBg1); @@ -2165,7 +2252,7 @@ public class QuotaControllerTest { advanceElapsedClock(10 * SECOND_IN_MILLIS); mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); - assertEquals(2, stats.jobCountInAllowedTime); + assertEquals(2, stats.jobCountInRateLimitingWindow); } /** @@ -2421,10 +2508,10 @@ public class QuotaControllerTest { /** * Tests that the start alarm is properly scheduled when a job has been throttled due to the job - * count quota. + * count rate limiting. */ @Test - public void testStartAlarmScheduled_JobCount_AllowedTime() { + public void testStartAlarmScheduled_JobCount_RateLimitingWindow() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // because it schedules an alarm too. Prevent it from doing so. spyOn(mQuotaController); @@ -2432,7 +2519,7 @@ public class QuotaControllerTest { // Essentially disable session throttling. mQcConstants.MAX_SESSION_COUNT_WORKING = - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE; mQcConstants.updateConstants(); final int standbyBucket = WORKING_INDEX; @@ -2444,7 +2531,7 @@ public class QuotaControllerTest { verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Ran jobs up to the job limit. All of them should be allowed to run. - for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; ++i) { + for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { JobStatus job = createJobStatus("testStartAlarmScheduled_JobCount_AllowedTime", i); setStandbyBucket(WORKING_INDEX, job); mQuotaController.maybeStartTrackingJobLocked(job, null); @@ -2466,18 +2553,17 @@ public class QuotaControllerTest { ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - final long expectedWorkingAlarmTime = - stats.jobCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS; + final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed; verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); } /** - * Tests that the start alarm is properly scheduled when a job has been throttled due to the job - * count quota. + * Tests that the start alarm is properly scheduled when a job has been throttled due to the + * session count rate limiting. */ @Test - public void testStartAlarmScheduled_TimingSessionCount_AllowedTime() { + public void testStartAlarmScheduled_TimingSessionCount_RateLimitingWindow() { // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests // because it schedules an alarm too. Prevent it from doing so. spyOn(mQuotaController); @@ -2485,10 +2571,10 @@ public class QuotaControllerTest { // Essentially disable job count throttling. mQcConstants.MAX_JOB_COUNT_FREQUENT = - mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE; - // Make sure throttling is because of COUNT_PER_ALLOWED_TIME. + mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE; + // Make sure throttling is because of COUNT_PER_RATE_LIMITING_WINDOW. mQcConstants.MAX_SESSION_COUNT_FREQUENT = - mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME + 1; + mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW + 1; mQcConstants.updateConstants(); final int standbyBucket = FREQUENT_INDEX; @@ -2500,7 +2586,7 @@ public class QuotaControllerTest { verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); // Ran jobs up to the job limit. All of them should be allowed to run. - for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME; ++i) { + for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { JobStatus job = createJobStatus( "testStartAlarmScheduled_TimingSessionCount_AllowedTime", i); setStandbyBucket(FREQUENT_INDEX, job); @@ -2515,7 +2601,7 @@ public class QuotaControllerTest { // Start alarm shouldn't have been scheduled since the app was in quota up until this point. verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); - // The app is now out of job count quota + // The app is now out of session count quota JobStatus throttledJob = createJobStatus( "testStartAlarmScheduled_TimingSessionCount_AllowedTime", 42); mQuotaController.maybeStartTrackingJobLocked(throttledJob, null); @@ -2523,8 +2609,7 @@ public class QuotaControllerTest { ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); - final long expectedWorkingAlarmTime = - stats.sessionCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS; + final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed; verify(mAlarmManager, times(1)) .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any()); }