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