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