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