• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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