Skip to content

Commit d4f2e3e

Browse files
committed
Add support for secondary bidders
1 parent 50beaa3 commit d4f2e3e

7 files changed

Lines changed: 494 additions & 40 deletions

File tree

src/main/java/org/prebid/server/auction/ExchangeService.java

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
import org.prebid.server.util.BidderUtil;
100100
import org.prebid.server.util.HttpUtil;
101101
import org.prebid.server.util.ListUtil;
102+
import org.prebid.server.util.MapUtil;
102103
import org.prebid.server.util.PbsUtil;
103104
import org.prebid.server.util.StreamUtil;
104105

@@ -115,6 +116,8 @@
115116
import java.util.Objects;
116117
import java.util.Optional;
117118
import java.util.Set;
119+
import java.util.function.Function;
120+
import java.util.function.Predicate;
118121
import java.util.stream.Collectors;
119122

120123
public class ExchangeService {
@@ -259,17 +262,7 @@ private Future<AuctionContext> runAuction(AuctionContext receivedContext) {
259262
.map(receivedContext::with))
260263

261264
.map(context -> updateRequestMetric(context, uidsCookie, aliases, account, requestTypeMetric))
262-
.compose(context -> Future.join(
263-
context.getAuctionParticipations().stream()
264-
.map(auctionParticipation -> processAndRequestBids(
265-
context,
266-
auctionParticipation.getBidderRequest(),
267-
timeout,
268-
aliases)
269-
.map(auctionParticipation::with))
270-
.toList())
271-
// send all the requests to the bidders and gathers results
272-
.map(CompositeFuture::<AuctionParticipation>list)
265+
.compose(context -> processAndRequestBids(context, timeout, aliases)
273266
.map(storedResponseProcessor::updateStoredBidResponse)
274267
.map(auctionParticipations -> storedResponseProcessor.mergeWithBidderResponses(
275268
auctionParticipations,
@@ -459,13 +452,10 @@ private void makeBidRejectionTrackers(Map<String, BidRejectionTracker> bidReject
459452
bidderToImpIds.computeIfAbsent(bidder, bidderName -> new HashSet<>()).add(impId));
460453
}
461454

462-
bidderToImpIds.forEach((bidder, impIds) -> {
463-
if (bidRejectionTrackers.containsKey(bidder)) {
464-
bidRejectionTrackers.put(bidder, new BidRejectionTracker(bidRejectionTrackers.get(bidder), impIds));
465-
} else {
466-
bidRejectionTrackers.put(bidder, new BidRejectionTracker(bidder, impIds, logSamplingRate));
467-
}
468-
});
455+
bidderToImpIds.forEach((bidder, impIds) -> bidRejectionTrackers.put(bidder,
456+
bidRejectionTrackers.containsKey(bidder)
457+
? BidRejectionTracker.withAdditionalImpIds(bidRejectionTrackers.get(bidder), impIds)
458+
: new BidRejectionTracker(bidder, impIds, logSamplingRate)));
469459
}
470460

471461
private static StoredResponseResult populateStoredResponse(StoredResponseResult storedResponseResult,
@@ -1149,10 +1139,93 @@ private AuctionContext updateRequestMetric(AuctionContext context,
11491139
return context;
11501140
}
11511141

1152-
private Future<BidderResponse> processAndRequestBids(AuctionContext auctionContext,
1153-
BidderRequest bidderRequest,
1154-
Timeout timeout,
1155-
BidderAliases aliases) {
1142+
private Future<List<AuctionParticipation>> processAndRequestBids(AuctionContext auctionContext,
1143+
Timeout timeout,
1144+
BidderAliases aliases) {
1145+
1146+
// when we ignore Futures from uncompleted secondary bidders,
1147+
// they continue running in the background and can write errors to BidRejectionTrackers.
1148+
// To prevent any issues, we provide them with a copy of BidRejectionTracker
1149+
// and only merge this copy back if secondary bidder has completed in time
1150+
final Map<String, BidRejectionTracker> copiedBidRejectionTrackers =
1151+
MapUtil.mapValues(auctionContext.getBidRejectionTrackers(), BidRejectionTracker::copyOf);
1152+
1153+
final Map<String, AuctionParticipation> bidderToAuctionParticipation = auctionContext
1154+
.getAuctionParticipations().stream().collect(Collectors.toMap(
1155+
AuctionParticipation::getBidder,
1156+
Function.identity()));
1157+
1158+
final Map<String, Future<BidderResponse>> bidderToFutureResponse = MapUtil.mapValues(
1159+
bidderToAuctionParticipation,
1160+
auctionParticipation -> processAndRequestBidsForSingleBidder(
1161+
auctionContext.with(copiedBidRejectionTrackers),
1162+
auctionParticipation.getBidderRequest(),
1163+
timeout,
1164+
aliases));
1165+
1166+
return buildPrimaryBiddersCompositeFuture(auctionContext, bidderToFutureResponse).transform(ignored -> {
1167+
mergeBidRejectionTrackers(auctionContext, copiedBidRejectionTrackers, bidderToFutureResponse);
1168+
1169+
return Future.succeededFuture(bidderToFutureResponse.entrySet().stream()
1170+
.map(MapUtil.mapEntryValueMapper((bidder, futureResponse) ->
1171+
futureResponse.isComplete() ? futureResponse : recoverUncompletedSecondaryBidder(bidder)))
1172+
.map(MapUtil.mapEntryMapper((bidder, futureResponse) -> futureResponse.map(bidderResponse ->
1173+
bidderToAuctionParticipation.get(bidder).with(bidderResponse))))
1174+
.map(Future::result)
1175+
.filter(Objects::nonNull)
1176+
.toList());
1177+
});
1178+
}
1179+
1180+
private Future<BidderResponse> recoverUncompletedSecondaryBidder(String bidderName) {
1181+
final BidderSeatBid bidderSeatBid = BidderSeatBid.builder()
1182+
.warnings(Collections.singletonList(
1183+
BidderError.of("secondary bidder timed out, auction proceeded", BidderError.Type.timeout)))
1184+
.build();
1185+
1186+
return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0));
1187+
}
1188+
1189+
private CompositeFuture buildPrimaryBiddersCompositeFuture(
1190+
AuctionContext auctionContext,
1191+
Map<String, Future<BidderResponse>> bidderToFutureResponse) {
1192+
1193+
final Set<String> secondaryBidders = Optional.of(auctionContext)
1194+
.map(AuctionContext::getAccount)
1195+
.map(Account::getAuction)
1196+
.map(AccountAuctionConfig::getSecondaryBidders)
1197+
.orElse(Collections.emptySet());
1198+
1199+
final List<Future<BidderResponse>> primaryBiddersFutureResponses = bidderToFutureResponse.keySet().stream()
1200+
.filter(Predicate.not(secondaryBidders::contains))
1201+
.map(bidderToFutureResponse::get)
1202+
.toList();
1203+
1204+
return Future.join(CollectionUtils.isNotEmpty(primaryBiddersFutureResponses)
1205+
? primaryBiddersFutureResponses
1206+
: bidderToFutureResponse.values().stream().toList());
1207+
}
1208+
1209+
private void mergeBidRejectionTrackers(
1210+
AuctionContext auctionContext,
1211+
Map<String, BidRejectionTracker> newBidRejectionTrackers,
1212+
Map<String, Future<BidderResponse>> bidderToFutureResponse) {
1213+
1214+
final Map<String, BidRejectionTracker> mergedBidRejectionTrackers = MapUtil.mapValues(
1215+
bidderToFutureResponse,
1216+
(bidder, futureResponse) -> futureResponse.isComplete()
1217+
? newBidRejectionTrackers.get(bidder)
1218+
: auctionContext.getBidRejectionTrackers().get(bidder)
1219+
.rejectAll(BidRejectionReason.ERROR_TIMED_OUT));
1220+
1221+
auctionContext.getBidRejectionTrackers().clear();
1222+
auctionContext.getBidRejectionTrackers().putAll(mergedBidRejectionTrackers);
1223+
}
1224+
1225+
private Future<BidderResponse> processAndRequestBidsForSingleBidder(AuctionContext auctionContext,
1226+
BidderRequest bidderRequest,
1227+
Timeout timeout,
1228+
BidderAliases aliases) {
11561229

11571230
return bidderRequestPostProcessor.process(bidderRequest, aliases, auctionContext)
11581231
.compose(result -> invokeHooksAndRequestBids(auctionContext, result.getValue(), timeout, aliases)

src/main/java/org/prebid/server/auction/externalortb/StoredResponseProcessor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ private List<SeatBid> resolveSeatBids(StoredResponse storedResponse,
224224
Map<String, String> idToStoredResponses,
225225
String impId) {
226226

227-
if (storedResponse instanceof StoredResponse.StoredResponseObject storedResponseObject) {
228-
return Collections.singletonList(storedResponseObject.seatBid());
227+
if (storedResponse instanceof StoredResponse.StoredResponseObject(SeatBid seatBid)) {
228+
return Collections.singletonList(seatBid);
229229
}
230230

231231
final String storedResponseId = ((StoredResponse.StoredResponseId) storedResponse).id();
@@ -313,7 +313,7 @@ private static AuctionParticipation updateStoredBidResponse(AuctionParticipation
313313
final BidRequest bidRequest = bidderRequest.getBidRequest();
314314

315315
final List<Imp> imps = bidRequest.getImp();
316-
// Аor now, Stored Bid Response works only for bid requests with single imp
316+
// For now, Stored Bid Response works only for bid requests with single imp
317317
if (imps.size() > 1 || StringUtils.isEmpty(bidderRequest.getStoredResponse())) {
318318
return auctionParticipation;
319319
}

src/main/java/org/prebid/server/auction/model/AuctionContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ public AuctionContext with(BidResponse bidResponse) {
8383
return this.toBuilder().bidResponse(bidResponse).build();
8484
}
8585

86+
public AuctionContext with(Map<String, BidRejectionTracker> bidRejectionTrackers) {
87+
return this.toBuilder().bidRejectionTrackers(bidRejectionTrackers).build();
88+
}
89+
8690
public AuctionContext with(List<AuctionParticipation> auctionParticipations) {
8791
return this.toBuilder().auctionParticipations(auctionParticipations).build();
8892
}

src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,49 @@ public BidRejectionTracker(String bidder, Set<String> involvedImpIds, double log
4545
rejections = new HashMap<>();
4646
}
4747

48-
public BidRejectionTracker(BidRejectionTracker anotherTracker, Set<String> additionalImpIds) {
49-
this.bidder = anotherTracker.bidder;
50-
this.logSamplingRate = anotherTracker.logSamplingRate;
51-
this.involvedImpIds = new HashSet<>(anotherTracker.involvedImpIds);
52-
this.involvedImpIds.addAll(additionalImpIds);
53-
54-
this.succeededBidsIds = new HashMap<>(anotherTracker.succeededBidsIds);
55-
this.rejections = new HashMap<>(anotherTracker.rejections);
48+
private BidRejectionTracker(
49+
String bidder,
50+
Set<String> involvedImpIds,
51+
Map<String, Set<String>> succeededBidsIds,
52+
Map<String, List<Rejection>> rejections,
53+
double logSamplingRate
54+
) {
55+
this.bidder = bidder;
56+
this.involvedImpIds = new HashSet<>(involvedImpIds);
57+
this.logSamplingRate = logSamplingRate;
58+
59+
this.succeededBidsIds = MapUtil.mapValues(succeededBidsIds, v -> new HashSet<>(v));
60+
this.rejections = MapUtil.mapValues(rejections, ArrayList::new);
5661
}
5762

58-
public void succeed(Collection<BidderBid> bids) {
63+
public static BidRejectionTracker copyOf(BidRejectionTracker anotherTracker) {
64+
return new BidRejectionTracker(
65+
anotherTracker.bidder,
66+
anotherTracker.involvedImpIds,
67+
anotherTracker.succeededBidsIds,
68+
anotherTracker.rejections,
69+
anotherTracker.logSamplingRate
70+
);
71+
}
72+
73+
public static BidRejectionTracker withAdditionalImpIds(
74+
BidRejectionTracker anotherTracker,
75+
Set<String> additionalImpIds
76+
) {
77+
final BidRejectionTracker newTracker = copyOf(anotherTracker);
78+
newTracker.involvedImpIds.addAll(additionalImpIds);
79+
return newTracker;
80+
}
81+
82+
public BidRejectionTracker succeed(Collection<BidderBid> bids) {
5983
bids.stream()
6084
.map(BidderBid::getBid)
6185
.filter(Objects::nonNull)
6286
.forEach(this::succeed);
87+
return this;
6388
}
6489

65-
private void succeed(Bid bid) {
90+
private BidRejectionTracker succeed(Bid bid) {
6691
final String bidId = bid.getId();
6792
final String impId = bid.getImpid();
6893
if (involvedImpIds.contains(impId)) {
@@ -73,21 +98,23 @@ private void succeed(Bid bid) {
7398
logSamplingRate);
7499
}
75100
}
101+
return this;
76102
}
77103

78104
public void restoreFromRejection(Collection<BidderBid> bids) {
79105
succeed(bids);
80106
}
81107

82-
public void reject(Collection<Rejection> rejections) {
108+
public BidRejectionTracker reject(Collection<Rejection> rejections) {
83109
rejections.forEach(this::reject);
110+
return this;
84111
}
85112

86-
public void reject(Rejection rejection) {
113+
public BidRejectionTracker reject(Rejection rejection) {
87114
if (rejection instanceof ImpRejection && rejection.reason().getValue() >= 300) {
88115
logger.warn("The rejected imp {} with the code {} equal to or higher than 300 assumes "
89116
+ "that there is a rejected bid that shouldn't be lost");
90-
return;
117+
return this;
91118
}
92119

93120
final String impId = rejection.impId();
@@ -113,14 +140,18 @@ public void reject(Rejection rejection) {
113140
}
114141
}
115142
}
143+
144+
return this;
116145
}
117146

118-
public void rejectImps(Collection<String> impIds, BidRejectionReason reason) {
147+
public BidRejectionTracker rejectImps(Collection<String> impIds, BidRejectionReason reason) {
119148
impIds.forEach(impId -> reject(ImpRejection.of(impId, reason)));
149+
return this;
120150
}
121151

122-
public void rejectAll(BidRejectionReason reason) {
152+
public BidRejectionTracker rejectAll(BidRejectionReason reason) {
123153
involvedImpIds.forEach(impId -> reject(ImpRejection.of(impId, reason)));
154+
return this;
124155
}
125156

126157
public Set<Rejection> getRejected() {

src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.prebid.server.spring.config.bidder.model.MediaType;
1010

1111
import java.util.Map;
12+
import java.util.Set;
1213

1314
@Builder(toBuilder = true)
1415
@Value
@@ -65,4 +66,7 @@ public class AccountAuctionConfig {
6566
Integer impressionLimit;
6667

6768
AccountProfilesConfig profiles;
69+
70+
@JsonProperty("secondarybidders")
71+
Set<String> secondaryBidders;
6872
}

src/main/java/org/prebid/server/util/MapUtil.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import java.util.Collections;
44
import java.util.HashMap;
55
import java.util.Map;
6+
import java.util.function.BiFunction;
7+
import java.util.function.Function;
8+
import java.util.stream.Collectors;
69

710
public class MapUtil {
811

@@ -15,4 +18,23 @@ public static <T, U> Map<T, U> merge(Map<T, U> left, Map<T, U> right) {
1518

1619
return Collections.unmodifiableMap(merged);
1720
}
21+
22+
public static <K, V1, V2> Map<K, V2> mapValues(Map<K, V1> map, Function<V1, V2> transform) {
23+
return mapValues(map, (ignored, value) -> transform.apply(value));
24+
}
25+
26+
public static <K, V1, V2> Map<K, V2> mapValues(Map<K, V1> map, BiFunction<K, V1, V2> transform) {
27+
return map.entrySet().stream().collect(
28+
Collectors.toMap(Map.Entry::getKey, entry -> transform.apply(entry.getKey(), entry.getValue())));
29+
}
30+
31+
public static <K, V1, V2> Function<Map.Entry<K, V1>, Map.Entry<K, V2>> mapEntryValueMapper(
32+
BiFunction<K, V1, V2> transform) {
33+
34+
return mapEntryMapper((key, value) -> Map.entry(key, transform.apply(key, value)));
35+
}
36+
37+
public static <K, V, T> Function<Map.Entry<K, V>, T> mapEntryMapper(BiFunction<K, V, T> transform) {
38+
return entry -> transform.apply(entry.getKey(), entry.getValue());
39+
}
1840
}

0 commit comments

Comments
 (0)