1 /*
2  * Copyright (C) 2022 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 android.adservices.ondevicepersonalization;
18 
19 import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
20 import android.adservices.ondevicepersonalization.aidl.IFederatedComputeCallback;
21 import android.adservices.ondevicepersonalization.aidl.IFederatedComputeService;
22 import android.annotation.FlaggedApi;
23 import android.annotation.NonNull;
24 import android.annotation.WorkerThread;
25 import android.federatedcompute.common.TrainingOptions;
26 import android.os.RemoteException;
27 
28 import com.android.adservices.ondevicepersonalization.flags.Flags;
29 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
30 
31 import java.util.concurrent.CountDownLatch;
32 
33 /**
34  * Handles scheduling federated compute jobs. See {@link
35  * IsolatedService#getFederatedComputeScheduler}.
36  */
37 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
38 public class FederatedComputeScheduler {
39     private static final String TAG = FederatedComputeScheduler.class.getSimpleName();
40     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
41 
42     private final IFederatedComputeService mFcService;
43     private final IDataAccessService mDataAccessService;
44 
45     /** @hide */
FederatedComputeScheduler( IFederatedComputeService binder, IDataAccessService dataService)46     public FederatedComputeScheduler(
47             IFederatedComputeService binder, IDataAccessService dataService) {
48         mFcService = binder;
49         mDataAccessService = dataService;
50     }
51 
52     // TODO(b/300461799): add federated compute server document.
53     // TODO(b/269665435): add sample code snippet.
54     /**
55      * Schedules a federated compute job. In {@link IsolatedService#onRequest}, the app can call
56      * {@link IsolatedService#getFederatedComputeScheduler} to pass scheduler when construct {@link
57      * IsolatedWorker}.
58      *
59      * @param params parameters related to job scheduling.
60      * @param input the configuration of the federated compute. It should be consistent with the
61      *     federated compute server setup.
62      */
63     @WorkerThread
schedule(@onNull Params params, @NonNull FederatedComputeInput input)64     public void schedule(@NonNull Params params, @NonNull FederatedComputeInput input) {
65         final long startTimeMillis = System.currentTimeMillis();
66         int responseCode = Constants.STATUS_INTERNAL_ERROR;
67         if (mFcService == null) {
68             throw new IllegalStateException(
69                     "FederatedComputeScheduler not available for this instance.");
70         }
71 
72         android.federatedcompute.common.TrainingInterval trainingInterval =
73                 convertTrainingInterval(params.getTrainingInterval());
74         TrainingOptions trainingOptions =
75                 new TrainingOptions.Builder()
76                         .setPopulationName(input.getPopulationName())
77                         .setTrainingInterval(trainingInterval)
78                         .build();
79         CountDownLatch latch = new CountDownLatch(1);
80         final int[] err = {0};
81         try {
82             mFcService.schedule(
83                     trainingOptions,
84                     new IFederatedComputeCallback.Stub() {
85                         @Override
86                         public void onSuccess() {
87                             latch.countDown();
88                         }
89 
90                         @Override
91                         public void onFailure(int i) {
92                             err[0] = i;
93                             latch.countDown();
94                         }
95                     });
96             latch.await();
97             if (err[0] != 0) {
98                 // Fail silently for now. TODO(b/346827691): update schedule/cancel API to return
99                 // error status to caller.
100                 sLogger.e("Internal failure occurred while scheduling job, error code %d", err[0]);
101                 responseCode = Constants.STATUS_INTERNAL_ERROR;
102                 return;
103             }
104             responseCode = Constants.STATUS_SUCCESS;
105         } catch (RemoteException | InterruptedException e) {
106             sLogger.e(TAG + ": Failed to schedule federated compute job", e);
107             throw new IllegalStateException(e);
108         } finally {
109             logApiCallStats(
110                     Constants.API_NAME_FEDERATED_COMPUTE_SCHEDULE,
111                     System.currentTimeMillis() - startTimeMillis,
112                     responseCode);
113         }
114     }
115 
116     /**
117      * Cancels a federated compute job with input training params. In {@link
118      * IsolatedService#onRequest}, the app can call {@link
119      * IsolatedService#getFederatedComputeScheduler} to pass scheduler when construct {@link
120      * IsolatedWorker}.
121      *
122      * @param input the configuration of the federated compute. It should be consistent with the
123      *     federated compute server setup.
124      */
125     @WorkerThread
cancel(@onNull FederatedComputeInput input)126     public void cancel(@NonNull FederatedComputeInput input) {
127         final long startTimeMillis = System.currentTimeMillis();
128         int responseCode = Constants.STATUS_INTERNAL_ERROR;
129         if (mFcService == null) {
130             throw new IllegalStateException(
131                     "FederatedComputeScheduler not available for this instance.");
132         }
133         CountDownLatch latch = new CountDownLatch(1);
134         final int[] err = {0};
135         try {
136             mFcService.cancel(
137                     input.getPopulationName(),
138                     new IFederatedComputeCallback.Stub() {
139                         @Override
140                         public void onSuccess() {
141                             latch.countDown();
142                         }
143 
144                         @Override
145                         public void onFailure(int i) {
146                             err[0] = i;
147                             latch.countDown();
148                         }
149                     });
150             latch.await();
151             if (err[0] != 0) {
152                 sLogger.e("Internal failure occurred while cancelling job, error code %d", err[0]);
153                 responseCode = Constants.STATUS_INTERNAL_ERROR;
154                 // Fail silently for now. TODO(b/346827691): update schedule/cancel API to return
155                 // error status to caller.
156                 return;
157             }
158             responseCode = Constants.STATUS_SUCCESS;
159         } catch (RemoteException | InterruptedException e) {
160             sLogger.e(TAG + ": Failed to cancel federated compute job", e);
161             throw new IllegalStateException(e);
162         } finally {
163             logApiCallStats(
164                     Constants.API_NAME_FEDERATED_COMPUTE_CANCEL,
165                     System.currentTimeMillis() - startTimeMillis,
166                     responseCode);
167         }
168     }
169 
convertTrainingInterval( TrainingInterval interval)170     private android.federatedcompute.common.TrainingInterval convertTrainingInterval(
171             TrainingInterval interval) {
172         return new android.federatedcompute.common.TrainingInterval.Builder()
173                 .setMinimumIntervalMillis(interval.getMinimumInterval().toMillis())
174                 .setSchedulingMode(convertSchedulingMode(interval))
175                 .build();
176     }
177 
178     private @android.federatedcompute.common.TrainingInterval.SchedulingMode int
convertSchedulingMode(TrainingInterval interval)179             convertSchedulingMode(TrainingInterval interval) {
180         switch (interval.getSchedulingMode()) {
181             case TrainingInterval.SCHEDULING_MODE_ONE_TIME:
182                 return android.federatedcompute.common.TrainingInterval.SCHEDULING_MODE_ONE_TIME;
183             case TrainingInterval.SCHEDULING_MODE_RECURRENT:
184                 return android.federatedcompute.common.TrainingInterval.SCHEDULING_MODE_RECURRENT;
185             default:
186                 throw new IllegalStateException(
187                         "Unsupported scheduling mode " + interval.getSchedulingMode());
188         }
189     }
190 
logApiCallStats(int apiName, long duration, int responseCode)191     private void logApiCallStats(int apiName, long duration, int responseCode) {
192         try {
193             mDataAccessService.logApiCallStats(apiName, duration, responseCode);
194         } catch (Exception e) {
195             sLogger.d(e, TAG + ": failed to log metrics");
196         }
197     }
198 
199     /** The parameters related to job scheduling. */
200     public static class Params {
201         /**
202          * If training interval is scheduled for recurrent tasks, the earliest time this task could
203          * start is after the minimum training interval expires. E.g. If the task is set to run
204          * maximum once per day, the first run of this task will be one day after this task is
205          * scheduled. When a one time job is scheduled, the earliest next runtime is calculated
206          * based on federated compute default interval.
207          */
208         @NonNull private final TrainingInterval mTrainingInterval;
209 
Params(@onNull TrainingInterval trainingInterval)210         public Params(@NonNull TrainingInterval trainingInterval) {
211             mTrainingInterval = trainingInterval;
212         }
213 
214         @NonNull
getTrainingInterval()215         public TrainingInterval getTrainingInterval() {
216             return mTrainingInterval;
217         }
218     }
219 }
220