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
121 changes: 77 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 @@ -52,17 +54,23 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor {

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 +90,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 +130,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);
final FetchStatus fetchStatus = fetchResult.getFetchStatus();

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

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 +147,73 @@ 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);
}

final String fetchErrorMessage = resolveFetchErrorMessage(fetchResult, isUsingDynamicDataAllowed);
return requestFloors == null
? noPriceFloorData(fetchStatus, account.getId(), bidRequest.getId(), fetchErrorMessage, warnings)
: getPriceFloorRules(bidRequest, account, requestFloors, fetchStatus, fetchErrorMessage, warnings);
}

private static String resolveFetchErrorMessage(FetchResult fetchResult, boolean isUsingDynamicDataAllowed) {
return switch (fetchResult.getFetchStatus()) {
case inprogress -> null;
case error, timeout, none -> fetchResult.getErrorMessage();
case success -> isUsingDynamicDataAllowed ? null : "Using dynamic data is not allowed";
};
}

private PriceFloorRules noPriceFloorData(FetchStatus fetchStatus,
String accountId,
String requestId,
String errorMessage,
List<String> warnings) {

if (errorMessage != null) {
warnings.add(errorMessage);
conditionalLogger.error("No price floor data for account %s and request %s, reason: %s"
.formatted(accountId, requestId, errorMessage), logSamplingRate);
metrics.updateAlertsMetrics(MetricName.general);
}

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

private PriceFloorRules mergeFloors(PriceFloorRules requestFloors,
PriceFloorData providerRulesData) {
private PriceFloorRules getPriceFloorRules(BidRequest bidRequest,
Account account,
PriceFloorRules requestFloors,
FetchStatus fetchStatus,
String fetchErrorMessage,
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, fetchStatus, PriceFloorLocation.request);
} catch (PreBidException e) {
final String errorMessage = fetchErrorMessage == null
? null
: "%s. Following parsing of request price floors is failed: %s"
.formatted(fetchErrorMessage, e.getMessage());
return noPriceFloorData(fetchStatus, account.getId(), bidRequest.getId(), errorMessage, warnings);
}
}

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

return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder())
Expand Down
68 changes: 36 additions & 32 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,19 @@ 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);
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,8 +150,8 @@ private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetch

fetchInProgress.add(accountId);
httpClient.get(fetchUrl, timeout, resolveMaxFileSize(maxFetchFileSizeKb))
.map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId))
.recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId))
.map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig))
.recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl))
.map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId))
.map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId));
}
Expand All @@ -159,40 +161,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 +220,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 @@ -267,30 +270,27 @@ private Long createMaxAgeTimer(String accountId, long cacheTtl) {
return vertx.setTimer(TimeUnit.SECONDS.toMillis(effectiveCacheTtl), id -> fetchedData.remove(accountId));
}

private Future<ResponseCacheInfo> recoverFromFailedFetching(Throwable throwable,
String fetchUrl,
String accountId) {

private Future<ResponseCacheInfo> recoverFromFailedFetching(Throwable throwable, String fetchUrl) {
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));
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 +318,8 @@ private static class AccountFetchContext {

FetchStatus fetchStatus;

String errorMessage;

Long maxAgeTimerId;
}

Expand All @@ -328,10 +330,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