1 /* 2 * Copyright (C) 2023 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.service.adselection; 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.net.Uri; 22 23 import com.android.adservices.LoggerFactory; 24 import com.android.adservices.concurrency.AdServicesExecutors; 25 import com.android.adservices.data.adselection.AdSelectionDebugReportDao; 26 import com.android.adservices.data.adselection.AdSelectionDebugReportingDatabase; 27 import com.android.adservices.data.adselection.DBAdSelectionDebugReport; 28 import com.android.adservices.service.Flags; 29 import com.android.adservices.service.FlagsFactory; 30 import com.android.adservices.service.common.SingletonRunner; 31 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient; 32 import com.android.adservices.service.devapi.DevContext; 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import com.google.common.collect.ImmutableList; 36 import com.google.common.util.concurrent.FluentFuture; 37 import com.google.common.util.concurrent.Futures; 38 import com.google.common.util.concurrent.ListenableFuture; 39 40 import java.time.Clock; 41 import java.time.Instant; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Objects; 45 import java.util.concurrent.TimeUnit; 46 import java.util.function.Supplier; 47 import java.util.stream.Collectors; 48 49 /** Worker class to send and clean debug reports generated for ad selection. */ 50 public class DebugReportSenderWorker { 51 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 52 public static final String JOB_DESCRIPTION = "Ad selection debug report sender job"; 53 private static final Object SINGLETON_LOCK = new Object(); 54 private static volatile DebugReportSenderWorker sDebugReportSenderWorker; 55 @NonNull private final AdSelectionDebugReportDao mAdSelectionDebugReportDao; 56 @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient; 57 @NonNull private final Flags mFlags; 58 @NonNull private final Clock mClock; 59 private final SingletonRunner<Void> mSingletonRunner = 60 new SingletonRunner<>(JOB_DESCRIPTION, this::doRun); 61 62 @VisibleForTesting DebugReportSenderWorker( @onNull AdSelectionDebugReportDao adSelectionDebugReportDao, @NonNull AdServicesHttpsClient adServicesHttpsClient, @NonNull Flags flags, @NonNull Clock clock)63 protected DebugReportSenderWorker( 64 @NonNull AdSelectionDebugReportDao adSelectionDebugReportDao, 65 @NonNull AdServicesHttpsClient adServicesHttpsClient, 66 @NonNull Flags flags, 67 @NonNull Clock clock) { 68 Objects.requireNonNull(adSelectionDebugReportDao); 69 Objects.requireNonNull(adServicesHttpsClient); 70 Objects.requireNonNull(flags); 71 Objects.requireNonNull(clock); 72 73 mAdSelectionDebugReportDao = adSelectionDebugReportDao; 74 mAdServicesHttpsClient = adServicesHttpsClient; 75 mClock = clock; 76 mFlags = flags; 77 } 78 79 /** 80 * Gets an instance of a {@link DebugReportSenderWorker}. If an instance hasn't been 81 * initialized, a new singleton will be created and returned. 82 */ 83 @NonNull getInstance(@onNull Context context)84 public static DebugReportSenderWorker getInstance(@NonNull Context context) { 85 Objects.requireNonNull(context); 86 87 if (sDebugReportSenderWorker == null) { 88 synchronized (SINGLETON_LOCK) { 89 if (sDebugReportSenderWorker == null) { 90 AdSelectionDebugReportDao adSelectionDebugReportDao = 91 AdSelectionDebugReportingDatabase.getInstance(context) 92 .getAdSelectionDebugReportDao(); 93 Flags flags = FlagsFactory.getFlags(); 94 AdServicesHttpsClient adServicesHttpsClient = 95 new AdServicesHttpsClient( 96 AdServicesExecutors.getBlockingExecutor(), 97 flags.getFledgeDebugReportSenderJobNetworkConnectionTimeoutMs(), 98 flags.getFledgeDebugReportSenderJobNetworkReadTimeoutMs(), 99 AdServicesHttpsClient.DEFAULT_MAX_BYTES); 100 sDebugReportSenderWorker = 101 new DebugReportSenderWorker( 102 adSelectionDebugReportDao, 103 adServicesHttpsClient, 104 flags, 105 Clock.systemUTC()); 106 } 107 } 108 } 109 return sDebugReportSenderWorker; 110 } 111 112 /** 113 * Runs the debug report sender job for Ad Selection Debug Reports. 114 * 115 * @return A future to be used to check when the task has completed. 116 */ runDebugReportSender()117 public FluentFuture<Void> runDebugReportSender() { 118 sLogger.d("Starting %s", JOB_DESCRIPTION); 119 return mSingletonRunner.runSingleInstance(); 120 } 121 122 /** Requests that any ongoing work be stopped gracefully and waits for work to be stopped. */ stopWork()123 public void stopWork() { 124 mSingletonRunner.stopWork(); 125 } 126 getDebugReports( @onNull Supplier<Boolean> shouldStop, @NonNull Instant jobStartTime)127 private FluentFuture<List<DBAdSelectionDebugReport>> getDebugReports( 128 @NonNull Supplier<Boolean> shouldStop, @NonNull Instant jobStartTime) { 129 if (shouldStop.get()) { 130 sLogger.d("Stopping " + JOB_DESCRIPTION); 131 return FluentFuture.from(Futures.immediateFuture(ImmutableList.of())); 132 } 133 int batchSizeForDebugReports = mFlags.getFledgeEventLevelDebugReportingMaxItemsPerBatch(); 134 sLogger.v("Getting %d debug reports from database", batchSizeForDebugReports); 135 return FluentFuture.from( 136 AdServicesExecutors.getBackgroundExecutor() 137 .submit( 138 () -> { 139 List<DBAdSelectionDebugReport> debugReports = 140 mAdSelectionDebugReportDao.getDebugReportsBeforeTime( 141 jobStartTime, batchSizeForDebugReports); 142 if (debugReports == null) { 143 sLogger.v("no debug reports to send"); 144 return Collections.emptyList(); 145 } 146 sLogger.v( 147 "found %d debug reports from database", 148 debugReports.size()); 149 return debugReports; 150 })); 151 } 152 cleanupDebugReportsData(Instant jobStartTime)153 private FluentFuture<Void> cleanupDebugReportsData(Instant jobStartTime) { 154 sLogger.v( 155 "cleaning up old debug reports from the database at time %s", 156 jobStartTime.toString()); 157 return FluentFuture.from( 158 AdServicesExecutors.getBackgroundExecutor() 159 .submit( 160 () -> { 161 mAdSelectionDebugReportDao.deleteDebugReportsBeforeTime( 162 jobStartTime); 163 return null; 164 })); 165 } 166 167 private ListenableFuture<Void> sendDebugReports( 168 @NonNull List<DBAdSelectionDebugReport> dbAdSelectionDebugReports) { 169 170 if (dbAdSelectionDebugReports.isEmpty()) { 171 sLogger.d("No debug reports found to send"); 172 return FluentFuture.from(Futures.immediateVoidFuture()); 173 } 174 175 sLogger.d("Sending %d debug reports", dbAdSelectionDebugReports.size()); 176 List<ListenableFuture<Void>> futures = 177 dbAdSelectionDebugReports.stream() 178 .map(this::sendDebugReport) 179 .collect(Collectors.toList()); 180 return Futures.whenAllComplete(futures) 181 .call(() -> null, AdServicesExecutors.getBlockingExecutor()); 182 } 183 184 private ListenableFuture<Void> sendDebugReport( 185 DBAdSelectionDebugReport dbAdSelectionDebugReport) { 186 Uri debugReportUri = dbAdSelectionDebugReport.getDebugReportUri(); 187 DevContext devContext = 188 DevContext.builder() 189 .setDevOptionsEnabled(dbAdSelectionDebugReport.getDevOptionsEnabled()) 190 .build(); 191 sLogger.v("Sending debug report %s", debugReportUri.toString()); 192 try { 193 return mAdServicesHttpsClient.getAndReadNothing(debugReportUri, devContext); 194 } catch (Exception ignored) { 195 sLogger.v("Failed to send debug report %s", debugReportUri.toString()); 196 return Futures.immediateVoidFuture(); 197 } 198 } 199 200 private FluentFuture<Void> doRun(@NonNull Supplier<Boolean> shouldStop) { 201 Instant jobStartTime = mClock.instant(); 202 return getDebugReports(shouldStop, jobStartTime) 203 .transform(this::sendDebugReports, AdServicesExecutors.getBackgroundExecutor()) 204 .transformAsync( 205 ignored -> cleanupDebugReportsData(jobStartTime), 206 AdServicesExecutors.getBackgroundExecutor()) 207 .withTimeout( 208 mFlags.getFledgeDebugReportSenderJobMaxRuntimeMs(), 209 TimeUnit.MILLISECONDS, 210 AdServicesExecutors.getScheduler()); 211 } 212 } 213