Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 82 additions & 44 deletions src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.prebid.server.log.ConditionalLogger;
import org.prebid.server.log.Logger;
import org.prebid.server.log.LoggerFactory;
import org.prebid.server.metric.MetricName;
import org.prebid.server.metric.Metrics;
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
Expand Down Expand Up @@ -50,19 +52,35 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor {
private static final int MODEL_WEIGHT_MAX_VALUE = 100;
private static final int MODEL_WEIGHT_MIN_VALUE = 1;

private static final String FETCH_FAILED_ERROR_MESSAGE = "Price floors processing failed: %s. "
+ "Following parsing of request price floors is failed: %s";
private static final String DYNAMIC_DATA_NOT_ALLOWED_MESSAGE =
"Price floors processing failed: Using dynamic data is not allowed. "
+ "Following parsing of request price floors is failed: %s";
private static final String INVALID_REQUEST_WARNING_MESSAGE =
"Price floors processing failed: parsing of request price floors is failed: %s";
private static final String ERROR_LOG_MESSAGE =
"Price Floors can't be resolved for account %s and request %s, reason: %s";

private final PriceFloorFetcher floorFetcher;
private final PriceFloorResolver floorResolver;
private final Metrics metrics;
private final JacksonMapper mapper;
private final double logSamplingRate;

private final RandomWeightedEntrySupplier<PriceFloorModelGroup> modelPicker;

public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
PriceFloorResolver floorResolver,
JacksonMapper mapper) {
Metrics metrics,
JacksonMapper mapper,
double logSamplingRate) {

this.floorFetcher = Objects.requireNonNull(floorFetcher);
this.floorResolver = Objects.requireNonNull(floorResolver);
this.metrics = Objects.requireNonNull(metrics);
this.mapper = Objects.requireNonNull(mapper);
this.logSamplingRate = logSamplingRate;

modelPicker = new RandomPositiveWeightedEntrySupplier<>(BasicPriceFloorProcessor::resolveModelGroupWeight);
}
Expand All @@ -82,7 +100,7 @@ public BidRequest enrichWithPriceFloors(BidRequest bidRequest,
return disableFloorsForRequest(bidRequest);
}

final PriceFloorRules floors = resolveFloors(account, bidRequest, errors);
final PriceFloorRules floors = resolveFloors(account, bidRequest, warnings);
return updateBidRequestWithFloors(bidRequest, bidder, floors, errors, warnings);
}

Expand Down Expand Up @@ -122,49 +140,13 @@ private static PriceFloorRules extractRequestFloors(BidRequest bidRequest) {
return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors);
}

private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List<String> errors) {
private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List<String> warnings) {
final PriceFloorRules requestFloors = extractRequestFloors(bidRequest);

final FetchResult fetchResult = floorFetcher.fetch(account);
final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus);

if (fetchResult != null && fetchStatus == FetchStatus.success && shouldUseDynamicData(account, fetchResult)) {
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
}
final FetchStatus fetchStatus = fetchResult.getFetchStatus();

if (requestFloors != null) {
try {
final Optional<AccountPriceFloorsConfig> priceFloorsConfig = Optional.of(account)
.map(Account::getAuction)
.map(AccountAuctionConfig::getPriceFloors);

final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
.orElse(null);
final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
.orElse(null);

PriceFloorRulesValidator.validateRules(
requestFloors,
PriceFloorsConfigResolver.resolveMaxValue(maxRules),
PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));

return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request);
} catch (PreBidException e) {
errors.add("Failed to parse price floors from request, with a reason: %s".formatted(e.getMessage()));
conditionalLogger.error(
"Failed to parse price floors from request with id: '%s', with a reason: %s"
.formatted(bidRequest.getId(), e.getMessage()),
0.01d);
}
}

return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData);
}

private static boolean shouldUseDynamicData(Account account, FetchResult fetchResult) {
final boolean isUsingDynamicDataAllowed = Optional.of(account)
.map(Account::getAuction)
final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account.getAuction())
.map(AccountAuctionConfig::getPriceFloors)
.map(AccountPriceFloorsConfig::getUseDynamicData)
.map(BooleanUtils::isNotFalse)
Expand All @@ -175,12 +157,68 @@ private static boolean shouldUseDynamicData(Account account, FetchResult fetchRe
.map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate)
.orElse(true);

return isUsingDynamicDataAllowed && shouldUseDynamicData;
if (fetchStatus == FetchStatus.success && isUsingDynamicDataAllowed && shouldUseDynamicData) {
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
}

return requestFloors == null
? createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData)
: getPriceFloorRules(
bidRequest, account, requestFloors, fetchResult, isUsingDynamicDataAllowed, warnings);
}

private PriceFloorRules getPriceFloorRules(BidRequest bidRequest,
Account account,
PriceFloorRules requestFloors,
FetchResult fetchResult,
boolean isDynamicDataAllowed,
List<String> warnings) {

try {
final Optional<AccountPriceFloorsConfig> priceFloorsConfig = Optional.of(account.getAuction())
.map(AccountAuctionConfig::getPriceFloors);

final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
.orElse(null);
final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
.orElse(null);

PriceFloorRulesValidator.validateRules(
requestFloors,
PriceFloorsConfigResolver.resolveMaxValue(maxRules),
PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));

return createFloorsFrom(requestFloors, fetchResult.getFetchStatus(), PriceFloorLocation.request);
} catch (PreBidException e) {
logErrorMessage(fetchResult, isDynamicDataAllowed, e, account.getId(), bidRequest.getId(), warnings);
return createFloorsFrom(null, fetchResult.getFetchStatus(), PriceFloorLocation.noData);
}
}

private PriceFloorRules mergeFloors(PriceFloorRules requestFloors,
PriceFloorData providerRulesData) {
private void logErrorMessage(FetchResult fetchResult,
boolean isDynamicDataAllowed,
PreBidException requestFloorsValidationException,
String accountId,
String requestId,
List<String> warnings) {

final String validationMessage = requestFloorsValidationException.getMessage();
final String errorMessage = switch (fetchResult.getFetchStatus()) {
case inprogress -> null;
case error, timeout, none -> FETCH_FAILED_ERROR_MESSAGE.formatted(
fetchResult.getErrorMessage(), validationMessage);
case success -> isDynamicDataAllowed ? null : DYNAMIC_DATA_NOT_ALLOWED_MESSAGE.formatted(validationMessage);
};

if (errorMessage != null) {
warnings.add(INVALID_REQUEST_WARNING_MESSAGE.formatted(validationMessage));
conditionalLogger.error(ERROR_LOG_MESSAGE.formatted(accountId, requestId, errorMessage), logSamplingRate);
metrics.updateAlertsMetrics(MetricName.general);
}
}

private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, PriceFloorData providerRulesData) {
final Price floorMinPrice = resolveFloorMinPrice(requestFloors);

return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder())
Expand Down
63 changes: 36 additions & 27 deletions src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ public FetchResult fetch(Account account) {
final AccountFetchContext accountFetchContext = fetchedData.get(account.getId());

return accountFetchContext != null
? FetchResult.of(accountFetchContext.getRulesData(), accountFetchContext.getFetchStatus())
? FetchResult.of(
accountFetchContext.getRulesData(),
accountFetchContext.getFetchStatus(),
accountFetchContext.getErrorMessage())
: fetchPriceFloorData(account);
}

Expand All @@ -99,20 +102,20 @@ private FetchResult fetchPriceFloorData(Account account) {
final Boolean fetchEnabled = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getEnabled);

if (BooleanUtils.isFalse(fetchEnabled)) {
return FetchResult.of(null, FetchStatus.none);
return FetchResult.none("Fetching is disabled");
}

final String accountId = account.getId();
final String fetchUrl = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getUrl);
if (!isUrlValid(fetchUrl)) {
logger.error("Malformed fetch.url: '%s', passed for account %s".formatted(fetchUrl, accountId));
return FetchResult.of(null, FetchStatus.error);
logger.error("Malformed fetch.url: '%s' passed for account %s".formatted(fetchUrl, accountId));
return FetchResult.error("Malformed fetch.url '%s' passed".formatted(fetchUrl));
}
if (!fetchInProgress.contains(accountId)) {
fetchPriceFloorDataAsynchronous(fetchConfig, accountId);
}

return FetchResult.of(null, FetchStatus.inprogress);
return FetchResult.inProgress();
}

private boolean isUrlValid(String url) {
Expand Down Expand Up @@ -148,7 +151,7 @@ private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetch

fetchInProgress.add(accountId);
httpClient.get(fetchUrl, timeout, resolveMaxFileSize(maxFetchFileSizeKb))
.map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId))
.map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig))
.recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId))
.map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId))
.map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId));
Expand All @@ -159,40 +162,38 @@ private static long resolveMaxFileSize(Long maxSizeInKBytes) {
}

private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientResponse,
AccountPriceFloorsFetchConfig fetchConfig,
String accountId) {
AccountPriceFloorsFetchConfig fetchConfig) {

final int statusCode = httpClientResponse.getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
throw new PreBidException("Failed to request for account %s, provider respond with status %s"
.formatted(accountId, statusCode));
throw new PreBidException("Failed to request, provider respond with status %s".formatted(statusCode));
}
final String body = httpClientResponse.getBody();

if (StringUtils.isBlank(body)) {
throw new PreBidException(
"Failed to parse price floor response for account %s, response body can not be empty"
.formatted(accountId));
throw new PreBidException("Failed to parse price floor response, response body can not be empty");
}

final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId);
final PriceFloorData priceFloorData = parsePriceFloorData(body);

PriceFloorRulesValidator.validateRulesData(
priceFloorData,
PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxRules()),
PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxSchemaDims()));

return ResponseCacheInfo.of(priceFloorData,
FetchStatus.success,
null,
cacheTtlFromResponse(httpClientResponse, fetchConfig.getUrl()));
}

private PriceFloorData parsePriceFloorData(String body, String accountId) {
private PriceFloorData parsePriceFloorData(String body) {
final PriceFloorData priceFloorData;
try {
priceFloorData = mapper.decodeValue(body, PriceFloorData.class);
} catch (DecodeException e) {
throw new PreBidException("Failed to parse price floor response for account %s, cause: %s"
.formatted(accountId, ExceptionUtils.getMessage(e)));
throw new PreBidException(
"Failed to parse price floor response, cause: %s".formatted(ExceptionUtils.getMessage(e)));
}
return priceFloorData;
}
Expand Down Expand Up @@ -220,8 +221,11 @@ private PriceFloorData updateCache(ResponseCacheInfo cacheInfo,
String accountId) {

final long maxAgeTimerId = createMaxAgeTimer(accountId, resolveCacheTtl(cacheInfo, fetchConfig));
final AccountFetchContext fetchContext =
AccountFetchContext.of(cacheInfo.getRulesData(), cacheInfo.getFetchStatus(), maxAgeTimerId);
final AccountFetchContext fetchContext = AccountFetchContext.of(
cacheInfo.getRulesData(),
cacheInfo.getFetchStatus(),
cacheInfo.getErrorMessage(),
maxAgeTimerId);

if (cacheInfo.getFetchStatus() == FetchStatus.success || !fetchedData.containsKey(accountId)) {
fetchedData.put(accountId, fetchContext);
Expand Down Expand Up @@ -274,23 +278,24 @@ private Future<ResponseCacheInfo> recoverFromFailedFetching(Throwable throwable,
metrics.updatePriceFloorFetchMetric(MetricName.failure);

final FetchStatus fetchStatus;
final String errorMessage;
if (throwable instanceof TimeoutException || throwable instanceof ConnectTimeoutException) {
fetchStatus = FetchStatus.timeout;
logger.error("Fetch price floor request timeout for fetch.url: '%s', account %s exceeded."
.formatted(fetchUrl, accountId));
errorMessage = "Fetch price floor request timeout for fetch.url '%s' exceeded.".formatted(fetchUrl);
} else {
fetchStatus = FetchStatus.error;
logger.error(
"Failed to fetch price floor from provider for fetch.url: '%s', account = %s with a reason : %s "
.formatted(fetchUrl, accountId, throwable.getMessage()));
errorMessage = "Failed to fetch price floor from provider for fetch.url '%s', with a reason: %s"
.formatted(fetchUrl, throwable.getMessage());
}

return Future.succeededFuture(ResponseCacheInfo.withStatus(fetchStatus));
logger.error("Price floor fetching failed for account %s: %s".formatted(accountId, errorMessage));
return Future.succeededFuture(ResponseCacheInfo.withError(fetchStatus, errorMessage));
}

private PriceFloorData createPeriodicTimerForRulesFetch(PriceFloorData priceFloorData,
AccountPriceFloorsFetchConfig fetchConfig,
String accountId) {

final long accountPeriodicTimeSec =
ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec);
final long periodicTimeSec =
Expand Down Expand Up @@ -318,6 +323,8 @@ private static class AccountFetchContext {

FetchStatus fetchStatus;

String errorMessage;

Long maxAgeTimerId;
}

Expand All @@ -328,10 +335,12 @@ private static class ResponseCacheInfo {

FetchStatus fetchStatus;

String errorMessage;

Long cacheTtl;

public static ResponseCacheInfo withStatus(FetchStatus status) {
return ResponseCacheInfo.of(null, status, null);
public static ResponseCacheInfo withError(FetchStatus status, String errorMessage) {
return ResponseCacheInfo.of(null, status, errorMessage, null);
}
}
}
14 changes: 14 additions & 0 deletions src/main/java/org/prebid/server/floors/proto/FetchResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,18 @@ public class FetchResult {
PriceFloorData rulesData;

FetchStatus fetchStatus;

String errorMessage;

public static FetchResult none(String errorMessage) {
return FetchResult.of(null, FetchStatus.none, errorMessage);
}

public static FetchResult error(String errorMessage) {
return FetchResult.of(null, FetchStatus.error, errorMessage);
}

public static FetchResult inProgress() {
return FetchResult.of(null, FetchStatus.inprogress, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.prebid.server.metric.Metrics;
import org.prebid.server.settings.ApplicationSettings;
import org.prebid.server.vertx.httpclient.HttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -84,9 +85,11 @@ PriceFloorResolver noOpPriceFloorResolver() {
@ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true")
PriceFloorProcessor basicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
PriceFloorResolver floorResolver,
JacksonMapper mapper) {
Metrics metrics,
JacksonMapper mapper,
@Value("${logging.sampling-rate:0.01}") double logSamplingRate) {

return new BasicPriceFloorProcessor(floorFetcher, floorResolver, mapper);
return new BasicPriceFloorProcessor(floorFetcher, floorResolver, metrics, mapper, logSamplingRate);
}

@Bean
Expand Down
Loading
Loading