• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.data.signals;
18 
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PAS_CONVERTING_UPDATE_SIGNALS_RESPONSE_TO_JSON_ERROR;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PAS_EMPTY_RESPONSE_FROM_CLIENT_DOWNLOADING_ENCODER;
21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PAS_INVALID_OR_MISSING_ENCODER_VERSION;
22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PAS_UPDATE_FOR_ENCODING_LOGIC_ON_PERSISTENCE_LAYER_FAILED;
23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__PAS;
24 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.ENCODING_FETCH_STATUS_OTHER_FAILURE;
25 
26 import android.adservices.common.AdTechIdentifier;
27 import android.content.Context;
28 import android.os.Trace;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.VisibleForTesting;
32 
33 import com.android.adservices.LoggerFactory;
34 import com.android.adservices.concurrency.AdServicesExecutors;
35 import com.android.adservices.errorlogging.ErrorLogUtil;
36 import com.android.adservices.service.Flags;
37 import com.android.adservices.service.FlagsFactory;
38 import com.android.adservices.service.common.httpclient.AdServicesHttpClientRequest;
39 import com.android.adservices.service.common.httpclient.AdServicesHttpClientResponse;
40 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
41 import com.android.adservices.service.devapi.DevContext;
42 import com.android.adservices.service.profiling.Tracing;
43 import com.android.adservices.service.stats.AdServicesLogger;
44 import com.android.adservices.service.stats.AdServicesLoggerImpl;
45 import com.android.adservices.service.stats.FetchProcessLogger;
46 import com.android.adservices.service.stats.FetchProcessLoggerNoLoggingImpl;
47 import com.android.adservices.service.stats.pas.EncodingFetchStats;
48 import com.android.adservices.service.stats.pas.EncodingJsFetchProcessLoggerImpl;
49 import com.android.adservices.shared.util.Clock;
50 
51 import com.google.common.collect.ImmutableSet;
52 import com.google.common.util.concurrent.FluentFuture;
53 import com.google.common.util.concurrent.Futures;
54 import com.google.common.util.concurrent.ListeningExecutorService;
55 
56 import java.time.Instant;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Objects;
61 import java.util.Set;
62 import java.util.concurrent.locks.ReentrantLock;
63 
64 /**
65  * Responsible for handling downloads, updates & delete for encoder logics for buyers
66  *
67  * <p>Thread safety:
68  *
69  * <ol>
70  *   <li>The updates are thread safe per buyer
71  *   <li>Updates for one buyer do not block updates for other buyer
72  * </ol>
73  */
74 public class EncoderLogicHandler {
75     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
76     private static final Clock mClock = Clock.getInstance();
77 
78     @VisibleForTesting
79     public static final String ENCODER_VERSION_RESPONSE_HEADER = "X_ENCODER_VERSION";
80 
81     @VisibleForTesting public static final String EMPTY_ADTECH_ID = "";
82     @VisibleForTesting static final int FALLBACK_VERSION = 0;
83     @NonNull private final EncoderPersistenceDao mEncoderPersistenceDao;
84     @NonNull private final EncoderEndpointsDao mEncoderEndpointsDao;
85     @NonNull private final EncoderLogicMetadataDao mEncoderLogicMetadataDao;
86     @NonNull private final ProtectedSignalsDao mProtectedSignalsDao;
87     @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
88     @NonNull private final ListeningExecutorService mBackgroundExecutorService;
89     @NonNull private final AdServicesLogger mAdServicesLogger;
90     @NonNull private final Flags mFlags;
91 
92     @NonNull
93     private static final Map<AdTechIdentifier, ReentrantLock> BUYER_REENTRANT_LOCK_HASH_MAP =
94             new HashMap<>();
95 
96     @NonNull
97     private final ImmutableSet<String> mDownloadRequestProperties =
98             ImmutableSet.of(ENCODER_VERSION_RESPONSE_HEADER);
99 
100     @VisibleForTesting
EncoderLogicHandler( @onNull EncoderPersistenceDao encoderPersistenceDao, @NonNull EncoderEndpointsDao encoderEndpointsDao, @NonNull EncoderLogicMetadataDao encoderLogicMetadataDao, @NonNull ProtectedSignalsDao protectedSignalsDao, @NonNull AdServicesHttpsClient httpsClient, @NonNull ListeningExecutorService backgroundExecutorService, @NonNull AdServicesLogger adServicesLogger, @NonNull Flags flags)101     public EncoderLogicHandler(
102             @NonNull EncoderPersistenceDao encoderPersistenceDao,
103             @NonNull EncoderEndpointsDao encoderEndpointsDao,
104             @NonNull EncoderLogicMetadataDao encoderLogicMetadataDao,
105             @NonNull ProtectedSignalsDao protectedSignalsDao,
106             @NonNull AdServicesHttpsClient httpsClient,
107             @NonNull ListeningExecutorService backgroundExecutorService,
108             @NonNull AdServicesLogger adServicesLogger,
109             @NonNull Flags flags) {
110         Objects.requireNonNull(encoderPersistenceDao);
111         Objects.requireNonNull(encoderEndpointsDao);
112         Objects.requireNonNull(encoderLogicMetadataDao);
113         Objects.requireNonNull(protectedSignalsDao);
114         Objects.requireNonNull(httpsClient);
115         Objects.requireNonNull(backgroundExecutorService);
116         Objects.requireNonNull(adServicesLogger);
117         Objects.requireNonNull(flags);
118         mEncoderPersistenceDao = encoderPersistenceDao;
119         mEncoderEndpointsDao = encoderEndpointsDao;
120         mEncoderLogicMetadataDao = encoderLogicMetadataDao;
121         mProtectedSignalsDao = protectedSignalsDao;
122         mAdServicesHttpsClient = httpsClient;
123         mBackgroundExecutorService = backgroundExecutorService;
124         mAdServicesLogger = adServicesLogger;
125         mFlags = flags;
126     }
127 
EncoderLogicHandler(@onNull Context context)128     public EncoderLogicHandler(@NonNull Context context) {
129         this(
130                 EncoderPersistenceDao.getInstance(),
131                 ProtectedSignalsDatabase.getInstance().getEncoderEndpointsDao(),
132                 ProtectedSignalsDatabase.getInstance().getEncoderLogicMetadataDao(),
133                 ProtectedSignalsDatabase.getInstance().protectedSignalsDao(),
134                 new AdServicesHttpsClient(
135                         AdServicesExecutors.getBackgroundExecutor(),
136                         FlagsFactory.getFlags().getPasSignalsDownloadConnectionTimeoutMs(),
137                         FlagsFactory.getFlags().getPasSignalsDownloadReadTimeoutMs(),
138                         AdServicesHttpsClient.DEFAULT_MAX_BYTES),
139                 AdServicesExecutors.getBackgroundExecutor(),
140                 AdServicesLoggerImpl.getInstance(),
141                 FlagsFactory.getFlags());
142     }
143 
144     /**
145      * When requested to update encoder for a buyer, following events take place
146      *
147      * <ol>
148      *   <li>1. Fetch the encoding URI from {@link EncoderEndpointsDao}
149      *   <li>2. Make a web request using {@link AdServicesHttpsClient} to download the encoder
150      *   <li>3. Extract the encoder from the web-response and persist
151      *       <ol>
152      *         <li>3a. The encoder body is persisted in file storage using {@link
153      *             EncoderPersistenceDao}
154      *         <li>3b. The entry for the downloaded encoder and the version is persisted using
155      *             {@link EncoderLogicMetadataDao}
156      *       </ol>
157      * </ol>
158      *
159      * @param buyer The buyer for which the encoder logic is required to be updated
160      * @param devContext development context used for testing network calls
161      * @return a Fluent Future with success or failure in the form of boolean
162      */
downloadAndUpdate( @onNull AdTechIdentifier buyer, @NonNull DevContext devContext)163     public FluentFuture<Boolean> downloadAndUpdate(
164             @NonNull AdTechIdentifier buyer, @NonNull DevContext devContext) {
165         Objects.requireNonNull(buyer);
166         int traceCookie = Tracing.beginAsyncSection(Tracing.DOWNLOAD_AND_UPDATE_ENCODER);
167 
168         EncodingFetchStats.Builder encodingJsFetchStatsBuilder = EncodingFetchStats.builder();
169         FetchProcessLogger fetchProcessLogger =
170                 getEncodingJsFetchStatsLogger(mFlags, encodingJsFetchStatsBuilder);
171         fetchProcessLogger.setJsDownloadStartTimestamp(mClock.currentTimeMillis());
172         // TODO(b/331682839): Logs enrollment id in AdTech ID field.
173         fetchProcessLogger.setAdTechId(EMPTY_ADTECH_ID);
174 
175         DBEncoderEndpoint encoderEndpoint = mEncoderEndpointsDao.getEndpoint(buyer);
176         if (encoderEndpoint == null) {
177             sLogger.v(
178                     String.format(
179                             "No encoder endpoint found for buyer: %s, skipping download and update",
180                             buyer));
181 
182             fetchProcessLogger.logEncodingJsFetchStats(ENCODING_FETCH_STATUS_OTHER_FAILURE);
183             Tracing.endAsyncSection(Tracing.DOWNLOAD_AND_UPDATE_ENCODER, traceCookie);
184 
185             return FluentFuture.from(Futures.immediateFuture(false));
186         }
187 
188         AdServicesHttpClientRequest downloadRequest =
189                 AdServicesHttpClientRequest.builder()
190                         .setUri(encoderEndpoint.getDownloadUri())
191                         .setUseCache(false)
192                         .setResponseHeaderKeys(mDownloadRequestProperties)
193                         .setDevContext(devContext)
194                         .build();
195         sLogger.v(
196                 "Initiating encoder download request for buyer: %s, uri: %s",
197                 buyer, encoderEndpoint.getDownloadUri());
198         FluentFuture<AdServicesHttpClientResponse> response =
199                 FluentFuture.from(
200                         mAdServicesHttpsClient.fetchPayloadWithLogging(
201                                 downloadRequest, fetchProcessLogger));
202 
203         return response.transform(
204                 r -> {
205                     boolean result = extractAndPersistEncoder(buyer, r);
206                     Tracing.endAsyncSection(Tracing.DOWNLOAD_AND_UPDATE_ENCODER, traceCookie);
207                     return result;
208                 },
209                 mBackgroundExecutorService);
210     }
211 
212     @VisibleForTesting
extractAndPersistEncoder( AdTechIdentifier buyer, AdServicesHttpClientResponse response)213     protected boolean extractAndPersistEncoder(
214             AdTechIdentifier buyer, AdServicesHttpClientResponse response) {
215         if (response == null || response.getResponseBody().isEmpty()) {
216             sLogger.e("Empty response from from client for downloading encoder");
217             ErrorLogUtil.e(
218                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PAS_EMPTY_RESPONSE_FROM_CLIENT_DOWNLOADING_ENCODER,
219                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__PAS);
220             return false;
221         }
222 
223         Trace.beginSection(Tracing.SAVE_BUYERS_ENCODER);
224         String encoderLogicBody = response.getResponseBody();
225 
226         int version = FALLBACK_VERSION;
227         try {
228             if (response.getResponseHeaders() != null
229                     && response.getResponseHeaders().get(ENCODER_VERSION_RESPONSE_HEADER) != null) {
230                 version =
231                         Integer.valueOf(
232                                 response.getResponseHeaders()
233                                         .get(ENCODER_VERSION_RESPONSE_HEADER)
234                                         .get(0));
235             }
236 
237         } catch (NumberFormatException e) {
238             sLogger.e("Invalid or missing version, setting to fallback: " + FALLBACK_VERSION);
239             ErrorLogUtil.e(
240                     e,
241                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PAS_INVALID_OR_MISSING_ENCODER_VERSION,
242                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__PAS);
243         }
244 
245         DBEncoderLogicMetadata encoderLogicEntry =
246                 DBEncoderLogicMetadata.builder()
247                         .setBuyer(buyer)
248                         .setCreationTime(Instant.now())
249                         .setVersion(version)
250                         .build();
251         boolean updateSucceeded = false;
252 
253         ReentrantLock buyerLock = getBuyerLock(buyer);
254         if (buyerLock.tryLock()) {
255             updateSucceeded = mEncoderPersistenceDao.persistEncoder(buyer, encoderLogicBody);
256 
257             if (updateSucceeded) {
258                 sLogger.v(
259                         "Update for encoding logic on persistence layer succeeded, updating DB"
260                                 + " entry");
261                 mEncoderLogicMetadataDao.persistEncoderLogicMetadata(encoderLogicEntry);
262             } else {
263                 sLogger.e(
264                         "Update for encoding logic on persistence layer failed, skipping update"
265                                 + " entry");
266                 ErrorLogUtil.e(
267                         AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PAS_UPDATE_FOR_ENCODING_LOGIC_ON_PERSISTENCE_LAYER_FAILED,
268                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__PAS);
269             }
270             buyerLock.unlock();
271         } else {
272             sLogger.v("Failed to acquire lock for buyer: %s", buyer);
273         }
274         Trace.endSection();
275         sLogger.v("Encoder update succeeded: %b", updateSucceeded);
276         return updateSucceeded;
277     }
278 
279     @VisibleForTesting
getBuyerLock(AdTechIdentifier buyer)280     protected ReentrantLock getBuyerLock(AdTechIdentifier buyer) {
281         synchronized (BUYER_REENTRANT_LOCK_HASH_MAP) {
282             ReentrantLock lock = BUYER_REENTRANT_LOCK_HASH_MAP.get(buyer);
283             if (lock == null) {
284                 lock = new ReentrantLock();
285                 BUYER_REENTRANT_LOCK_HASH_MAP.put(buyer, lock);
286             }
287             return lock;
288         }
289     }
290 
291     /**
292      * @return all the buyers that have registered their encoders
293      */
getBuyersWithEncoders()294     public List<AdTechIdentifier> getBuyersWithEncoders() {
295         return mEncoderLogicMetadataDao.getAllBuyersWithRegisteredEncoders();
296     }
297 
298     /** Returns all registered encoding logic metadata. */
getAllRegisteredEncoders()299     public List<DBEncoderLogicMetadata> getAllRegisteredEncoders() {
300         Trace.beginSection(Tracing.GET_ALL_ENCODERS);
301         List<DBEncoderLogicMetadata> result = mEncoderLogicMetadataDao.getAllRegisteredEncoders();
302         Trace.endSection();
303         return result;
304     }
305 
306     /** Returns the encoding logic for the given buyer. */
getEncoder(AdTechIdentifier buyer)307     public String getEncoder(AdTechIdentifier buyer) {
308         Trace.beginSection(Tracing.GET_ENCODER_FOR_BUYER);
309         String result = mEncoderPersistenceDao.getEncoder(buyer);
310         Trace.endSection();
311         return result;
312     }
313 
314     /** Returns the encoder metadata for the given buyer. */
getEncoderLogicMetadata(AdTechIdentifier adTechIdentifier)315     public DBEncoderLogicMetadata getEncoderLogicMetadata(AdTechIdentifier adTechIdentifier) {
316         return mEncoderLogicMetadataDao.getMetadata(adTechIdentifier);
317     }
318 
319     /** Update the failed count for a buyer */
updateEncoderFailedCount(AdTechIdentifier adTechIdentifier, int count)320     public void updateEncoderFailedCount(AdTechIdentifier adTechIdentifier, int count) {
321         Trace.beginSection(Tracing.UPDATE_FAILED_ENCODING);
322         mEncoderLogicMetadataDao.updateEncoderFailedCount(adTechIdentifier, count);
323         Trace.endSection();
324     }
325 
326     /**
327      * @param expiry time before which the encoders are considered stale
328      * @return the list of buyers that have stale encoders
329      */
getBuyersWithStaleEncoders(Instant expiry)330     public List<AdTechIdentifier> getBuyersWithStaleEncoders(Instant expiry) {
331         return mEncoderLogicMetadataDao.getBuyersWithEncodersBeforeTime(expiry);
332     }
333 
334     /** Deletes the encoder endpoint and logic for a list of buyers */
deleteEncodersForBuyers(Set<AdTechIdentifier> buyers)335     public void deleteEncodersForBuyers(Set<AdTechIdentifier> buyers) {
336         for (AdTechIdentifier buyer : buyers) {
337             deleteEncoderForBuyer(buyer);
338         }
339     }
340 
341     /** Deletes the encoder endpoint and logic for a certain buyer. */
deleteEncoderForBuyer(AdTechIdentifier buyer)342     public void deleteEncoderForBuyer(AdTechIdentifier buyer) {
343         ReentrantLock buyerLock = getBuyerLock(buyer);
344         if (buyerLock.tryLock()) {
345             mEncoderLogicMetadataDao.deleteEncoder(buyer);
346             mEncoderPersistenceDao.deleteEncoder(buyer);
347             mEncoderEndpointsDao.deleteEncoderEndpoint(buyer);
348             mProtectedSignalsDao.deleteSignalsUpdateMetadata(buyer);
349             buyerLock.unlock();
350         }
351     }
352 
getEncodingJsFetchStatsLogger( Flags flags, EncodingFetchStats.Builder builder)353     private FetchProcessLogger getEncodingJsFetchStatsLogger(
354             Flags flags, EncodingFetchStats.Builder builder) {
355         if (flags.getPasExtendedMetricsEnabled()) {
356             return new EncodingJsFetchProcessLoggerImpl(mAdServicesLogger, mClock, builder);
357         } else {
358             return new FetchProcessLoggerNoLoggingImpl();
359         }
360     }
361 }
362