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.federatedcompute.services.scheduling; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON; 20 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED; 21 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED; 22 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL; 23 24 import android.app.job.JobInfo; 25 import android.app.job.JobParameters; 26 import android.app.job.JobScheduler; 27 import android.app.job.JobService; 28 import android.content.ComponentName; 29 import android.content.Context; 30 31 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode; 32 import com.android.federatedcompute.internal.util.LogUtil; 33 import com.android.federatedcompute.services.common.FederatedComputeExecutors; 34 import com.android.federatedcompute.services.common.FederatedComputeJobInfo; 35 import com.android.federatedcompute.services.common.FederatedComputeJobUtil; 36 import com.android.federatedcompute.services.common.Flags; 37 import com.android.federatedcompute.services.common.FlagsFactory; 38 import com.android.federatedcompute.services.data.FederatedTrainingTaskDao; 39 import com.android.federatedcompute.services.data.ODPAuthorizationTokenDao; 40 import com.android.federatedcompute.services.statsd.joblogging.FederatedComputeJobServiceLogger; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.odp.module.common.Clock; 43 import com.android.odp.module.common.MonotonicClock; 44 45 import com.google.common.util.concurrent.FutureCallback; 46 import com.google.common.util.concurrent.Futures; 47 import com.google.common.util.concurrent.ListenableFuture; 48 import com.google.common.util.concurrent.ListeningExecutorService; 49 50 import java.util.List; 51 52 public class DeleteExpiredJobService extends JobService { 53 54 private static final String TAG = DeleteExpiredJobService.class.getSimpleName(); 55 56 private static final int DELETE_EXPIRED_JOB_ID = FederatedComputeJobInfo.DELETE_EXPIRED_JOB_ID; 57 58 private final Injector mInjector; 59 DeleteExpiredJobService()60 public DeleteExpiredJobService() { 61 mInjector = new Injector(); 62 } 63 64 @VisibleForTesting DeleteExpiredJobService(Injector injector)65 public DeleteExpiredJobService(Injector injector) { 66 mInjector = injector; 67 } 68 69 static class Injector { getExecutor()70 ListeningExecutorService getExecutor() { 71 return FederatedComputeExecutors.getBackgroundExecutor(); 72 } 73 getODPAuthorizationTokenDao(Context context)74 ODPAuthorizationTokenDao getODPAuthorizationTokenDao(Context context) { 75 return ODPAuthorizationTokenDao.getInstance(context); 76 } 77 getTrainingTaskDao(Context context)78 FederatedTrainingTaskDao getTrainingTaskDao(Context context) { 79 return FederatedTrainingTaskDao.getInstance(context); 80 } 81 getClock()82 Clock getClock() { 83 return MonotonicClock.getInstance(); 84 } 85 getFlags()86 Flags getFlags() { 87 return FlagsFactory.getFlags(); 88 } 89 } 90 91 @Override onStartJob(JobParameters params)92 public boolean onStartJob(JobParameters params) { 93 LogUtil.d(TAG, "DeleteExpiredJobService.onStartJob %d", params.getJobId()); 94 FederatedComputeJobServiceLogger.getInstance(this).recordOnStartJob(DELETE_EXPIRED_JOB_ID); 95 Flags flags = mInjector.getFlags(); 96 97 if (flags.getGlobalKillSwitch()) { 98 LogUtil.d(TAG, "GlobalKillSwitch is enabled, finishing job."); 99 return FederatedComputeJobUtil.cancelAndFinishJob( 100 this, 101 params, 102 DELETE_EXPIRED_JOB_ID, 103 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON); 104 } 105 106 // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this 107 // DeleteExpiredJobService will be cancelled for the same job ID. 108 // 109 // Note the job without a flex period will execute immediately after rescheduling with the 110 // same ID. Therefore, ending the execution here and let it run in the new SPE job. 111 if (flags.getSpePilotJobEnabled()) { 112 LogUtil.d( 113 TAG, 114 "SPE is enabled. Reschedule DeleteExpiredJobService with" 115 + " DeleteExpiredJob."); 116 DeleteExpiredJob.schedule(this, flags); 117 return false; 118 } 119 120 ListenableFuture<Integer> deleteExpiredAuthTokenFuture = 121 Futures.submit( 122 () -> 123 mInjector 124 .getODPAuthorizationTokenDao(this) 125 .deleteExpiredAuthorizationTokens(), 126 mInjector.getExecutor()); 127 ListenableFuture<Integer> deleteExpiredTaskHistoryFuture = 128 Futures.submit( 129 () -> { 130 long deleteTime = 131 mInjector.getClock().currentTimeMillis() 132 - flags.getTaskHistoryTtl(); 133 return mInjector 134 .getTrainingTaskDao(this) 135 .deleteExpiredTaskHistory(deleteTime); 136 }, 137 mInjector.getExecutor()); 138 ListenableFuture<List<Integer>> futuresList = 139 Futures.allAsList(deleteExpiredAuthTokenFuture, deleteExpiredTaskHistoryFuture); 140 Futures.addCallback( 141 futuresList, 142 new FutureCallback<List<Integer>>() { 143 @Override 144 public void onSuccess(List<Integer> result) { 145 LogUtil.d(TAG, "Deleted expired records %s", result.toString()); 146 boolean wantsReschedule = false; 147 FederatedComputeJobServiceLogger.getInstance(DeleteExpiredJobService.this) 148 .recordJobFinished( 149 DELETE_EXPIRED_JOB_ID, 150 /* isSuccessful= */ true, 151 wantsReschedule); 152 jobFinished(params, /* wantsReschedule= */ wantsReschedule); 153 } 154 155 @Override 156 public void onFailure(Throwable t) { 157 LogUtil.e(TAG, t, "Exception encountered when deleting expired records"); 158 boolean wantsReschedule = false; 159 FederatedComputeJobServiceLogger.getInstance(DeleteExpiredJobService.this) 160 .recordJobFinished( 161 DELETE_EXPIRED_JOB_ID, 162 /* isSuccessful= */ false, 163 wantsReschedule); 164 jobFinished(params, /* wantsReschedule= */ wantsReschedule); 165 } 166 }, 167 FederatedComputeExecutors.getLightweightExecutor()); 168 return true; 169 } 170 171 @Override onStopJob(JobParameters params)172 public boolean onStopJob(JobParameters params) { 173 LogUtil.d(TAG, "DeleteExpiredJobService.onStopJob %d", params.getJobId()); 174 boolean wantsReschedule = false; 175 FederatedComputeJobServiceLogger.getInstance(this) 176 .recordOnStopJob(params, DELETE_EXPIRED_JOB_ID, wantsReschedule); 177 return wantsReschedule; 178 } 179 180 /** Schedule the periodic deletion job if it is not scheduled. */ 181 @JobSchedulingResultCode scheduleJobIfNeeded(Context context, Flags flags, boolean forceSchedule)182 public static int scheduleJobIfNeeded(Context context, Flags flags, boolean forceSchedule) { 183 final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 184 if (jobScheduler == null) { 185 LogUtil.e(TAG, "Failed to get job scheduler from system service."); 186 return SCHEDULING_RESULT_CODE_FAILED; 187 } 188 189 final JobInfo scheduledJob = jobScheduler.getPendingJob(DELETE_EXPIRED_JOB_ID); 190 final JobInfo jobInfo = 191 new JobInfo.Builder( 192 DELETE_EXPIRED_JOB_ID, 193 new ComponentName(context, DeleteExpiredJobService.class)) 194 .setPeriodic( 195 flags.getAuthorizationTokenDeletionPeriodSeconds() 196 * 1000) // convert to milliseconds 197 .setRequiresBatteryNotLow(true) 198 .setRequiresDeviceIdle(true) 199 .setPersisted(true) 200 .build(); 201 202 if (forceSchedule || !jobInfo.equals(scheduledJob)) { 203 jobScheduler.schedule(jobInfo); 204 LogUtil.d(TAG, "Scheduled job DeleteExpiredJobService id %d", DELETE_EXPIRED_JOB_ID); 205 return SCHEDULING_RESULT_CODE_SUCCESSFUL; 206 } else { 207 LogUtil.d( 208 TAG, 209 "Already scheduled job DeleteExpiredJobService id %d", 210 DELETE_EXPIRED_JOB_ID); 211 return SCHEDULING_RESULT_CODE_SKIPPED; 212 } 213 } 214 } 215