1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.shared.spe.framework; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_JOB_EXECUTION_FAILURE; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_JOB_ON_STOP_EXECUTION_FAILURE; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON; 22 import static com.android.adservices.shared.spe.JobServiceConstants.ERROR_CODE_JOB_SCHEDULER_IS_UNAVAILABLE; 23 import static com.android.adservices.shared.spe.JobServiceConstants.JOB_ENABLED_STATUS_ENABLED; 24 import static com.android.adservices.shared.spe.JobServiceConstants.SKIP_REASON_JOB_NOT_CONFIGURED; 25 import static com.android.adservices.shared.spe.framework.ExecutionResult.CANCELLED_BY_SCHEDULER; 26 import static com.android.adservices.shared.spe.framework.ExecutionResult.FAILURE_WITHOUT_RETRY; 27 import static com.android.adservices.shared.spe.framework.ExecutionResult.FAILURE_WITH_RETRY; 28 import static com.android.adservices.shared.spe.framework.ExecutionRuntimeParameters.convertJobParameters; 29 import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; 30 31 import android.annotation.Nullable; 32 import android.app.job.JobParameters; 33 import android.app.job.JobScheduler; 34 import android.app.job.JobService; 35 36 import com.android.adservices.shared.errorlogging.AdServicesErrorLogger; 37 import com.android.adservices.shared.proto.JobPolicy; 38 import com.android.adservices.shared.spe.JobUtil; 39 import com.android.adservices.shared.spe.logging.JobServiceLogger; 40 import com.android.adservices.shared.spe.scheduling.BackoffPolicy; 41 import com.android.adservices.shared.spe.scheduling.PolicyJobScheduler; 42 import com.android.adservices.shared.util.LogUtil; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import com.google.common.util.concurrent.FluentFuture; 46 import com.google.common.util.concurrent.ListenableFuture; 47 48 import java.util.Map; 49 import java.util.concurrent.CancellationException; 50 import java.util.concurrent.ConcurrentHashMap; 51 import java.util.concurrent.Executor; 52 53 /** 54 * The execution part of SPE (Scheduling Policy Engine) framework on top of Platform's {@link 55 * JobScheduler} to provide simple and reliable background job implementations. See the scheduling 56 * part in {@link PolicyJobScheduler}. 57 * 58 * <p>To onboard SPE instance for your own module, it needs to, 59 * 60 * <ul> 61 * <li>Implement this class {@link AbstractJobService} by providing a {@link JobServiceFactory} 62 * that configures specific components for your module. See details in {@link 63 * JobServiceFactory}. 64 * <li>Register your module's instance of {@link AbstractJobService} in the Manifest.xml as a 65 * service. 66 * <li>Create an instance of {@link PolicyJobScheduler} for your module and use this instance to 67 * schedule jobs in your module. 68 * <li>(Optional) Create a Flag in the flag server and point {@link 69 * JobServiceFactory#getModuleJobPolicy()} to this flag to get the encoded String, so that the 70 * {@link PolicyJobScheduler} can sync the {@link JobPolicy} from the flag server. 71 * </ul> 72 */ 73 public abstract class AbstractJobService extends JobService { 74 // Store the running listenable futures used by onStopJob() to cancel the running execution 75 // future if there is any unsatisfied constraint. Note the instance of this Service class won't 76 // be destroyed if onStartJob() hasn't finished. And in onStopJob(), the live listenable future 77 // will be cancelled. Therefore, the lifetime of the futures stored in this map should be no 78 // longer than the lifetime of this Service class. 79 protected final ConcurrentHashMap<Integer, ListenableFuture<ExecutionResult>> 80 mRunningFuturesMap = new ConcurrentHashMap<>(); 81 82 private JobServiceFactory mJobServiceFactory; 83 private JobServiceLogger mJobServiceLogger; 84 private AdServicesErrorLogger mErrorLogger; 85 private Executor mExecutor; 86 private Map<Integer, String> mJobIdToNameMap; 87 getJobServiceFactory()88 protected abstract JobServiceFactory getJobServiceFactory(); 89 90 @Override onCreate()91 public void onCreate() { 92 super.onCreate(); 93 mJobServiceFactory = getJobServiceFactory(); 94 mJobServiceLogger = mJobServiceFactory.getJobServiceLogger(); 95 mExecutor = mJobServiceFactory.getBackgroundExecutor(); 96 mJobIdToNameMap = mJobServiceFactory.getJobIdToNameMap(); 97 mErrorLogger = mJobServiceFactory.getErrorLogger(); 98 } 99 100 @Override onStartJob(JobParameters params)101 public boolean onStartJob(JobParameters params) { 102 int jobId = params.getJobId(); 103 JobUtil.logV("Starting executing onStartJob() for jobId = %d.", jobId); 104 105 mJobServiceLogger.recordOnStartJob(jobId); 106 107 String jobName = getJobName(params, jobId); 108 if (jobName == null) { 109 return false; 110 } 111 112 LogUtil.v("Running onStartJob() for %s, jobId = %d.", jobName, jobId); 113 114 JobWorker worker = getJobWorker(params, jobId, jobName); 115 if (worker == null) { 116 return false; 117 } 118 119 JobUtil.logV("Begin job execution for %s...", jobName); 120 ListenableFuture<ExecutionResult> executionFuture = mRunningFuturesMap.get(jobId); 121 // Cancel the unfinished future for the same job. This should rarely happen due to 122 // JobScheduler doesn't call onStartJob() again before the end of previous call on the same 123 // job (with the same job ID). Nevertheless, do the defensive programming here. 124 if (executionFuture != null) { 125 executionFuture.cancel(/* mayInterruptIfRunning= */ true); 126 } 127 executionFuture = worker.getExecutionFuture(this, convertJobParameters(params)); 128 mRunningFuturesMap.put(jobId, executionFuture); 129 130 // Add Logging callback. 131 // TODO(b/331285831): Standardize the usage of unused future. 132 ListenableFuture<Void> unusedFuture = 133 onJobPostExecution( 134 executionFuture, jobId, jobName, params, worker.getBackoffPolicy()); 135 136 return true; 137 } 138 139 @Override onStopJob(JobParameters params)140 public boolean onStopJob(JobParameters params) { 141 int jobId = params.getJobId(); 142 String jobName = getJobName(params, jobId); 143 144 JobUtil.logV("Running onStopJob() for %s...", jobName); 145 146 JobWorker worker = getJobWorker(params, jobId, jobName); 147 // This should never happen as onStartJob() has done the null check, but do it here to 148 // bypass the null-check warning below. 149 if (worker == null) { 150 return false; 151 } 152 153 // Cancel the running execution future. 154 ListenableFuture<ExecutionResult> executionFuture = mRunningFuturesMap.get(jobId); 155 if (executionFuture != null) { 156 executionFuture.cancel(/* mayInterruptIfRunning= */ true); 157 } 158 159 // Execute customized logic if the execution is stopped by the JobScheduler. 160 // TODO(b/326150705): Add a callback for this future and maybe log the result. 161 @SuppressWarnings("unused") 162 ListenableFuture<Void> executionStopFuture = 163 FluentFuture.from(worker.getExecutionStopFuture(this, convertJobParameters(params))) 164 .catching( 165 RuntimeException.class, 166 e -> { 167 LogUtil.e( 168 e, 169 "The customized logic in onStopJob() encounters error" 170 + " for %s!", 171 jobName); 172 mErrorLogger.logErrorWithExceptionInfo( 173 e, 174 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_JOB_ON_STOP_EXECUTION_FAILURE, 175 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 176 return null; 177 }, 178 mExecutor); 179 180 // Add logging. 181 boolean shouldReschedule = worker.getBackoffPolicy().shouldRetryOnExecutionStop(); 182 mJobServiceLogger.recordOnStopJob(params, jobId, shouldReschedule); 183 184 return shouldReschedule; 185 } 186 187 /** 188 * Skips the execution and also cancels the job. 189 * 190 * @param params the {@link JobParameters} when the execution is invoked. 191 * @param skipReason the reason to skip the current job. 192 */ 193 @VisibleForTesting(visibility = PROTECTED) skipAndCancelBackgroundJob(JobParameters params, int skipReason)194 public void skipAndCancelBackgroundJob(JobParameters params, int skipReason) { 195 mExecutor.execute( 196 () -> { 197 int jobId = params.getJobId(); 198 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 199 200 if (jobScheduler != null) { 201 jobScheduler.cancel(jobId); 202 LogUtil.d("Job %d has been cancelled.", jobId); 203 } else { 204 LogUtil.e( 205 "Cannot fetch JobScheduler! Failed to cancel %s.", 206 mJobIdToNameMap.get(jobId)); 207 mErrorLogger.logError( 208 ERROR_CODE_JOB_SCHEDULER_IS_UNAVAILABLE, 209 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 210 } 211 212 mJobServiceLogger.recordJobSkipped(jobId, skipReason); 213 214 jobFinished(params, /* wantsReschedule= */ false); 215 }); 216 } 217 218 @Nullable getJobWorker(JobParameters params, int jobId, String jobName)219 protected JobWorker getJobWorker(JobParameters params, int jobId, String jobName) { 220 JobWorker worker = mJobServiceFactory.getJobWorkerInstance(jobId); 221 if (worker == null) { 222 skipForJobInfoNotConfigured(params); 223 return null; 224 } 225 226 int isEnabled = worker.getJobEnablementStatus(); 227 if (isEnabled != JOB_ENABLED_STATUS_ENABLED) { 228 LogUtil.v("Stop execution and cancel %s due to reason %d", jobName, isEnabled); 229 230 skipAndCancelBackgroundJob(params, isEnabled); 231 return null; 232 } 233 234 return worker; 235 } 236 237 // Logs the execution stats and decides whether to retry the job. 238 @VisibleForTesting onJobPostExecution( ListenableFuture<ExecutionResult> executionFuture, int jobId, String jobName, JobParameters params, BackoffPolicy backoffPolicy)239 ListenableFuture<Void> onJobPostExecution( 240 ListenableFuture<ExecutionResult> executionFuture, 241 int jobId, 242 String jobName, 243 JobParameters params, 244 BackoffPolicy backoffPolicy) { 245 return FluentFuture.from(executionFuture) 246 // Handle execution failures. Fall back to RETRY or FAILURE based on BackoffPolicy. 247 .catching( 248 Exception.class, 249 e -> { 250 LogUtil.e(e, "Encounter Job Execution Failure for Job %s.", jobName); 251 252 if (e instanceof CancellationException) { 253 return CANCELLED_BY_SCHEDULER; 254 } 255 256 mErrorLogger.logErrorWithExceptionInfo( 257 e, 258 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SPE_JOB_EXECUTION_FAILURE, 259 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 260 261 return backoffPolicy.shouldRetryOnExecutionFailure() 262 ? FAILURE_WITH_RETRY 263 : FAILURE_WITHOUT_RETRY; 264 }, 265 mExecutor) 266 .transform( 267 executionResult -> { 268 JobUtil.logV( 269 "Job execution is finished for %s, result is %s.", 270 jobName, executionResult); 271 272 // onStopJob() handles both logging and whether to retry. Skip them 273 // here. 274 if (!executionResult.equals(CANCELLED_BY_SCHEDULER)) { 275 mJobServiceLogger.recordJobFinished(jobId, executionResult); 276 } 277 278 jobFinished(params, executionResult.equals(FAILURE_WITH_RETRY)); 279 return null; 280 }, 281 mExecutor); 282 } 283 284 @VisibleForTesting skipForJobInfoNotConfigured(JobParameters params)285 protected void skipForJobInfoNotConfigured(JobParameters params) { 286 LogUtil.e( 287 "Disabling %d job because it's not configured in JobInfo or" 288 + " JobConfig! Please check the setup.", 289 params.getJobId()); 290 291 skipAndCancelBackgroundJob(params, SKIP_REASON_JOB_NOT_CONFIGURED); 292 } 293 getJobName(JobParameters parameters, int jobId)294 private String getJobName(JobParameters parameters, int jobId) { 295 if (mJobIdToNameMap == null) { 296 skipForJobInfoNotConfigured(parameters); 297 return null; 298 } 299 300 String jobName = mJobIdToNameMap.get(jobId); 301 302 if (jobName == null) { 303 skipForJobInfoNotConfigured(parameters); 304 return null; 305 } 306 307 return jobName; 308 } 309 } 310