From a57583df65bce8789dcb580fa540c37257f5e737 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Fri, 26 May 2023 21:46:39 +0000 Subject: [PATCH] Add app-level timeout & ANR limits. If an app's jobs keep timing out, then the app potentially has a bug. Timeouts resulted in backoff of the individual job that timed out, but didn't affect other jobs. This meant that apps could bypass backoff by scheduling one-off jobs instead of rescheduling using the return parameter to onStopJob. Now, JobScheduler will track the number of job timeouts across all of the app's jobs and reduce the app's max job time if it exceeds a threshold. If the total number of timeouts exceeds another limit, JobScheduler will consider the app buggy and attempt to put the app in the RESTRICTED bucket. Similarly, if an app keeps ANRing from JobScheduler's perspective, it will consider the app buggy and attempt to put it in the RESTRICTED bucket. When an unexempted app is considered buggy, don't treat any of its jobs as in the ACTIVE unless the app is current active. Also, reduce the delay for forcing an app in the RESTRICTED bucket (eg. when the app is buggy) to 1 hour. The normal bucket timeout continues to be 8 days. Bug: 284080732 Test: atest AppStandbyControllerTests Test: atest FrameworksMockingServicesTests:JobSchedulerServiceTest Test: atest FrameworksMockingServicesTests:JobStatusTest Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job Change-Id: I8eb333a922658ba6137012dffd7a143cd990376d --- .../server/job/JobSchedulerInternal.java | 4 + .../server/job/JobSchedulerService.java | 317 +++++++++- .../server/job/controllers/JobStatus.java | 75 ++- .../job/controllers/QuotaController.java | 8 + .../server/usage/AppStandbyController.java | 2 +- .../server/job/JobSchedulerServiceTest.java | 568 +++++++++++++++++- .../ConnectivityControllerTest.java | 5 + .../server/job/controllers/JobStatusTest.java | 37 ++ .../controllers/PrefetchControllerTest.java | 4 + .../job/controllers/QuotaControllerTest.java | 3 + 10 files changed, 1000 insertions(+), 23 deletions(-) diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java index fd8ddbcf38094..6c8af39015f5d 100644 --- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java @@ -59,6 +59,10 @@ public interface JobSchedulerInternal { */ void reportAppUsage(String packageName, int userId); + /** @return {@code true} if the app is considered buggy from JobScheduler's perspective. */ + boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName, + int timeoutBlameUserId, @NonNull String timeoutBlamePackageName); + /** * @return {@code true} if the given notification is associated with any user-initiated jobs. */ diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 4f5eb37d871d8..965dda7acec96 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -322,16 +322,25 @@ public class JobSchedulerService extends com.android.server.SystemService 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 String QUOTA_TRACKER_TIMEOUT_UIJ_TAG = "timeout-uij"; + private static final String QUOTA_TRACKER_TIMEOUT_EJ_TAG = "timeout-ej"; + private static final String QUOTA_TRACKER_TIMEOUT_REG_TAG = "timeout-reg"; + private static final String QUOTA_TRACKER_TIMEOUT_TOTAL_TAG = "timeout-total"; + private static final String QUOTA_TRACKER_ANR_TAG = "anr"; 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; - }; + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ = + new Category(QUOTA_TRACKER_TIMEOUT_UIJ_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ = + new Category(QUOTA_TRACKER_TIMEOUT_EJ_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_REG = + new Category(QUOTA_TRACKER_TIMEOUT_REG_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL = + new Category(QUOTA_TRACKER_TIMEOUT_TOTAL_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_ANR = new Category(QUOTA_TRACKER_ANR_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_DISABLED = new Category("disabled"); /** * Queue of pending jobs. The JobServiceContext class will receive jobs from this list @@ -493,10 +502,18 @@ public class JobSchedulerService extends com.android.server.SystemService } switch (name) { case Constants.KEY_ENABLE_API_QUOTAS: + case Constants.KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC: case Constants.KEY_API_QUOTA_SCHEDULE_COUNT: case Constants.KEY_API_QUOTA_SCHEDULE_WINDOW_MS: case Constants.KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT: case Constants.KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS: if (!apiQuotaScheduleUpdated) { mConstants.updateApiQuotaConstantsLocked(); updateQuotaTracker(); @@ -583,10 +600,26 @@ public class JobSchedulerService extends com.android.server.SystemService @VisibleForTesting void updateQuotaTracker() { - mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); + mQuotaTracker.setEnabled( + mConstants.ENABLE_API_QUOTAS || mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC); mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_REG, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_ANR, + mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS); } /** @@ -616,6 +649,8 @@ public class JobSchedulerService extends com.android.server.SystemService "conn_low_signal_strength_relax_frac"; private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = "prefetch_force_batch_relax_threshold_ms"; + // This has been enabled for 3+ full releases. We're unlikely to disable it. + // TODO(141645789): remove this flag private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas"; private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count"; private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms"; @@ -623,6 +658,22 @@ public class JobSchedulerService extends com.android.server.SystemService "aq_schedule_throw_exception"; private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; + private static final String KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC = + "enable_execution_safeguards_udc"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = + "es_u_timeout_uij_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = + "es_u_timeout_ej_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = + "es_u_timeout_reg_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = + "es_u_timeout_total_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + "es_u_timeout_window_ms"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = + "es_u_anr_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + "es_u_anr_window_ms"; private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = "runtime_free_quota_max_limit_ms"; @@ -662,6 +713,17 @@ public class JobSchedulerService extends com.android.server.SystemService 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; + private static final boolean DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + // EJs have a shorter timeout, so set a higher limit for them to start with. + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 5; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 3; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = 10; + private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + 24 * HOUR_IN_MILLIS; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = 3; + private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + 6 * HOUR_IN_MILLIS; @VisibleForTesting public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS; @VisibleForTesting @@ -774,6 +836,55 @@ public class JobSchedulerService extends com.android.server.SystemService public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; + /** + * Whether to enable the execution safeguards added in UDC. + */ + public boolean ENABLE_EXECUTION_SAFEGUARDS_UDC = DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC; + /** + * The maximum number of times an app can have a user-iniated job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT; + /** + * The maximum number of times an app can have an expedited job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT; + /** + * The maximum number of times an app can have a regular job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT; + /** + * The maximum number of times an app can have jobs time out before the system + * attempts to restrict most of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT; + /** + * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT}, + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT}, + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT}, and + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT} should be evaluated over. + */ + public long EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS; + + /** + * The maximum number of times an app can ANR from JobScheduler's perspective before + * JobScheduler will attempt to restrict the app. + */ + public int EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT; + /** + * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_ANR_COUNT} + * should be evaluated over. + */ + public long EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS; + /** The maximum amount of time we will let a job run for when quota is "free". */ public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; @@ -915,6 +1026,9 @@ public class JobSchedulerService extends com.android.server.SystemService private void updateApiQuotaConstantsLocked() { ENABLE_API_QUOTAS = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_ENABLE_API_QUOTAS, DEFAULT_ENABLE_API_QUOTAS); + ENABLE_EXECUTION_SAFEGUARDS_UDC = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC); // Set a minimum value on the quota limit so it's not so low that it interferes with // legitimate use cases. API_QUOTA_SCHEDULE_COUNT = Math.max(250, @@ -931,6 +1045,40 @@ public class JobSchedulerService extends com.android.server.SystemService DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); + + // Set a minimum value on the timeout limit so it's not so low that it interferes with + // legitimate use cases. + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT)); + final int highestTimeoutCount = Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = Math.max(highestTimeoutCount, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = Math.max(1, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT)); + EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS); } private void updateRuntimeConstantsLocked() { @@ -1029,6 +1177,23 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); + pw.print(KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, ENABLE_EXECUTION_SAFEGUARDS_UDC) + .println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + EXECUTION_SAFEGUARDS_UDC_ANR_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS, + EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS).println(); + pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) @@ -2250,12 +2415,52 @@ 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, QUOTA_CATEGORIZER); - mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, - mConstants.API_QUOTA_SCHEDULE_COUNT, - mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + + final Categorizer quotaCategorizer = (userId, packageName, tag) -> { + if (QUOTA_TRACKER_TIMEOUT_UIJ_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_EJ_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_REG_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_REG + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_TOTAL_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_ANR_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_ANR + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) { + return mConstants.ENABLE_API_QUOTAS + ? QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_SCHEDULE_LOGGED.equals(tag)) { + return mConstants.ENABLE_API_QUOTAS + ? QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + Slog.wtf(TAG, "Unexpected category tag: " + tag); + return QUOTA_TRACKER_CATEGORY_DISABLED; + }; + mQuotaTracker = new CountQuotaTracker(context, quotaCategorizer); + updateQuotaTracker(); // Log at most once per minute. + // Set outside updateQuotaTracker() since this is intentionally not configurable. mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_DISABLED, Integer.MAX_VALUE, 60_000); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); mAppStandbyInternal.addListener(mStandbyTracker); @@ -2760,6 +2965,48 @@ public class JobSchedulerService extends com.android.server.SystemService 0 /* Reset cumulativeExecutionTime because of successful execution */); } + @VisibleForTesting + void maybeProcessBuggyJob(@NonNull JobStatus jobStatus, int debugStopReason) { + boolean jobTimedOut = debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT; + // If madeActive = 0, the job never actually started. + if (!jobTimedOut && jobStatus.madeActive > 0) { + final long executionDurationMs = sUptimeMillisClock.millis() - jobStatus.madeActive; + // The debug reason may be different if we stopped the job for some other reason + // (eg. constraints), so look at total execution time to be safe. + if (jobStatus.startedAsUserInitiatedJob) { + // TODO: factor in different min guarantees for different UI job types + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + } else if (jobStatus.startedAsExpeditedJob) { + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + } else { + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_GUARANTEE_MS; + } + } + if (jobTimedOut) { + final int userId = jobStatus.getTimeoutBlameUserId(); + final String pkg = jobStatus.getTimeoutBlamePackageName(); + mQuotaTracker.noteEvent(userId, pkg, + jobStatus.startedAsUserInitiatedJob + ? QUOTA_TRACKER_TIMEOUT_UIJ_TAG + : (jobStatus.startedAsExpeditedJob + ? QUOTA_TRACKER_TIMEOUT_EJ_TAG + : QUOTA_TRACKER_TIMEOUT_REG_TAG)); + if (!mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_TIMEOUT_TOTAL_TAG)) { + mAppStandbyInternal.restrictApp( + pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); + } + } + + if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_ANR) { + final int callingUserId = jobStatus.getUserId(); + final String callingPkg = jobStatus.getServiceComponent().getPackageName(); + if (!mQuotaTracker.noteEvent(callingUserId, callingPkg, QUOTA_TRACKER_ANR_TAG)) { + mAppStandbyInternal.restrictApp(callingPkg, callingUserId, + UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); + } + } + } + // JobCompletedListener implementations. /** @@ -2782,6 +3029,8 @@ public class JobSchedulerService extends com.android.server.SystemService mLastCompletedJobTimeElapsed[mLastCompletedJobIndex] = sElapsedRealtimeClock.millis(); mLastCompletedJobIndex = (mLastCompletedJobIndex + 1) % NUM_COMPLETED_JOB_HISTORY; + maybeProcessBuggyJob(jobStatus, debugStopReason); + if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_UNINSTALL || debugStopReason == JobParameters.INTERNAL_STOP_REASON_DATA_CLEARED) { // The job should have already been cleared from the rest of the JS tracking. No need @@ -3509,26 +3758,36 @@ public class JobSchedulerService extends com.android.server.SystemService if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( job.getSourceUid(), job.getSourcePackageName())) { + // The calling package is the one doing the work, so use it in the + // timeout quota checks. + final boolean isWithinTimeoutQuota = mQuotaTracker.isWithinQuota( + job.getTimeoutBlameUserId(), job.getTimeoutBlamePackageName(), + QUOTA_TRACKER_TIMEOUT_UIJ_TAG); + final long upperLimitMs = isWithinTimeoutQuota + ? mConstants.RUNTIME_UI_LIMIT_MS + : mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; if (job.getJob().getRequiredNetwork() != null) { // User-initiated data transfers. if (mConstants.RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS) { final long estimatedTransferTimeMs = mConnectivityController.getEstimatedTransferTimeMs(job); if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) { - return mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS; + return Math.min(upperLimitMs, + mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS); } // Try to give the job at least as much time as we think the transfer // will take, but cap it at the maximum limit. final long factoredTransferTimeMs = (long) (estimatedTransferTimeMs * mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR); - return Math.min(mConstants.RUNTIME_UI_LIMIT_MS, + return Math.min(upperLimitMs, Math.max(factoredTransferTimeMs, mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS)); } - return Math.max(mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, - mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS); + return Math.min(upperLimitMs, + Math.max(mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS)); } - return mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + return Math.min(upperLimitMs, mConstants.RUNTIME_MIN_UI_GUARANTEE_MS); } else if (job.shouldTreatAsExpeditedJob()) { // Don't guarantee RESTRICTED jobs more than 5 minutes. return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX @@ -3545,13 +3804,24 @@ public class JobSchedulerService extends com.android.server.SystemService synchronized (mLock) { if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( - job.getSourceUid(), job.getSourcePackageName())) { + job.getSourceUid(), job.getSourcePackageName()) + && mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(), + job.getTimeoutBlamePackageName(), + QUOTA_TRACKER_TIMEOUT_UIJ_TAG)) { return mConstants.RUNTIME_UI_LIMIT_MS; } if (job.shouldTreatAsUserInitiatedJob()) { return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; } - return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + // Only let the app use the higher runtime if it hasn't repeatedly timed out. + final String timeoutTag = job.shouldTreatAsExpeditedJob() + ? QUOTA_TRACKER_TIMEOUT_EJ_TAG : QUOTA_TRACKER_TIMEOUT_REG_TAG; + final long upperLimitMs = + mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(), + job.getTimeoutBlamePackageName(), timeoutTag) + ? mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS + : mConstants.RUNTIME_MIN_GUARANTEE_MS; + return Math.min(upperLimitMs, mConstants.USE_TARE_POLICY ? mTareController.getMaxJobExecutionTimeMsLocked(job) : mQuotaController.getMaxJobExecutionTimeMsLocked(job)); @@ -3794,6 +4064,17 @@ public class JobSchedulerService extends com.android.server.SystemService JobSchedulerService.this.reportAppUsage(packageName, userId); } + @Override + public boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName, + int timeoutBlameUserId, @NonNull String timeoutBlamePackageName) { + return !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName, + QUOTA_TRACKER_ANR_TAG) + || !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName, + QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG) + || !mQuotaTracker.isWithinQuota(timeoutBlameUserId, timeoutBlamePackageName, + QUOTA_TRACKER_TIMEOUT_TOTAL_TAG); + } + @Override public boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId, int userId, @NonNull String packageName) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index a8b4c695889c5..831ddc379167d 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -1024,13 +1024,77 @@ public final class JobStatus { return UserHandle.getUserId(callingUid); } + private boolean shouldBlameSourceForTimeout() { + // If the system scheduled the job on behalf of an app, assume the app is the one + // doing the work and blame the app directly. This is the case with things like + // syncs via SyncManager. + // If the system didn't schedule the job on behalf of an app, then + // blame the app doing the actual work. Proxied jobs are a little tricky. + // Proxied jobs scheduled by built-in system apps like DownloadManager may be fine + // and we could consider exempting those jobs. For example, in DownloadManager's + // case, all it does is download files and the code is vetted. A timeout likely + // means it's downloading a large file, which isn't an error. For now, DownloadManager + // is an exempted app, so this shouldn't be an issue. + // However, proxied jobs coming from other system apps (such as those that can + // be updated separately from an OTA) may not be fine and we would want to apply + // this policy to those jobs/apps. + // TODO(284512488): consider exempting DownloadManager or other system apps + return UserHandle.isCore(callingUid); + } + + /** + * Returns the package name that should most likely be blamed for the job timing out. + */ + public String getTimeoutBlamePackageName() { + if (shouldBlameSourceForTimeout()) { + return sourcePackageName; + } + return getServiceComponent().getPackageName(); + } + + /** + * Returns the UID that should most likely be blamed for the job timing out. + */ + public int getTimeoutBlameUid() { + if (shouldBlameSourceForTimeout()) { + return sourceUid; + } + return callingUid; + } + + /** + * Returns the userId that should most likely be blamed for the job timing out. + */ + public int getTimeoutBlameUserId() { + if (shouldBlameSourceForTimeout()) { + return sourceUserId; + } + return UserHandle.getUserId(callingUid); + } + /** * Returns an appropriate standby bucket for the job, taking into account any standby * exemptions. */ public int getEffectiveStandbyBucket() { + final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class); + final boolean isBuggy = jsi.isAppConsideredBuggy( + getUserId(), getServiceComponent().getPackageName(), + getTimeoutBlameUserId(), getTimeoutBlamePackageName()); + final int actualBucket = getStandbyBucket(); if (actualBucket == EXEMPTED_INDEX) { + // EXEMPTED apps always have their jobs exempted, even if they're buggy, because the + // user has explicitly told the system to avoid restricting the app for power reasons. + if (isBuggy) { + final String pkg; + if (getServiceComponent().getPackageName().equals(sourcePackageName)) { + pkg = sourcePackageName; + } else { + pkg = getServiceComponent().getPackageName() + "/" + sourcePackageName; + } + Slog.w(TAG, "Exempted app " + pkg + " considered buggy"); + } return actualBucket; } if (uidActive || getJob().isExemptedFromAppStandby()) { @@ -1038,13 +1102,18 @@ public final class JobStatus { // like other ACTIVE apps. return ACTIVE_INDEX; } + // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket + // (potentially because it's used frequently by the user), limit its effective bucket + // so that it doesn't get to run as much as a normal ACTIVE app. + final int highestBucket = isBuggy ? WORKING_INDEX : ACTIVE_INDEX; if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX && mHasMediaBackupExemption) { - // Cap it at WORKING_INDEX as media back up jobs are important to the user, and the + // Treat it as if it's at least WORKING_INDEX since media backup jobs are important + // to the user, and the // source package may not have been used directly in a while. - return Math.min(WORKING_INDEX, actualBucket); + return Math.max(highestBucket, Math.min(WORKING_INDEX, actualBucket)); } - return actualBucket; + return Math.max(highestBucket, actualBucket); } /** Returns the real standby bucket of the job. */ diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 07958dd0fef50..1c29982dbd489 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -773,6 +773,14 @@ public final class QuotaController extends StateController { // If quota is currently "free", then the job can run for the full amount of time, // regardless of bucket (hence using charging instead of isQuotaFreeLocked()). if (mService.isBatteryCharging() + // The top and foreground cases here were added because apps in those states + // aren't really restricted and the work could be something the user is + // waiting for. Now that user-initiated jobs are a defined concept, we may + // not need these exemptions as much. However, UIJs are currently limited + // (as of UDC) to data transfer work. There may be other work that could + // rely on this exception. Once we add more UIJ types, we can re-evaluate + // the need for these exceptions. + // TODO: re-evaluate the need for these exceptions || mTopAppCache.get(jobStatus.getSourceUid()) || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) { diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 55e681521048b..7d3837786be9c 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -3025,7 +3025,7 @@ public class AppStandbyController public static final long DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT = COMPRESS_TIME ? ONE_MINUTE : 30 * ONE_MINUTE; public static final long DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS = - COMPRESS_TIME ? ONE_MINUTE : ONE_DAY; + COMPRESS_TIME ? ONE_MINUTE : ONE_HOUR; public static final boolean DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = true; public static final long DEFAULT_BROADCAST_RESPONSE_WINDOW_DURATION_MS = 2 * ONE_MINUTE; diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index c5ff8cc7b0d66..dd23d9f006e32 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -28,6 +28,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.RARE_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sUptimeMillisClock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -148,6 +149,9 @@ public class JobSchedulerServiceTest { // Used in JobConcurrencyManager. doReturn(mock(UserManagerInternal.class)) .when(() -> LocalServices.getService(UserManagerInternal.class)); + // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); // Called via IdleController constructor. when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); @@ -168,6 +172,8 @@ public class JobSchedulerServiceTest { JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); + // Make sure the uptime is at least 24 hours so that tests that rely on high uptime work. + sUptimeMillisClock = getAdvancedClock(sUptimeMillisClock, 24 * HOUR_IN_MILLIS); // Called by DeviceIdlenessTracker when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class)); @@ -312,6 +318,260 @@ public class JobSchedulerServiceTest { mService.getMinJobExecutionGuaranteeMs(jobUIDT)); } + @Test + public void testGetMinJobExecutionGuaranteeMs_timeoutSafeguards_disabled() { + JobStatus jobUij = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = false; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // Safeguards disabled -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 UIJ timeouts. Safeguards disabled -> no penalties. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 EJ timeouts. Safeguards disabled -> no penalties. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 Reg timeouts. Safeguards disabled -> no penalties. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + } + + @Test + public void testGetMinJobExecutionGuaranteeMs_timeoutSafeguards_enabled() { + JobStatus jobUij = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // No timeouts -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 UIJ timeout. No execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // Not a timeout -> 1 UIJ timeout. No execution penalty yet. + jobUij.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 UIJ timeouts. Min execution penalty only for UIJs. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 EJ timeouts. Max execution penalty for EJs. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 Reg timeouts. Max execution penalty for regular jobs. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + } + @Test public void testGetMaxJobExecutionTimeMs() { JobStatus jobUIDT = createJobStatus("testGetMaxJobExecutionTimeMs", @@ -327,7 +587,7 @@ public class JobSchedulerServiceTest { doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) - .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); grantRunUserInitiatedJobsPermission(true); assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, @@ -337,6 +597,306 @@ public class JobSchedulerServiceTest { mService.getMaxJobExecutionTimeMs(jobUIDT)); } + @Test + public void testGetMaxJobExecutionTimeMs_timeoutSafeguards_disabled() { + JobStatus jobUij = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + QuotaController quotaController = mService.getQuotaController(); + spyOn(quotaController); + TareController tareController = mService.getTareController(); + spyOn(tareController); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = false; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // Safeguards disabled -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 UIJ timeouts. Safeguards disabled -> no penalties. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 EJ timeouts. Safeguards disabled -> no penalties. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 Reg timeouts. Safeguards disabled -> no penalties. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + } + + @Test + public void testGetMaxJobExecutionTimeMs_timeoutSafeguards_enabled() { + JobStatus jobUij = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + QuotaController quotaController = mService.getQuotaController(); + spyOn(quotaController); + TareController tareController = mService.getTareController(); + spyOn(tareController); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // No timeouts -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 UIJ timeout. No max execution penalty yet. + jobUij.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 UIJ timeouts. Max execution penalty only for UIJs. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 EJ timeout. No max execution penalty yet. + jobEj.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 EJ timeouts. Max execution penalty for EJs. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 reg timeout. No max execution penalty yet. + jobReg.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 Reg timeouts. Max execution penalty for regular jobs. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + } + /** * Confirm that * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)} @@ -1226,6 +1786,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1249,6 +1810,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1270,6 +1832,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1292,6 +1855,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1315,6 +1879,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(false).build(); final JobWorkItem item = new JobWorkItem.Builder().build(); @@ -1337,6 +1902,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); final JobWorkItem item = new JobWorkItem.Builder().build(); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 2180a781e4373..2b56ea8bba33b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -73,6 +73,7 @@ import android.telephony.TelephonyManager; import android.util.DataUnit; import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobSchedulerService.Constants; import com.android.server.net.NetworkPolicyManagerInternal; @@ -124,6 +125,10 @@ public class ConnectivityControllerTest { LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class); LocalServices.addService(NetworkPolicyManagerInternal.class, mNetPolicyManagerInternal); + // Used in JobStatus. + LocalServices.removeServiceForTest(JobSchedulerInternal.class); + LocalServices.addService(JobSchedulerInternal.class, mock(JobSchedulerInternal.class)); + // Freeze the clocks at this moment in time JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index 05780ebe6c4be..1de7e37191121 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -21,6 +21,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; import static com.android.server.job.JobSchedulerService.NEVER_INDEX; import static com.android.server.job.JobSchedulerService.RARE_INDEX; @@ -45,6 +46,8 @@ import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.when; @@ -581,6 +584,40 @@ public class JobStatusTest { assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority()); } + @Test + public void testGetEffectiveStandbyBucket_buggyApp() { + when(mJobSchedulerInternal.isAppConsideredBuggy( + anyInt(), anyString(), anyInt(), anyString())) + .thenReturn(true); + + final JobInfo jobInfo = new JobInfo.Builder(1234, TEST_JOB_COMPONENT).build(); + JobStatus job = createJobStatus(jobInfo); + + // Exempt apps be exempting. + job.setStandbyBucket(EXEMPTED_INDEX); + assertEquals(EXEMPTED_INDEX, job.getEffectiveStandbyBucket()); + + // Actual bucket is higher than the buggy cap, so the cap comes into effect. + job.setStandbyBucket(ACTIVE_INDEX); + assertEquals(WORKING_INDEX, job.getEffectiveStandbyBucket()); + + // Buckets at the cap or below shouldn't be affected. + job.setStandbyBucket(WORKING_INDEX); + assertEquals(WORKING_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(FREQUENT_INDEX); + assertEquals(FREQUENT_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(RARE_INDEX); + assertEquals(RARE_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(RESTRICTED_INDEX); + assertEquals(RESTRICTED_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(NEVER_INDEX); + assertEquals(NEVER_INDEX, job.getEffectiveStandbyBucket()); + } + @Test public void testModifyingInternalFlags() { final JobInfo jobInfo = diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java index fb59ea2bb63bc..7cc01e1b42925 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java @@ -58,6 +58,7 @@ import android.util.SparseArray; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.controllers.PrefetchController.PcConstants; @@ -135,6 +136,9 @@ public class PrefetchControllerTest { when(mJobSchedulerService.getPackagesForUidLocked(anyInt())) .thenAnswer(invocationOnMock -> mPackagesForUid.get(invocationOnMock.getArgument(0))); + // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions // in the past, and PrefetchController sometimes floors values at 0, so if the test time 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 6f713e0fad64c..dce162c58d0ba 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 @@ -85,6 +85,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobStore; import com.android.server.job.controllers.QuotaController.ExecutionStats; @@ -190,6 +191,8 @@ public class QuotaControllerTest { doReturn(mPowerAllowlistInternal) .when(() -> LocalServices.getService(PowerAllowlistInternal.class)); // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); doReturn(mPackageManagerInternal) .when(() -> LocalServices.getService(PackageManagerInternal.class)); // Used in QuotaController.Handler.