Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tmax adjustments for individual adapters #3615

Merged
merged 5 commits into from
Jan 8, 2025
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
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
Loading