/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.adservices.ondevicepersonalization; import android.adservices.ondevicepersonalization.aidl.IDataAccessService; import android.adservices.ondevicepersonalization.aidl.IFederatedComputeCallback; import android.adservices.ondevicepersonalization.aidl.IFederatedComputeService; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.WorkerThread; import android.federatedcompute.common.TrainingOptions; import android.os.RemoteException; import com.android.adservices.ondevicepersonalization.flags.Flags; import com.android.ondevicepersonalization.internal.util.LoggerFactory; import java.util.concurrent.CountDownLatch; /** * Handles scheduling federated compute jobs. See {@link * IsolatedService#getFederatedComputeScheduler}. */ @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED) public class FederatedComputeScheduler { private static final String TAG = FederatedComputeScheduler.class.getSimpleName(); private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); private final IFederatedComputeService mFcService; private final IDataAccessService mDataAccessService; /** @hide */ public FederatedComputeScheduler( IFederatedComputeService binder, IDataAccessService dataService) { mFcService = binder; mDataAccessService = dataService; } // TODO(b/300461799): add federated compute server document. // TODO(b/269665435): add sample code snippet. /** * Schedules a federated compute job. In {@link IsolatedService#onRequest}, the app can call * {@link IsolatedService#getFederatedComputeScheduler} to pass scheduler when construct {@link * IsolatedWorker}. * * @param params parameters related to job scheduling. * @param input the configuration of the federated compute. It should be consistent with the * federated compute server setup. */ @WorkerThread public void schedule(@NonNull Params params, @NonNull FederatedComputeInput input) { final long startTimeMillis = System.currentTimeMillis(); int responseCode = Constants.STATUS_INTERNAL_ERROR; if (mFcService == null) { throw new IllegalStateException( "FederatedComputeScheduler not available for this instance."); } android.federatedcompute.common.TrainingInterval trainingInterval = convertTrainingInterval(params.getTrainingInterval()); TrainingOptions trainingOptions = new TrainingOptions.Builder() .setPopulationName(input.getPopulationName()) .setTrainingInterval(trainingInterval) .build(); CountDownLatch latch = new CountDownLatch(1); final int[] err = {0}; try { mFcService.schedule( trainingOptions, new IFederatedComputeCallback.Stub() { @Override public void onSuccess() { latch.countDown(); } @Override public void onFailure(int i) { err[0] = i; latch.countDown(); } }); latch.await(); if (err[0] != 0) { // Fail silently for now. TODO(b/346827691): update schedule/cancel API to return // error status to caller. sLogger.e("Internal failure occurred while scheduling job, error code %d", err[0]); responseCode = Constants.STATUS_INTERNAL_ERROR; return; } responseCode = Constants.STATUS_SUCCESS; } catch (RemoteException | InterruptedException e) { sLogger.e(TAG + ": Failed to schedule federated compute job", e); throw new IllegalStateException(e); } finally { logApiCallStats( Constants.API_NAME_FEDERATED_COMPUTE_SCHEDULE, System.currentTimeMillis() - startTimeMillis, responseCode); } } /** * Cancels a federated compute job with input training params. In {@link * IsolatedService#onRequest}, the app can call {@link * IsolatedService#getFederatedComputeScheduler} to pass scheduler when construct {@link * IsolatedWorker}. * * @param input the configuration of the federated compute. It should be consistent with the * federated compute server setup. */ @WorkerThread public void cancel(@NonNull FederatedComputeInput input) { final long startTimeMillis = System.currentTimeMillis(); int responseCode = Constants.STATUS_INTERNAL_ERROR; if (mFcService == null) { throw new IllegalStateException( "FederatedComputeScheduler not available for this instance."); } CountDownLatch latch = new CountDownLatch(1); final int[] err = {0}; try { mFcService.cancel( input.getPopulationName(), new IFederatedComputeCallback.Stub() { @Override public void onSuccess() { latch.countDown(); } @Override public void onFailure(int i) { err[0] = i; latch.countDown(); } }); latch.await(); if (err[0] != 0) { sLogger.e("Internal failure occurred while cancelling job, error code %d", err[0]); responseCode = Constants.STATUS_INTERNAL_ERROR; // Fail silently for now. TODO(b/346827691): update schedule/cancel API to return // error status to caller. return; } responseCode = Constants.STATUS_SUCCESS; } catch (RemoteException | InterruptedException e) { sLogger.e(TAG + ": Failed to cancel federated compute job", e); throw new IllegalStateException(e); } finally { logApiCallStats( Constants.API_NAME_FEDERATED_COMPUTE_CANCEL, System.currentTimeMillis() - startTimeMillis, responseCode); } } private android.federatedcompute.common.TrainingInterval convertTrainingInterval( TrainingInterval interval) { return new android.federatedcompute.common.TrainingInterval.Builder() .setMinimumIntervalMillis(interval.getMinimumInterval().toMillis()) .setSchedulingMode(convertSchedulingMode(interval)) .build(); } private @android.federatedcompute.common.TrainingInterval.SchedulingMode int convertSchedulingMode(TrainingInterval interval) { switch (interval.getSchedulingMode()) { case TrainingInterval.SCHEDULING_MODE_ONE_TIME: return android.federatedcompute.common.TrainingInterval.SCHEDULING_MODE_ONE_TIME; case TrainingInterval.SCHEDULING_MODE_RECURRENT: return android.federatedcompute.common.TrainingInterval.SCHEDULING_MODE_RECURRENT; default: throw new IllegalStateException( "Unsupported scheduling mode " + interval.getSchedulingMode()); } } private void logApiCallStats(int apiName, long duration, int responseCode) { try { mDataAccessService.logApiCallStats(apiName, duration, responseCode); } catch (Exception e) { sLogger.d(e, TAG + ": failed to log metrics"); } } /** The parameters related to job scheduling. */ public static class Params { /** * If training interval is scheduled for recurrent tasks, the earliest time this task could * start is after the minimum training interval expires. E.g. If the task is set to run * maximum once per day, the first run of this task will be one day after this task is * scheduled. When a one time job is scheduled, the earliest next runtime is calculated * based on federated compute default interval. */ @NonNull private final TrainingInterval mTrainingInterval; public Params(@NonNull TrainingInterval trainingInterval) { mTrainingInterval = trainingInterval; } @NonNull public TrainingInterval getTrainingInterval() { return mTrainingInterval; } } }