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