Skip to content

Commit

Permalink
Core: Stored Auction Response on Global Level (#3247)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Jul 24, 2024
1 parent 2151a83 commit 28574b5
Show file tree
Hide file tree
Showing 24 changed files with 1,323 additions and 221 deletions.
22 changes: 18 additions & 4 deletions src/main/java/org/prebid/server/auction/BidResponseCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,24 @@ private static int validateTruncateAttrChars(int truncateAttrChars) {
return truncateAttrChars;
}

/**
* Creates an OpenRTB {@link BidResponse} from the bids supplied by the bidder,
* including processing of winning bids with cache IDs.
*/
Future<BidResponse> createOnSkippedAuction(AuctionContext auctionContext, List<SeatBid> seatBids) {
final BidRequest bidRequest = auctionContext.getBidRequest();

final ExtBidResponse extBidResponse = ExtBidResponse.builder()
.warnings(extractContextWarnings(auctionContext))
.tmaxrequest(bidRequest.getTmax())
.build();

final BidResponse bidResponse = BidResponse.builder()
.id(bidRequest.getId())
.cur(Stream.ofNullable(bidRequest.getCur()).flatMap(Collection::stream).findFirst().orElse(null))
.seatbid(Optional.ofNullable(seatBids).orElse(Collections.emptyList()))
.ext(extBidResponse)
.build();

return Future.succeededFuture(bidResponse);
}

Future<BidResponse> create(AuctionContext auctionContext,
BidRequestCacheInfo cacheInfo,
Map<String, MultiBidConfig> bidderToMultiBids) {
Expand Down
105 changes: 105 additions & 0 deletions src/main/java/org/prebid/server/auction/SkippedAuctionService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.prebid.server.auction;

import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.Future;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.auction.model.StoredResponseResult;
import org.prebid.server.exception.InvalidRequestException;
import org.prebid.server.execution.Timeout;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class SkippedAuctionService {

private final StoredResponseProcessor storedResponseProcessor;
private final BidResponseCreator bidResponseCreator;

public SkippedAuctionService(StoredResponseProcessor storedResponseProcessor,
BidResponseCreator bidResponseCreator) {

this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor);
this.bidResponseCreator = Objects.requireNonNull(bidResponseCreator);
}

public Future<AuctionContext> skipAuction(AuctionContext auctionContext) {
if (auctionContext.isRequestRejected()) {
return Future.succeededFuture(auctionContext.with(
BidResponse.builder().seatbid(Collections.emptyList()).build()));
}

final ExtStoredAuctionResponse storedResponse = Optional.ofNullable(auctionContext.getBidRequest())
.map(BidRequest::getExt)
.map(ExtRequest::getPrebid)
.map(ExtRequestPrebid::getStoredAuctionResponse)
.orElse(null);

if (storedResponse == null) {
return Future.failedFuture(new InvalidRequestException(
"the auction can not be skipped, ext.prebid.storedauctionresponse is absent"));
}

final List<SeatBid> seatBids = storedResponse.getSeatBids();
if (seatBids != null) {
return validateStoredSeatBid(seatBids)
.recover(throwable -> {
auctionContext.getDebugWarnings().add(throwable.getMessage());
return Future.succeededFuture(Collections.emptyList());
})
.compose(storedSeatBids -> enrichAuctionContextWithBidResponse(auctionContext, storedSeatBids))
.map(AuctionContext::skipAuction);
}

if (storedResponse.getId() != null) {
final Timeout timeout = auctionContext.getTimeoutContext().getTimeout();
return storedResponseProcessor.getStoredResponseResult(storedResponse.getId(), timeout)
.map(StoredResponseResult::getAuctionStoredResponse)
.recover(throwable -> {
auctionContext.getDebugWarnings().add(throwable.getMessage());
return Future.succeededFuture(Collections.emptyList());
})
.compose(storedSeatBids -> enrichAuctionContextWithBidResponse(auctionContext, storedSeatBids))
.map(AuctionContext::skipAuction);
}

return Future.failedFuture(new InvalidRequestException(
"the auction can not be skipped, ext.prebid.storedauctionresponse can not be resolved properly"));

}

private Future<List<SeatBid>> validateStoredSeatBid(List<SeatBid> seatBids) {
for (final SeatBid seatBid : seatBids) {
if (seatBid == null) {
return Future.failedFuture(
new InvalidRequestException("SeatBid can't be null in stored response"));
}
if (StringUtils.isEmpty(seatBid.getSeat())) {
return Future.failedFuture(
new InvalidRequestException("Seat can't be empty in stored response seatBid"));
}

if (CollectionUtils.isEmpty(seatBid.getBid())) {
return Future.failedFuture(
new InvalidRequestException("There must be at least one bid in stored response seatBid"));
}
}

return Future.succeededFuture(seatBids);
}

private Future<AuctionContext> enrichAuctionContextWithBidResponse(AuctionContext auctionContext,
List<SeatBid> seatBids) {

auctionContext.getDebugWarnings().add("no auction. response defined by storedauctionresponse");
return bidResponseCreator.createOnSkippedAuction(auctionContext, seatBids).map(auctionContext::with);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ Future<StoredResponseResult> getStoredResponseResult(List<Imp> imps, Timeout tim
impToBidderToStoredBidResponseId)));
}

Future<StoredResponseResult> getStoredResponseResult(String storedId, Timeout timeout) {
return applicationSettings.getStoredResponses(Collections.singleton(storedId), timeout)
.recover(exception -> Future.failedFuture(new InvalidRequestException(
"Stored response fetching failed with reason: " + exception.getMessage())))
.map(storedResponseDataResult -> StoredResponseResult.of(
Collections.emptyList(),
convertToSeatBid(storedResponseDataResult),
Collections.emptyMap()));
}

private List<Imp> excludeStoredAuctionResponseImps(List<Imp> imps,
Map<String, String> auctionStoredResponseToImpId) {

Expand Down Expand Up @@ -260,6 +270,23 @@ private List<SeatBid> convertToSeatBid(StoredResponseDataResult storedResponseDa
return mergeSameBidderSeatBid(resolvedSeatBids);
}

private List<SeatBid> convertToSeatBid(StoredResponseDataResult storedResponseDataResult) {
final List<SeatBid> resolvedSeatBids = new ArrayList<>();
final Map<String, String> idToStoredResponses = storedResponseDataResult.getIdToStoredResponses();
for (final Map.Entry<String, String> storedIdToImpId : idToStoredResponses.entrySet()) {
final String id = storedIdToImpId.getKey();
final String rowSeatBid = storedIdToImpId.getValue();
if (rowSeatBid == null) {
throw new InvalidRequestException(
"Failed to fetch stored auction response for storedAuctionResponse id = %s.".formatted(id));
}
final List<SeatBid> seatBids = parseSeatBid(id, rowSeatBid);
validateStoredSeatBid(seatBids);
resolvedSeatBids.addAll(seatBids);
}
return mergeSameBidderSeatBid(resolvedSeatBids);
}

private List<SeatBid> parseSeatBid(String id, String rowSeatBid) {
try {
return mapper.mapper().readValue(rowSeatBid, SEATBID_LIST_TYPE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public class AuctionContext {

boolean requestRejected;

boolean auctionSkipped;

CachedDebugLog cachedDebugLog;

public AuctionContext with(Account account) {
Expand Down Expand Up @@ -127,4 +129,10 @@ public AuctionContext withRequestRejected() {
.requestRejected(true)
.build();
}

public AuctionContext skipAuction() {
return this.toBuilder()
.auctionSkipped(true)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ public AuctionRequestFactory(long maxRequestSize,
}

/**
* Creates {@link AuctionContext} based on {@link RoutingContext}.
* Creates {@link AuctionContext} and parses BidRequest based on {@link RoutingContext}.
*/
public Future<AuctionContext> fromRequest(RoutingContext routingContext, long startTime) {
public Future<AuctionContext> parseRequest(RoutingContext routingContext, long startTime) {
final String body;
try {
body = extractAndValidateBody(routingContext);
Expand All @@ -103,9 +103,18 @@ public Future<AuctionContext> fromRequest(RoutingContext routingContext, long st
.map(bidRequest -> ortb2RequestFactory
.enrichAuctionContext(initialAuctionContext, httpRequest, bidRequest, startTime)
.with(requestTypeMetric(bidRequest))))
.recover(ortb2RequestFactory::restoreResultFromRejection);
}

.compose(auctionContext -> ortb2RequestFactory.fetchAccount(auctionContext)
.map(auctionContext::with))
/**
* Enriches {@link AuctionContext}.
*/
public Future<AuctionContext> enrichAuctionContext(AuctionContext initialContext) {
if (initialContext.isRequestRejected()) {
return Future.succeededFuture(initialContext);
}

return ortb2RequestFactory.fetchAccount(initialContext).map(initialContext::with)

.map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext)))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.RoutingContext;
import org.prebid.server.analytics.model.AuctionEvent;
import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
import org.prebid.server.auction.ExchangeService;
import org.prebid.server.auction.SkippedAuctionService;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.auction.requestfactory.AuctionRequestFactory;
import org.prebid.server.cookie.UidsCookie;
Expand Down Expand Up @@ -50,6 +52,7 @@ public class AuctionHandler implements ApplicationResource {
private final double logSamplingRate;
private final AuctionRequestFactory auctionRequestFactory;
private final ExchangeService exchangeService;
private final SkippedAuctionService skippedAuctionService;
private final AnalyticsReporterDelegator analyticsDelegator;
private final Metrics metrics;
private final Clock clock;
Expand All @@ -60,6 +63,7 @@ public class AuctionHandler implements ApplicationResource {
public AuctionHandler(double logSamplingRate,
AuctionRequestFactory auctionRequestFactory,
ExchangeService exchangeService,
SkippedAuctionService skippedAuctionService,
AnalyticsReporterDelegator analyticsDelegator,
Metrics metrics,
Clock clock,
Expand All @@ -70,6 +74,7 @@ public AuctionHandler(double logSamplingRate,
this.logSamplingRate = logSamplingRate;
this.auctionRequestFactory = Objects.requireNonNull(auctionRequestFactory);
this.exchangeService = Objects.requireNonNull(exchangeService);
this.skippedAuctionService = Objects.requireNonNull(skippedAuctionService);
this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator);
this.metrics = Objects.requireNonNull(metrics);
this.clock = Objects.requireNonNull(clock);
Expand All @@ -94,8 +99,16 @@ public void handle(RoutingContext routingContext) {
final AuctionEvent.AuctionEventBuilder auctionEventBuilder = AuctionEvent.builder()
.httpContext(HttpRequestContext.from(routingContext));

auctionRequestFactory.fromRequest(routingContext, startTime)
auctionRequestFactory.parseRequest(routingContext, startTime)
.compose(auctionContext -> skippedAuctionService.skipAuction(auctionContext)
.recover(throwable -> holdAuction(auctionEventBuilder, auctionContext)))
.onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime));
}

private Future<AuctionContext> holdAuction(AuctionEvent.AuctionEventBuilder auctionEventBuilder,
AuctionContext auctionContext) {

return auctionRequestFactory.enrichAuctionContext(auctionContext)
.map(this::updateAppAndNoCookieAndImpsMetrics)

// In case of holdAuction Exception and auctionContext is not present below
Expand All @@ -104,8 +117,7 @@ public void handle(RoutingContext routingContext) {
.compose(exchangeService::holdAuction)
// populate event with updated context
.map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context))
.map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context))
.onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime));
.map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context));
}

private static <T, R> R addToEvent(T field, Consumer<T> consumer, R result) {
Expand Down Expand Up @@ -138,6 +150,7 @@ private void handleResult(AsyncResult<AuctionContext> responseResult,
final boolean responseSucceeded = responseResult.succeeded();

final AuctionContext auctionContext = responseSucceeded ? responseResult.result() : null;
final boolean isAuctionSkipped = responseSucceeded && auctionContext.isAuctionSkipped();
final MetricName requestType = responseSucceeded
? auctionContext.getRequestTypeMetric()
: MetricName.openrtb2web;
Expand Down Expand Up @@ -215,42 +228,34 @@ private void handleResult(AsyncResult<AuctionContext> responseResult,
final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null;
final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty();

respondWith(
routingContext,
status,
body,
startTime,
requestType,
metricRequestStatus,
auctionEvent,
tcfContext);
final boolean responseSent = respondWith(routingContext, status, body, requestType);

if (responseSent) {
metrics.updateRequestTimeMetric(MetricName.request_time, clock.millis() - startTime);
metrics.updateRequestTypeMetric(requestType, metricRequestStatus);
if (!isAuctionSkipped) {
analyticsDelegator.processEvent(auctionEvent, tcfContext);
}
} else {
metrics.updateRequestTypeMetric(requestType, MetricName.networkerr);
}

httpInteractionLogger.maybeLogOpenrtb2Auction(auctionContext, routingContext, status.code(), body);
}

private void respondWith(RoutingContext routingContext,
HttpResponseStatus status,
String body,
long startTime,
MetricName requestType,
MetricName metricRequestStatus,
AuctionEvent event,
TcfContext tcfContext) {
private boolean respondWith(RoutingContext routingContext,
HttpResponseStatus status,
String body,
MetricName requestType) {

final boolean responseSent = HttpUtil.executeSafely(
return HttpUtil.executeSafely(
routingContext,
Endpoint.openrtb2_auction,
response -> response
.exceptionHandler(throwable -> handleResponseException(throwable, requestType))
.setStatusCode(status.code())
.end(body));

if (responseSent) {
metrics.updateRequestTimeMetric(MetricName.request_time, clock.millis() - startTime);
metrics.updateRequestTypeMetric(requestType, metricRequestStatus);
analyticsDelegator.processEvent(event, tcfContext);
} else {
metrics.updateRequestTypeMetric(requestType, MetricName.networkerr);
}
}

private void handleResponseException(Throwable throwable, MetricName requestType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public class ExtRequestPrebid {
*/
ExtStoredRequest storedrequest;

/**
* Defines the contract for bidrequest.ext.prebid.storedauctionresponse
*/
@JsonProperty("storedauctionresponse")
ExtStoredAuctionResponse storedAuctionResponse;

/**
* Defines the contract for bidrequest.ext.prebid.cache
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package org.prebid.server.proto.openrtb.ext.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.iab.openrtb.response.SeatBid;
import lombok.AllArgsConstructor;
import lombok.Value;

import java.util.List;

@AllArgsConstructor(staticName = "of")
@Value
public class ExtStoredAuctionResponse {

String id;

@JsonProperty("seatbidarr")
List<SeatBid> seatBids;
}
Loading

0 comments on commit 28574b5

Please sign in to comment.