Skip to content

Commit

Permalink
tmax adjustments for individual adapters (#3615)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Jan 8, 2025
1 parent a6c719e commit ba33fac
Show file tree
Hide file tree
Showing 17 changed files with 192 additions and 50 deletions.
1 change: 1 addition & 0 deletions docs/config-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ There are several typical keys:
- `adapters.<BIDDER_NAME>.usersync.type` - usersync type (i.e. redirect, iframe).
- `adapters.<BIDDER_NAME>.usersync.support-cors` - flag signals if CORS supported by usersync.
- `adapters.<BIDDER_NAME>.debug.allow` - enables debug output in the auction response for the given bidder. Default `true`.
- `adapters.<BIDDER_NAME>.tmax-deduction-ms` - adjusts the tmax sent to the bidder by deducting the provided value (ms). Default `0 ms` - no deduction.

In addition, each bidder could have arbitrary aliases configured that will look and act very much the same as the bidder itself.
Aliases are configured by adding child configuration object at `adapters.<BIDDER_NAME>.aliases.<BIDDER_ALIAS>.`, aliases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ private static BidderInfo bidderInfo(OrtbVersion ortbVersion) {
false,
false,
null,
Ortb.of(false));
Ortb.of(false),
0L);
}

private static BidRequest emptyRequest() {
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/org/prebid/server/auction/ExchangeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,7 @@ private Future<BidderResponse> requestBids(BidderRequest bidderRequest,
final String bidderName = bidderRequest.getBidder();
final String resolvedBidderName = aliases.resolveBidder(bidderName);
final Bidder<?> bidder = bidderCatalog.bidderByName(resolvedBidderName);
final long bidderTmaxDeductionMs = bidderCatalog.bidderInfoByName(resolvedBidderName).getTmaxDeductionMs();
final BidRejectionTracker bidRejectionTracker = auctionContext.getBidRejectionTrackers().get(bidderName);

final TimeoutContext timeoutContext = auctionContext.getTimeoutContext();
Expand All @@ -1225,7 +1226,8 @@ private Future<BidderResponse> requestBids(BidderRequest bidderRequest,
final long bidderRequestStartTime = clock.millis();

return Future.succeededFuture(bidderRequest.getBidRequest())
.map(bidRequest -> adjustTmax(bidRequest, auctionStartTime, adjustmentFactor, bidderRequestStartTime))
.map(bidRequest -> adjustTmax(
bidRequest, auctionStartTime, adjustmentFactor, bidderRequestStartTime, bidderTmaxDeductionMs))
.map(bidRequest -> ortbVersionConversionManager.convertFromAuctionSupportedVersion(
bidRequest, bidderRequest.getOrtbVersion()))
.map(bidderRequest::with)
Expand All @@ -1240,9 +1242,16 @@ private Future<BidderResponse> requestBids(BidderRequest bidderRequest,
.map(seatBid -> BidderResponse.of(bidderName, seatBid, responseTime(bidderRequestStartTime)));
}

private BidRequest adjustTmax(BidRequest bidRequest, long startTime, int adjustmentFactor, long currentTime) {
private BidRequest adjustTmax(BidRequest bidRequest,
long startTime,
int adjustmentFactor,
long currentTime,
long bidderTmaxDeductionMs) {

final long tmax = timeoutResolver.limitToMax(bidRequest.getTmax());
final long adjustedTmax = timeoutResolver.adjustForBidder(tmax, adjustmentFactor, currentTime - startTime);
final long adjustedTmax = timeoutResolver.adjustForBidder(
tmax, adjustmentFactor, currentTime - startTime, bidderTmaxDeductionMs);

return tmax != adjustedTmax
? bidRequest.toBuilder().tmax(adjustedTmax).build()
: bidRequest;
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/prebid/server/auction/TimeoutResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ public long limitToMax(Long timeout) {
: Math.min(timeout, maxTimeout);
}

public long adjustForBidder(long timeout, int adjustFactor, long spentTime) {
return adjustWithFactor(timeout, adjustFactor / 100.0, spentTime);
public long adjustForBidder(long timeout, int adjustFactor, long spentTime, long bidderTmaxDeductionMs) {
return adjustWithFactor(timeout, adjustFactor / 100.0, spentTime, bidderTmaxDeductionMs);
}

public long adjustForRequest(long timeout, long spentTime) {
return adjustWithFactor(timeout, 1.0, spentTime);
return adjustWithFactor(timeout, 1.0, spentTime, 0L);
}

private long adjustWithFactor(long timeout, double adjustFactor, long spentTime) {
return limitToMin((long) (timeout * adjustFactor) - spentTime - upstreamResponseTime);
private long adjustWithFactor(long timeout, double adjustFactor, long spentTime, long deductionTime) {
return limitToMin((long) (timeout * adjustFactor) - spentTime - deductionTime - upstreamResponseTime);
}

private long limitToMin(long timeout) {
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/org/prebid/server/bidder/BidderInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class BidderInfo {

Ortb ortb;

long tmaxDeductionMs;

public static BidderInfo create(boolean enabled,
OrtbVersion ortbVersion,
boolean debugAllowed,
Expand All @@ -55,7 +57,8 @@ public static BidderInfo create(boolean enabled,
boolean ccpaEnforced,
boolean modifyingVastXmlAllowed,
CompressionType compressionType,
org.prebid.server.spring.config.bidder.model.Ortb ortb) {
org.prebid.server.spring.config.bidder.model.Ortb ortb,
long tmaxDeductionMs) {

return of(
enabled,
Expand All @@ -74,7 +77,8 @@ public static BidderInfo create(boolean enabled,
ccpaEnforced,
modifyingVastXmlAllowed,
compressionType,
Ortb.of(ortb.getMultiFormatSupported()));
Ortb.of(ortb.getMultiFormatSupported()),
tmaxDeductionMs);
}

private static PlatformInfo platformInfo(List<MediaType> mediaTypes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public class BidderConfigurationProperties {

private Ortb ortb;

private long tmaxDeductionMs;

private final Class<? extends BidderConfigurationProperties> selfClass;

public BidderConfigurationProperties() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public static BidderInfo create(BidderConfigurationProperties configurationPrope
configurationProperties.getPbsEnforcesCcpa(),
configurationProperties.getModifyingVastXmlAllowed(),
configurationProperties.getEndpointCompression(),
configurationProperties.getOrtb());
configurationProperties.getOrtb(),
configurationProperties.getTmaxDeductionMs());
}
}
111 changes: 99 additions & 12 deletions src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ class TimeoutSpec extends BaseSpec {

private static final int DEFAULT_TIMEOUT = getRandomTimeout()
private static final int MIN_TIMEOUT = PBSUtils.getRandomNumber(50, 150)
private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String,
"auction.biddertmax.min" : MIN_TIMEOUT as String]
private static final Long MAX_AUCTION_BIDDER_TIMEOUT = 3000
private static final Long MIN_AUCTION_BIDDER_TIMEOUT = 1000
private static final Map PBS_CONFIG = ["auction.biddertmax.max": MAX_AUCTION_BIDDER_TIMEOUT as String,
"auction.biddertmax.min": MIN_AUCTION_BIDDER_TIMEOUT as String]

@Shared
PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG)
Expand Down Expand Up @@ -136,9 +138,10 @@ class TimeoutSpec extends BaseSpec {

and: "Pbs config with default request"
def pbsContainer = new PrebidServerContainer(
["default-request.file.path" : APP_WORKDIR + defaultRequest.fileName,
"auction.biddertmax.max" : MAX_TIMEOUT as String]).tap {
withCopyFileToContainer(MountableFile.forHostPath(defaultRequest), APP_WORKDIR) }
["default-request.file.path": APP_WORKDIR + defaultRequest.fileName,
"auction.biddertmax.max" : MAX_TIMEOUT as String]).tap {
withCopyFileToContainer(MountableFile.forHostPath(defaultRequest), APP_WORKDIR)
}
pbsContainer.start()
def pbsService = new PrebidServerService(pbsContainer)

Expand Down Expand Up @@ -284,8 +287,9 @@ class TimeoutSpec extends BaseSpec {
def "PBS should choose min timeout form config for bidder request when in request value lowest that in auction.biddertmax.min"() {
given: "PBS config with percent"
def minBidderTmax = PBSUtils.getRandomNumber(MIN_TIMEOUT, MAX_TIMEOUT)
def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.min" : minBidderTmax as String,
"auction.biddertmax.max" : MAX_TIMEOUT as String])
def prebidServerService = pbsServiceFactory.getService(
["auction.biddertmax.min": minBidderTmax as String,
"auction.biddertmax.max": MAX_TIMEOUT as String])

and: "Default basic BidRequest"
def timeout = PBSUtils.getRandomNumber(0, minBidderTmax)
Expand All @@ -307,11 +311,14 @@ class TimeoutSpec extends BaseSpec {
def "PBS should change timeout for bidder due to percent in auction.biddertmax.percent"() {
given: "PBS config with percent"
def percent = PBSUtils.getRandomNumber(2, 98)
def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.percent": percent as String]
+ PBS_CONFIG)
def pbsConfig = ["auction.biddertmax.percent": percent as String,
"auction.biddertmax.max" : MAX_TIMEOUT as String,
"auction.biddertmax.min" : MIN_TIMEOUT as String]
def prebidServerService = pbsServiceFactory.getService(
pbsConfig)

and: "Default basic BidRequest with generic bidder"
def timeout = getRandomTimeout()
def timeout = randomTimeout
def bidRequest = BidRequest.defaultBidRequest.tap {
tmax = timeout
}
Expand All @@ -321,17 +328,97 @@ class TimeoutSpec extends BaseSpec {

then: "Bidder request should contain percent of request value"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert isInternalProcessingTime(bidderRequest.tmax, getPercentOfValue(percent,timeout))
assert isInternalProcessingTime(bidderRequest.tmax, getPercentOfValue(percent, timeout))

and: "PBS response should contain tmax from request"
assert bidResponse?.ext?.tmaxrequest == timeout as Long

cleanup: "Stop and remove pbs container"
pbsServiceFactory.removeContainer(pbsConfig)
}

def "PBS should apply auction.biddertmax.max timeout when adapters.generic.tmax-deduction-ms exceeds valid top range"() {
given: "PBS config with adapters.generic.tmax-deduction-ms"
def pbsConfig = PBS_CONFIG + ["adapters.generic.tmax-deduction-ms": PBSUtils.getRandomNumber(MAX_AUCTION_BIDDER_TIMEOUT as int) as String]
def prebidServerService = pbsServiceFactory.getService(pbsConfig)

and: "Default basic BidRequest with generic bidder"
def bidRequest = BidRequest.defaultBidRequest.tap {
tmax = randomTimeout
}

when: "PBS processes auction request"
def bidResponse = prebidServerService.sendAuctionRequest(bidRequest)

then: "Bidder request should contain min"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert bidderRequest.tmax == MIN_AUCTION_BIDDER_TIMEOUT

and: "PBS response should contain tmax"
assert bidResponse?.ext?.tmaxrequest == MAX_AUCTION_BIDDER_TIMEOUT

cleanup: "Stop and remove pbs container"
pbsServiceFactory.removeContainer(pbsConfig)
}

def "PBS should resolve timeout as usual when adapters.generic.tmax-deduction-ms specifies zero"() {
given: "PBS config with adapters.generic.tmax-deduction-ms"
def pbsConfig = ["adapters.generic.tmax-deduction-ms": "0"] + PBS_CONFIG
def prebidServerService = pbsServiceFactory.getService(pbsConfig)

and: "Default basic BidRequest with generic bidder"
def timeout = PBSUtils.getRandomNumber(
MIN_AUCTION_BIDDER_TIMEOUT as int,
MAX_AUCTION_BIDDER_TIMEOUT as int)
def bidRequest = BidRequest.defaultBidRequest.tap {
tmax = timeout
}

when: "PBS processes auction request"
def bidResponse = prebidServerService.sendAuctionRequest(bidRequest)

then: "Bidder request should contain right value in tmax"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert isInternalProcessingTime(bidderRequest.tmax, timeout)

and: "PBS response should contain tmax"
assert bidResponse?.ext?.tmaxrequest == timeout as Long

cleanup: "Stop and remove pbs container"
pbsServiceFactory.removeContainer(pbsConfig)
}

def "PBS should properly resolve tmax deduction ms when adapters.generic.tmax-deduction-ms specified"() {
given: "PBS config with adapters.generic.tmax-deduction-ms"
def genericDeductionMs = PBSUtils.getRandomNumber(100, 300)
def randomTimeout = PBSUtils.getRandomNumber(MIN_AUCTION_BIDDER_TIMEOUT + genericDeductionMs as int, MAX_AUCTION_BIDDER_TIMEOUT as int)
def pbsConfig = PBS_CONFIG + ["adapters.generic.tmax-deduction-ms": genericDeductionMs as String]
def prebidServerService = pbsServiceFactory.getService(pbsConfig)

and: "Default basic BidRequest with generic bidder"
def bidRequest = BidRequest.defaultBidRequest.tap {
tmax = randomTimeout
}

when: "PBS processes auction request"
def bidResponse = prebidServerService.sendAuctionRequest(bidRequest)

then: "Bidder request should contain right value in tmax"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert isInternalProcessingTime(bidderRequest.tmax, randomTimeout)

and: "PBS response should contain tmax"
assert bidResponse?.ext?.tmaxrequest == randomTimeout as Long

cleanup: "Stop and remove pbs container"
pbsServiceFactory.removeContainer(pbsConfig)
}

private static long getPercentOfValue(int percent, int value) {
(percent * value) / 100.0 as Long
}

private static boolean isInternalProcessingTime(long bidderRequestTimeout, long requestTimeout){
private static boolean isInternalProcessingTime(long bidderRequestTimeout, long requestTimeout) {
0 < requestTimeout - bidderRequestTimeout
}
}
37 changes: 29 additions & 8 deletions src/test/java/org/prebid/server/auction/ExchangeServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ public void setUp() {
false,
false,
CompressionType.NONE,
Ortb.of(false)));
Ortb.of(false),
0L));

given(privacyEnforcementService.mask(any(), argThat(MapUtils::isNotEmpty), any()))
.willAnswer(inv ->
Expand Down Expand Up @@ -381,7 +382,7 @@ public void setUp() {
given(criteriaLogManager.traceResponse(any(), any(), any(), anyBoolean()))
.willAnswer(inv -> inv.getArgument(1));

given(timeoutResolver.adjustForBidder(anyLong(), anyInt(), anyLong()))
given(timeoutResolver.adjustForBidder(anyLong(), anyInt(), anyLong(), anyLong()))
.willAnswer(invocation -> invocation.getArgument(0));

given(timeoutResolver.adjustForRequest(anyLong(), anyLong()))
Expand Down Expand Up @@ -3774,7 +3775,9 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() {
false,
false,
CompressionType.NONE,
Ortb.of(false)));
Ortb.of(false),
0L));

given(bidResponseCreator.create(
argThat(argument -> argument.getAuctionParticipations().getFirst()
.getBidderResponse()
Expand Down Expand Up @@ -3837,17 +3840,35 @@ public void shouldConvertBidRequestOpenRTBVersionToConfiguredByBidder() {
@Test
public void shouldPassAdjustedTimeoutToAdapterAndToBidResponseCreator() {
// given
given(timeoutResolver.adjustForBidder(anyLong(), eq(90), anyLong()))
.willReturn(400L);
given(timeoutResolver.adjustForRequest(anyLong(), anyLong()))
.willReturn(450L);
given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create(
true,
null,
false,
null,
null,
null,
null,
null,
null,
null,
0,
null,
false,
false,
CompressionType.NONE,
Ortb.of(false),
100L));

given(timeoutResolver.adjustForBidder(anyLong(), eq(90), eq(200L), eq(100L))).willReturn(400L);
given(timeoutResolver.adjustForRequest(anyLong(), eq(200L))).willReturn(450L);

final BidRequest bidRequest = givenBidRequest(
givenSingleImp(singletonMap("bidderName", 1)),
request -> request.source(Source.builder().tid("uniqTid").build()));

// when
target.holdAuction(givenRequestContext(bidRequest));
target.holdAuction(givenRequestContext(bidRequest).toBuilder()
.timeoutContext(TimeoutContext.of(clock.millis() - 200L, timeout, 90)).build());

// then
final ArgumentCaptor<BidderRequest> bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ public void limitToMaxShouldReturnMaxTimeout() {

@Test
public void adjustForBidderShouldReturnExpectedResult() {
assertThat(timeoutResolver.adjustForBidder(200L, 70, 10L)).isEqualTo(120L);
assertThat(timeoutResolver.adjustForBidder(300L, 70, 10L, 50L)).isEqualTo(140L);
}

@Test
public void adjustForBidderShouldReturnMinTimeout() {
assertThat(timeoutResolver.adjustForBidder(200L, 50, 10L)).isEqualTo(MIN_TIMEOUT);
assertThat(timeoutResolver.adjustForBidder(200L, 50, 10L, 100L)).isEqualTo(MIN_TIMEOUT);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ private static BidderInfo givenBidderInfo(List<MediaType> appMediaTypes,
false,
false,
CompressionType.NONE,
Ortb.of(false));
Ortb.of(false),
0L);
}

private static BidRequest givenBidRequest(UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ private static BidderInfo givenBidderInfo(boolean multiFormatSupported) {
false,
false,
CompressionType.NONE,
Ortb.of(multiFormatSupported));
Ortb.of(multiFormatSupported),
0L);
}

private static BidRequest givenBidRequest(UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
Expand Down
Loading

0 comments on commit ba33fac

Please sign in to comment.