From 625f83a2cee8ee8e900d79d40360a3a68af34990 Mon Sep 17 00:00:00 2001 From: Danylo Date: Wed, 11 Jun 2025 00:19:14 +0200 Subject: [PATCH 01/24] Move `StoredRequestProcessor` and `StoredResponseProcessor` --- .../server/auction/BidResponseCreator.java | 1 + .../server/auction/ExchangeService.java | 11 +++++----- .../server/auction/SkippedAuctionService.java | 1 + .../StoredRequestProcessor.java | 10 ++++----- .../StoredResponseProcessor.java | 14 ++++++------ .../requestfactory/AmpRequestFactory.java | 2 +- .../requestfactory/AuctionRequestFactory.java | 2 +- .../requestfactory/Ortb2RequestFactory.java | 2 +- .../spring/config/ServiceConfiguration.java | 8 +++---- .../auction/BidResponseCreatorTest.java | 4 +++- .../server/auction/ExchangeServiceTest.java | 1 + .../auction/SkippedAuctionServiceTest.java | 1 + .../StoredRequestProcessorTest.java | 2 +- .../StoredResponseProcessorTest.java | 2 +- .../requestfactory/AmpRequestFactoryTest.java | 2 +- .../AuctionRequestFactoryTest.java | 4 ++-- .../Ortb2RequestFactoryTest.java | 22 +++++++++---------- 17 files changed, 48 insertions(+), 41 deletions(-) rename src/main/java/org/prebid/server/auction/{ => externalortb}/StoredRequestProcessor.java (97%) rename src/main/java/org/prebid/server/auction/{ => externalortb}/StoredResponseProcessor.java (97%) rename src/test/java/org/prebid/server/auction/{ => externalortb}/StoredRequestProcessorTest.java (99%) rename src/test/java/org/prebid/server/auction/{ => externalortb}/StoredResponseProcessorTest.java (99%) diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index dd8f79c1920..aec0c82831b 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.auction.categorymapping.CategoryMappingService; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidInfo; diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 04191d0839d..d9561ccc868 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -23,6 +23,7 @@ import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -31,6 +32,7 @@ import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; @@ -101,7 +103,6 @@ import org.prebid.server.util.ListUtil; import org.prebid.server.util.PbsUtil; import org.prebid.server.util.StreamUtil; -import org.apache.commons.lang3.tuple.Pair; import java.math.BigDecimal; import java.time.Clock; @@ -570,10 +571,10 @@ private static List firstPartyDataBidders(ExtRequest requestExt) { } private Map> prepareUsersAndDevices(List bidders, - AuctionContext context, - BidderAliases aliases, - Map biddersToConfigs, - Map> eidPermissions) { + AuctionContext context, + BidderAliases aliases, + Map biddersToConfigs, + Map> eidPermissions) { final BidRequest bidRequest = context.getBidRequest(); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); diff --git a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java index 1eb1ee1318b..78e87a44799 100644 --- a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java +++ b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java @@ -7,6 +7,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.bidder.model.BidderError; diff --git a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java similarity index 97% rename from src/main/java/org/prebid/server/auction/StoredRequestProcessor.java rename to src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java index 3729c5da661..e02b17b157f 100644 --- a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction; +package org.prebid.server.auction.externalortb; import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.BidRequest; @@ -130,10 +130,10 @@ private Future processAmpStoredRequest(String accountId, String ampR .map(this::generateBidRequestId); } - Future videoStoredDataResult(String accountId, - List imps, - List errors, - Timeout timeout) { + public Future videoStoredDataResult(String accountId, + List imps, + List errors, + Timeout timeout) { return videoStoredDataResultInternal(accountId, imps, errors, timeout) .onFailure(cause -> updateInvalidStoredResultMetrics(accountId, cause)) diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/StoredResponseProcessor.java similarity index 97% rename from src/main/java/org/prebid/server/auction/StoredResponseProcessor.java rename to src/main/java/org/prebid/server/auction/externalortb/StoredResponseProcessor.java index 2257320ef69..b44939a38e6 100644 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/StoredResponseProcessor.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction; +package org.prebid.server.auction.externalortb; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -67,7 +67,7 @@ public StoredResponseProcessor(ApplicationSettings applicationSettings, this.mapper = Objects.requireNonNull(mapper); } - Future getStoredResponseResult(List imps, Timeout timeout) { + public Future getStoredResponseResult(List imps, Timeout timeout) { final Map impExtPrebids = getImpsExtPrebid(imps); final Map impIdsToStoredResponses = getAuctionStoredResponses(impExtPrebids); final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, impIdsToStoredResponses); @@ -96,7 +96,7 @@ Future getStoredResponseResult(List imps, Timeout tim impToBidderToStoredBidResponseId))); } - Future getStoredResponseResult(String storedId, Timeout timeout) { + public Future 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()))) @@ -345,10 +345,10 @@ private static BidderBid resolveBidImpId(BidderBid bidderBid, String impId) { .build(); } - List mergeWithBidderResponses(List auctionParticipations, - List storedAuctionResponses, - List imps, - Map bidRejectionTrackers) { + public List mergeWithBidderResponses(List auctionParticipations, + List storedAuctionResponses, + List imps, + Map bidRejectionTrackers) { if (CollectionUtils.isEmpty(storedAuctionResponses)) { return auctionParticipations; diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index 6d2ac3ee72d..032ead0978b 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -23,7 +23,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.OrtbTypesResolver; import org.prebid.server.auction.PriceGranularity; -import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AmpGppService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.ConsentType; diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 628ea212fd8..11f2588ef73 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -11,7 +11,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.OrtbTypesResolver; -import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AuctionGppService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionStoredResult; diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 5a122d5a64e..48ef9fa453b 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -22,8 +22,8 @@ import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.auction.IpAddressHelper; -import org.prebid.server.auction.StoredRequestProcessor; import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.auction.model.TimeoutContext; diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index aa57b0b04b7..e2841fd36a9 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -27,18 +27,17 @@ import org.prebid.server.auction.OrtbTypesResolver; import org.prebid.server.auction.SecBrowsingTopicsResolver; import org.prebid.server.auction.SkippedAuctionService; -import org.prebid.server.auction.StoredRequestProcessor; -import org.prebid.server.auction.StoredResponseProcessor; import org.prebid.server.auction.SupplyChainResolver; import org.prebid.server.auction.TimeoutResolver; import org.prebid.server.auction.UidUpdater; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.WinningBidComparatorFactory; -import org.prebid.server.bidadjustments.BidAdjustmentFactorResolver; import org.prebid.server.auction.categorymapping.BasicCategoryMappingService; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.categorymapping.NoOpCategoryMappingService; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.gpp.AmpGppService; import org.prebid.server.auction.gpp.AuctionGppService; import org.prebid.server.auction.gpp.CookieSyncGppService; @@ -65,9 +64,10 @@ import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverterFactory; +import org.prebid.server.bidadjustments.BidAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; import org.prebid.server.bidadjustments.BidAdjustmentsResolver; -import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderDeps; diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 83deee74ce8..d956beae62f 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -35,6 +35,7 @@ import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.activity.infrastructure.rule.Rule; import org.prebid.server.auction.categorymapping.CategoryMappingService; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidInfo; @@ -1106,7 +1107,8 @@ public void shouldSetExpectedBidsWithRanksWhenBidRankingEnabled() { contextBuilder -> contextBuilder .auctionParticipations(toAuctionParticipant(bidderResponses)) .account(Account.builder().auction(AccountAuctionConfig.builder() - .ranking(AccountBidRankingConfig.of(true)).build()) + .ranking(AccountBidRankingConfig.of(true)) + .build()) .build())); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build(); diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index a913772645e..0acce691b01 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -43,6 +43,7 @@ import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; diff --git a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java index 7f6e1623b37..854fef12ac8 100644 --- a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java +++ b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.auction.model.TimeoutContext; diff --git a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/externalortb/StoredRequestProcessorTest.java similarity index 99% rename from src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java rename to src/test/java/org/prebid/server/auction/externalortb/StoredRequestProcessorTest.java index 9b9aa5aba48..f1fb8f30f24 100644 --- a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/externalortb/StoredRequestProcessorTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction; +package org.prebid.server.auction.externalortb; import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.App; diff --git a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java b/src/test/java/org/prebid/server/auction/externalortb/StoredResponseProcessorTest.java similarity index 99% rename from src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java rename to src/test/java/org/prebid/server/auction/externalortb/StoredResponseProcessorTest.java index dc57b3c1f11..d281139df5f 100644 --- a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/externalortb/StoredResponseProcessorTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction; +package org.prebid.server.auction.externalortb; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index fe4c72b89ca..d208c5ee3d3 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -30,7 +30,7 @@ import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.OrtbTypesResolver; -import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AmpGppService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.debug.DebugContext; diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 19e414e80c9..237c95da851 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -30,7 +30,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.OrtbTypesResolver; -import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AuctionGppService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionStoredResult; @@ -39,6 +39,7 @@ import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.geolocation.model.GeoInfo; @@ -52,7 +53,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index d5c2cb5bea3..55abf02b786 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -26,8 +26,8 @@ import org.prebid.server.VertxTest; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.auction.IpAddressHelper; -import org.prebid.server.auction.StoredRequestProcessor; import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.model.IpAddress; @@ -1028,11 +1028,11 @@ public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressV4FromPri // when final Future result = target.enrichBidRequestWithAccountAndPrivacyData( - AuctionContext.builder() - .bidRequest(bidRequest) - .account(account) - .privacyContext(privacyContext) - .build()); + AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .privacyContext(privacyContext) + .build()); // then assertThat(result).isSucceeded().unwrap() @@ -1061,11 +1061,11 @@ public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressV6FromPri // when final Future result = target.enrichBidRequestWithAccountAndPrivacyData( - AuctionContext.builder() - .bidRequest(bidRequest) - .account(account) - .privacyContext(privacyContext) - .build()); + AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .privacyContext(privacyContext) + .build()); // then assertThat(result).isSucceeded().unwrap() From 5b376a5fdc3c8eed411673d4fae440243ac30af3 Mon Sep 17 00:00:00 2001 From: Danylo Date: Wed, 11 Jun 2025 03:46:10 +0200 Subject: [PATCH 02/24] Initial implementation --- .../externalortb/ProfilesProcessor.java | 210 ++++++++++++++++++ .../requestfactory/Ortb2RequestFactory.java | 1 + .../exception/InvalidProfileException.java | 14 ++ .../openrtb/ext/request/ExtImpPrebid.java | 5 + .../openrtb/ext/request/ExtRequestPrebid.java | 5 + .../server/settings/ApplicationSettings.java | 15 ++ .../settings/model/AccountAuctionConfig.java | 2 + .../settings/model/AccountProfilesConfig.java | 18 ++ .../prebid/server/settings/model/Profile.java | 26 +++ .../settings/model/StoredProfileResult.java | 16 ++ 10 files changed, 312 insertions(+) create mode 100644 src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java create mode 100644 src/main/java/org/prebid/server/exception/InvalidProfileException.java create mode 100644 src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java create mode 100644 src/main/java/org/prebid/server/settings/model/Profile.java create mode 100644 src/main/java/org/prebid/server/settings/model/StoredProfileResult.java diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java new file mode 100644 index 00000000000..9d81563d7b7 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -0,0 +1,210 @@ +package org.prebid.server.auction.externalortb; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import io.vertx.core.Future; +import org.prebid.server.exception.InvalidProfileException; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.metric.Metrics; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; +import org.prebid.server.settings.model.StoredProfileResult; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class ProfilesProcessor { + + private final int maxProfiles; + private final long defaultTimeoutMillis; + private final ApplicationSettings applicationSettings; + private final TimeoutFactory timeoutFactory; + private final Metrics metrics; + private final JacksonMapper mapper; + private final JsonMerger jsonMerger; + + public ProfilesProcessor(int maxProfiles, + long defaultTimeoutMillis, + ApplicationSettings applicationSettings, + TimeoutFactory timeoutFactory, + Metrics metrics, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + this.maxProfiles = maxProfiles; + this.defaultTimeoutMillis = defaultTimeoutMillis; + this.applicationSettings = Objects.requireNonNull(applicationSettings); + this.timeoutFactory = Objects.requireNonNull(timeoutFactory); + this.metrics = Objects.requireNonNull(metrics); + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public Future process(Account account, BidRequest bidRequest) { + final List imps = bidRequest.getImp(); + + final AllProfilesIds profilesIds = truncate(new AllProfilesIds( + requestProfilesIds(bidRequest), + imps.stream() + .map(this::impProfilesIds) + .toList())); + + if (profilesIds.isEmpty()) { + return Future.succeededFuture(bidRequest); + } + + return fetchProfiles(account.getId(), profilesIds, timeoutMillis(bidRequest)) + .map(profiles -> mergeResults( + applyRequestProfiles(profilesIds.request(), profiles.getIdToRequestProfile(), bidRequest), + applyImpsProfiles(profilesIds.imps(), profiles.getIdToImpProfile(), imps))) + .recover(e -> Future.failedFuture( + new InvalidRequestException("Error during processing profiles: " + e.getMessage()))); + } + + private static List requestProfilesIds(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getProfiles) + .orElse(Collections.emptyList()); + } + + private List impProfilesIds(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("prebid")) + .map(this::parseImpExt) + .map(ExtImpPrebid::getProfiles) + .orElse(Collections.emptyList()); + } + + private ExtImpPrebid parseImpExt(JsonNode jsonNode) { + try { + return mapper.mapper().treeToValue(jsonNode, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new InvalidProfileException(e.getMessage()); + } + } + + private AllProfilesIds truncate(AllProfilesIds profilesIds) { + // TODO: + // 1. How to limit for multiple imps (each contains profiles)? + // 2. Which of these approaches is correct? + // - limit -> fetch + // - fetch -> limit (don't count invalid profiles) + // - fetch -> limit (count invalid profiles) + return profilesIds; + } + + private long timeoutMillis(BidRequest bidRequest) { + final Long tmax = bidRequest.getTmax(); + return tmax != null && tmax > 0 ? tmax : defaultTimeoutMillis; + } + + private Future fetchProfiles(String accountId, + AllProfilesIds allProfilesIds, + long timeoutMillis) { + + final Set requestProfilesIds = new HashSet<>(allProfilesIds.request()); + final Set impProfilesIds = allProfilesIds.imps().stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + final Timeout timeout = timeoutFactory.create(timeoutMillis); + + return applicationSettings.getProfiles(accountId, requestProfilesIds, impProfilesIds, timeout) + .compose(profiles -> profiles.getErrors().isEmpty() + ? Future.succeededFuture(profiles) + : Future.failedFuture(new InvalidProfileException(profiles.getErrors()))); + } + + private BidRequest applyRequestProfiles(List profilesIds, + Map idToRequestProfile, + BidRequest bidRequest) { + + return !idToRequestProfile.isEmpty() + ? applyProfiles(profilesIds, idToRequestProfile, bidRequest, BidRequest.class) + : bidRequest; + } + + private T applyProfiles(List profilesIds, + Map idToProfile, + T original, + Class tClass) { + + if (profilesIds.isEmpty()) { + return original; + } + + ObjectNode result = mapper.mapper().valueToTree(original); + for (String profileId : profilesIds) { + final Profile profile = idToProfile.get(profileId); + result = mergeProfile(result, profile, profileId); + } + + try { + return mapper.mapper().treeToValue(result, tClass); + } catch (JsonProcessingException e) { + throw new InvalidProfileException(e.getMessage()); + } + } + + private ObjectNode mergeProfile(ObjectNode original, Profile profile, String profileId) { + return switch (profile.getMergePrecedence()) { + case REQUEST -> merge(original, profile.getBody(), profileId); + case PROFILE -> merge(profile.getBody(), original, profileId); + }; + } + + private ObjectNode merge(ObjectNode takePrecedence, ObjectNode other, String profileId) { + try { + return (ObjectNode) jsonMerger.merge(takePrecedence, other); + } catch (InvalidRequestException e) { + throw new InvalidProfileException("Can't merge with profile %s: %s".formatted(profileId, e.getMessage())); + } + } + + private List applyImpsProfiles(List> profilesIds, + Map idToImpProfile, + List imps) { + + if (idToImpProfile.isEmpty()) { + return imps; + } + + final List updatedImps = new ArrayList<>(imps); + for (int i = 0; i < profilesIds.size(); i++) { + updatedImps.set(i, applyProfiles(profilesIds.get(i), idToImpProfile, imps.get(i), Imp.class)); + } + + return Collections.unmodifiableList(updatedImps); + } + + private static BidRequest mergeResults(BidRequest bidRequest, List imps) { + return bidRequest.toBuilder().imp(imps).build(); + } + + private record AllProfilesIds(List request, List> imps) { + + public boolean isEmpty() { + return request.isEmpty() && imps.stream().allMatch(List::isEmpty); + } + } +} diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 48ef9fa453b..70f4f9b1135 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -455,6 +455,7 @@ private Future findAccountIdFrom(BidRequest bidRequest, boolean isLookup return StringUtils.isNotBlank(accountId) || !isLookupStoredRequest ? Future.succeededFuture(accountId) : storedRequestProcessor.processAuctionRequest(accountId, bidRequest) + // TODO: add profiles? .map(storedAuctionResult -> accountIdFrom(storedAuctionResult.bidRequest())); } diff --git a/src/main/java/org/prebid/server/exception/InvalidProfileException.java b/src/main/java/org/prebid/server/exception/InvalidProfileException.java new file mode 100644 index 00000000000..d8a3ebc3aae --- /dev/null +++ b/src/main/java/org/prebid/server/exception/InvalidProfileException.java @@ -0,0 +1,14 @@ +package org.prebid.server.exception; + +import java.util.List; + +public class InvalidProfileException extends RuntimeException { + + public InvalidProfileException(String message) { + super(message); + } + + public InvalidProfileException(List messages) { + super(String.join("\n", messages)); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java index 0d626029c9f..33ac57c13c0 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java @@ -20,6 +20,11 @@ public class ExtImpPrebid { */ ExtStoredRequest storedrequest; + /** + * Defines the contract for bidrequest.imp[i].ext.prebid.profiles + */ + List profiles; + /** * Defines the contract for bidrequest.imp[i].ext.prebid.storedauctionresponse */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index 98754a459a5..25dffdc486c 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -76,6 +76,11 @@ public class ExtRequestPrebid { */ ExtStoredRequest storedrequest; + /** + * Defines the contract for bidrequest.ext.prebid.profiles + */ + List profiles; + /** * Defines the contract for bidrequest.ext.prebid.storedauctionresponse */ diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index 7a6582ccd42..f8dcd00ef41 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -4,8 +4,10 @@ import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; +import java.util.Collections; import java.util.Map; import java.util.Set; @@ -44,6 +46,19 @@ Future getAmpStoredData(String accountId, Set requestI Future getVideoStoredData(String accountId, Set requestIds, Set impIds, Timeout timeout); + /** + * Fetches profiles for request and imps by IDs. + */ + // TODO: remove default + default Future getProfiles(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + + return Future.succeededFuture(StoredProfileResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())); + } + /** * Fetches stored response by IDs. */ diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index e41f005df54..41dd410e822 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -60,4 +60,6 @@ public class AccountAuctionConfig { AccountCacheConfig cache; AccountBidRankingConfig ranking; + + AccountProfilesConfig profiles; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java b/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java new file mode 100644 index 00000000000..fcfce2c47d3 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java @@ -0,0 +1,18 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountProfilesConfig { + + Integer limit; + + // TODO: need confirmation to move property + // TODO: Decide + // - skip all profiles on any invalid + // - skip only invalid + // TODO: metrics + @JsonAlias("fail-on-unknown") + Boolean failOnUnknown; +} diff --git a/src/main/java/org/prebid/server/settings/model/Profile.java b/src/main/java/org/prebid/server/settings/model/Profile.java new file mode 100644 index 00000000000..14dbc8e7326 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/Profile.java @@ -0,0 +1,26 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Profile { + + Type type; + + @JsonProperty("mergeprecedence") + MergePrecedence mergePrecedence; + + ObjectNode body; + + public enum Type { + + REQUEST, IMP + } + + public enum MergePrecedence { + + REQUEST, PROFILE + } +} diff --git a/src/main/java/org/prebid/server/settings/model/StoredProfileResult.java b/src/main/java/org/prebid/server/settings/model/StoredProfileResult.java new file mode 100644 index 00000000000..e3330a4262d --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/StoredProfileResult.java @@ -0,0 +1,16 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class StoredProfileResult { + + Map idToRequestProfile; + + Map idToImpProfile; + + List errors; +} From 0ea40b58c0e9c6d044e7bf2042b0cd614d4aab95 Mon Sep 17 00:00:00 2001 From: Danylo Date: Thu, 12 Jun 2025 17:54:59 +0200 Subject: [PATCH 03/24] Refactor `*ApplicationSettings` and some related classes --- .../server/settings/ApplicationSettings.java | 58 +--- .../settings/CachingApplicationSettings.java | 188 ++++++------- .../CompositeApplicationSettings.java | 237 +++++++++------- .../settings/DatabaseApplicationSettings.java | 150 +++++------ .../EnrichingApplicationSettings.java | 64 +++-- .../settings/FileApplicationSettings.java | 228 ++++++++-------- .../settings/HttpApplicationSettings.java | 255 +++++++++--------- .../settings/S3ApplicationSettings.java | 21 +- .../DatabaseStoredDataResultMapper.java | 196 +++++++------- .../DatabaseStoredResponseResultMapper.java | 34 +-- .../helper/ParametrizedQueryHelper.java | 3 +- .../helper/ParametrizedQueryMySqlHelper.java | 10 +- .../ParametrizedQueryPostgresHelper.java | 11 +- .../settings/helper/StoredDataFetcher.java | 17 +- .../settings/helper/StoredItemResolver.java | 6 +- .../helper/StoredResponseFetcher.java | 13 + .../spring/config/SettingsConfiguration.java | 23 +- .../settings/FileApplicationSettingsTest.java | 24 -- .../settings/HttpApplicationSettingsTest.java | 59 ++-- .../DatabaseStoredDataResultMapperTest.java | 6 +- ...atabaseStoredResponseResultMapperTest.java | 6 +- 21 files changed, 821 insertions(+), 788 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/helper/StoredResponseFetcher.java diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index f8dcd00ef41..64856f9d4fd 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -7,66 +7,34 @@ import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; -import java.util.Collections; import java.util.Map; import java.util.Set; -/** - * Defines the contract of getting application settings (account, stored ad unit configurations and - * stored requests and imps) from the source. - * - * @see FileApplicationSettings - * @see DatabaseApplicationSettings - * @see HttpApplicationSettings - * @see CachingApplicationSettings - * @see CompositeApplicationSettings - */ public interface ApplicationSettings { - /** - * Returns {@link Account} for the given account ID. - */ Future getAccountById(String accountId, Timeout timeout); - /** - * Fetches stored requests and imps by IDs. - */ - Future getStoredData(String accountId, Set requestIds, Set impIds, + Future getStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout); - /** - * Fetches AMP stored requests and imps by IDs. - */ - Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout); - /** - * Fetches Video stored requests and imps by IDs. - */ - Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout); - /** - * Fetches profiles for request and imps by IDs. - */ - // TODO: remove default - default Future getProfiles(String accountId, Set requestIds, Set impIds, - Timeout timeout) { + Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); - return Future.succeededFuture(StoredProfileResult.of( - Collections.emptyMap(), - Collections.emptyMap(), - Collections.emptyList())); - } - - /** - * Fetches stored response by IDs. - */ Future getStoredResponses(Set responseIds, Timeout timeout); - - /** - * Fetches video category - */ Future> getCategories(String primaryAdServer, String publisher, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 9f8fcea9ff2..95275aa076d 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -13,6 +13,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredItem; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.Collections; @@ -24,9 +25,6 @@ import java.util.function.BiFunction; import java.util.function.Consumer; -/** - * Adds caching functionality for {@link ApplicationSettings} implementation. - */ public class CachingApplicationSettings implements ApplicationSettings { private static final Logger logger = LoggerFactory.getLogger(CachingApplicationSettings.class); @@ -69,9 +67,6 @@ public CachingApplicationSettings(ApplicationSettings delegate, this.metrics = Objects.requireNonNull(metrics); } - /** - * Retrieves account from cache or delegates it to original fetcher. - */ @Override public Future getAccountById(String accountId, Timeout timeout) { return getFromCacheOrDelegate( @@ -83,58 +78,6 @@ public Future getAccountById(String accountId, Timeout timeout) { event -> metrics.updateSettingsCacheEventMetric(MetricName.account, event)); } - /** - * Retrieves stored data from cache or delegates it to original fetcher. - */ - @Override - public Future getStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - return getFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); - } - - /** - * Retrieves amp stored data from cache or delegates it to original fetcher. - */ - @Override - public Future getAmpStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - return getFromCacheOrDelegate(ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); - } - - @Override - public Future getVideoStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - return getFromCacheOrDelegate(videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); - } - - /** - * Delegates stored response retrieve to original fetcher, as caching is not supported fot stored response. - */ - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return delegate.getStoredResponses(responseIds, timeout); - } - - @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - final String compoundKey = StringUtils.isNotBlank(publisher) - ? "%s_%s".formatted(primaryAdServer, publisher) - : primaryAdServer; - - return getFromCacheOrDelegate(categoryConfigCache, adServerPublisherToErrorCache, compoundKey, timeout, - (key, timeoutParam) -> delegate.getCategories(primaryAdServer, publisher, timeout), - CachingApplicationSettings::noOp); - } - private static Future getFromCacheOrDelegate(Map cache, Map accountToErrorCache, String key, @@ -164,41 +107,75 @@ private static Future getFromCacheOrDelegate(Map cache, .recover(throwable -> cacheAndReturnFailedFuture(throwable, key, accountToErrorCache)); } - /** - * Retrieves stored data from cache and collects ids which were absent. For absent ids makes look up to original - * source, combines results and updates cache with missed stored item. In case when origin source returns failed - * {@link Future} propagates its result to caller. In successive call return {@link Future<StoredDataResult>} - * with all found stored items and error from origin source id call was made. - */ - private static Future getFromCacheOrDelegate( - SettingsCache cache, - String accountId, - Set requestIds, - Set impIds, - Timeout timeout, - StoredDataFetcher, Set, Timeout, Future> retriever) { + private static Future cacheAndReturnFailedFuture(Throwable throwable, + String key, + Map cache) { + + if (throwable instanceof PreBidException) { + cache.put(key, throwable.getMessage()); + } + + return Future.failedFuture(throwable); + } + + @Override + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); + } + + @Override + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate( + ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); + } + + @Override + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate( + videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); + } + + private static Future getStoredDataFromCacheOrDelegate(SettingsCache cache, + String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher retriever) { // empty string account ID doesn't make sense final String normalizedAccountId = StringUtils.stripToNull(accountId); - // search in cache final Map> requestCache = cache.getRequestCache(); final Map> impCache = cache.getImpCache(); final Set missedRequestIds = new HashSet<>(); - final Map storedIdToRequest = getFromCacheOrAddMissedIds(normalizedAccountId, requestIds, - requestCache, missedRequestIds); + final Map storedIdToRequest = getStoredDataFromCacheOrAddMissedIds( + normalizedAccountId, requestIds, requestCache, missedRequestIds); final Set missedImpIds = new HashSet<>(); - final Map storedIdToImp = getFromCacheOrAddMissedIds(normalizedAccountId, impIds, impCache, - missedImpIds); + final Map storedIdToImp = getStoredDataFromCacheOrAddMissedIds( + normalizedAccountId, impIds, impCache, missedImpIds); if (missedRequestIds.isEmpty() && missedImpIds.isEmpty()) { return Future.succeededFuture( - StoredDataResult.of(storedIdToRequest, storedIdToImp, Collections.emptyList())); + StoredDataResult.of( + Collections.unmodifiableMap(storedIdToRequest), + Collections.unmodifiableMap(storedIdToImp), + Collections.emptyList())); } - // delegate call to original source for missed ids and update cache with it return retriever.apply(normalizedAccountId, missedRequestIds, missedImpIds, timeout).map(result -> { final Map storedIdToRequestFromDelegate = result.getStoredIdToRequest(); storedIdToRequest.putAll(storedIdToRequestFromDelegate); @@ -212,25 +189,17 @@ private static Future getFromCacheOrDelegate( cache.saveImpCache(normalizedAccountId, entry.getKey(), entry.getValue()); } - return StoredDataResult.of(storedIdToRequest, storedIdToImp, result.getErrors()); + return StoredDataResult.of( + Collections.unmodifiableMap(storedIdToRequest), + Collections.unmodifiableMap(storedIdToImp), + result.getErrors()); }); } - private static Future cacheAndReturnFailedFuture(Throwable throwable, - String key, - Map cache) { - - if (throwable instanceof PreBidException) { - cache.put(key, throwable.getMessage()); - } - - return Future.failedFuture(throwable); - } - - private static Map getFromCacheOrAddMissedIds(String accountId, - Set ids, - Map> cache, - Set missedIds) { + private static Map getStoredDataFromCacheOrAddMissedIds(String accountId, + Set ids, + Map> cache, + Set missedIds) { final Map idToStoredItem = new HashMap<>(ids.size()); @@ -246,6 +215,37 @@ private static Map getFromCacheOrAddMissedIds(String accountId, return idToStoredItem; } + @Override + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + // TODO: add cache + return delegate.getProfiles(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return delegate.getStoredResponses(responseIds, timeout); + } + + @Override + // TODO: ??? + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + final String compoundKey = StringUtils.isNotBlank(publisher) + ? "%s_%s".formatted(primaryAdServer, publisher) + : primaryAdServer; + + return getFromCacheOrDelegate( + categoryConfigCache, + adServerPublisherToErrorCache, + compoundKey, + timeout, + (key, timeoutParam) -> delegate.getCategories(primaryAdServer, publisher, timeout), + CachingApplicationSettings::noOp); + } + public void invalidateAccountCache(String accountId) { accountCache.remove(accountId); accountToErrorCache.remove(accountId); diff --git a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java index 32d47d6abad..a95929a436f 100644 --- a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java @@ -4,10 +4,11 @@ import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.helper.StoredDataFetcher; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -15,11 +16,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiFunction; -/** - * Implements composite pattern for a list of {@link ApplicationSettings}. - */ public class CompositeApplicationSettings implements ApplicationSettings { private final Proxy proxy; @@ -42,58 +39,57 @@ private static Proxy createProxy(List delegates) { return proxy; } - /** - * Runs a process to get account by id from a chain of retrievers - * and returns {@link Future<{@link Account}>}. - */ @Override public Future getAccountById(String accountId, Timeout timeout) { return proxy.getAccountById(accountId, timeout); } - /** - * Runs a process to get stored requests by a collection of ids from a chain of retrievers - * and returns {@link Future<{@link StoredDataResult }>}. - */ @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return proxy.getStoredData(accountId, requestIds, impIds, timeout); } - /** - * Runs a process to get stored requests by a collection of amp ids from a chain of retrievers - * and returns {@link Future<{@link StoredDataResult }>}. - */ @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { - return proxy.getAmpStoredData(accountId, requestIds, Collections.emptySet(), timeout); + + return proxy.getAmpStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return proxy.getVideoStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return proxy.getCategories(primaryAdServer, publisher, timeout); + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return proxy.getProfiles(accountId, requestIds, impIds, timeout); } - /** - * Runs a process to get stored responses by a collection of ids from a chain of retrievers - * and returns {@link Future<{@link StoredResponseDataResult }>}. - */ @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { return proxy.getStoredResponses(responseIds, timeout); } - /** - * Decorates {@link ApplicationSettings} for a chain of retrievers. - */ + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return proxy.getCategories(primaryAdServer, publisher, timeout); + } + private static class Proxy implements ApplicationSettings { private final ApplicationSettings applicationSettings; @@ -106,105 +102,160 @@ private Proxy(ApplicationSettings applicationSettings, Proxy next) { @Override public Future getAccountById(String accountId, Timeout timeout) { - return getConfig(accountId, timeout, applicationSettings::getAccountById, - next != null ? next::getAccountById : null); - } - - private static Future getConfig(String key, Timeout timeout, - BiFunction> retriever, - BiFunction> nextRetriever) { - return retriever.apply(key, timeout) - .recover(throwable -> nextRetriever != null - ? nextRetriever.apply(key, timeout) - : Future.failedFuture(throwable)); - } - - @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return applicationSettings.getCategories(primaryAdServer, publisher, timeout) + return applicationSettings.getAccountById(accountId, timeout) .recover(throwable -> next != null - ? next.getCategories(primaryAdServer, publisher, timeout) + ? next.getAccountById(accountId, timeout) : Future.failedFuture(throwable)); } @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { - return getStoredRequests(accountId, requestIds, impIds, timeout, applicationSettings::getStoredData, + + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, + applicationSettings::getStoredData, next != null ? next::getStoredData : null); } @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { - return getStoredRequests(accountId, requestIds, Collections.emptySet(), timeout, + + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, applicationSettings::getAmpStoredData, next != null ? next::getAmpStoredData : null); } @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { - return getStoredRequests(accountId, requestIds, impIds, timeout, - applicationSettings::getVideoStoredData, next != null ? next::getVideoStoredData : null); - } - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return getStoredResponses(responseIds, timeout, applicationSettings::getStoredResponses, - next != null ? next::getStoredResponses : null); + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, + applicationSettings::getVideoStoredData, + next != null ? next::getVideoStoredData : null); } - private static Future getStoredResponses( - Set responseIds, Timeout timeout, - BiFunction, Timeout, Future> retriever, - BiFunction, Timeout, Future> nextRetriever) { - - return retriever.apply(responseIds, timeout) - .compose(retrieverResult -> - nextRetriever == null || retrieverResult.getErrors().isEmpty() - ? Future.succeededFuture(retrieverResult) - : getRemainingStoredResponses(responseIds, timeout, - retrieverResult.getIdToStoredResponses(), nextRetriever)); - } - - private static Future getStoredRequests( - String accountId, Set requestIds, Set impIds, Timeout timeout, - StoredDataFetcher, Set, Timeout, Future> retriever, - StoredDataFetcher, Set, Timeout, Future> nextRetriever) { + private static Future getStoredDataOrDelegate(String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher retriever, + StoredDataFetcher nextRetriever) { return retriever.apply(accountId, requestIds, impIds, timeout) - .compose(retrieverResult -> - nextRetriever == null || retrieverResult.getErrors().isEmpty() - ? Future.succeededFuture(retrieverResult) - : getRemainingStoredRequests(accountId, requestIds, impIds, timeout, - retrieverResult.getStoredIdToRequest(), retrieverResult.getStoredIdToImp(), - nextRetriever)); + .compose(retrieverResult -> nextRetriever == null || retrieverResult.getErrors().isEmpty() + ? Future.succeededFuture(retrieverResult) + : getRemainingStoredData( + accountId, + requestIds, + impIds, + timeout, + retrieverResult.getStoredIdToRequest(), + retrieverResult.getStoredIdToImp(), + nextRetriever)); } - private static Future getRemainingStoredRequests( - String accountId, Set requestIds, Set impIds, Timeout timeout, - Map storedIdToRequest, Map storedIdToImp, - StoredDataFetcher, Set, Timeout, Future> retriever) { - - return retriever.apply(accountId, subtractSets(requestIds, storedIdToRequest.keySet()), - subtractSets(impIds, storedIdToImp.keySet()), timeout) + private static Future getRemainingStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + Map storedIdToRequest, + Map storedIdToImp, + StoredDataFetcher retriever) { + + return retriever.apply( + accountId, + subtractSets(requestIds, storedIdToRequest.keySet()), + subtractSets(impIds, storedIdToImp.keySet()), + timeout) .map(result -> StoredDataResult.of( combineMaps(storedIdToRequest, result.getStoredIdToRequest()), combineMaps(storedIdToImp, result.getStoredIdToImp()), result.getErrors())); } - private static Future getRemainingStoredResponses( - Set responseIds, Timeout timeout, Map storedSeatBids, - BiFunction, Timeout, Future> retriever) { + @Override + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return applicationSettings.getProfiles(accountId, requestIds, impIds, timeout) + .compose(result -> next == null || result.getErrors().isEmpty() + ? Future.succeededFuture(result) + : getRemainingProfiles( + accountId, + requestIds, + impIds, + timeout, + result.getIdToRequestProfile(), + result.getIdToImpProfile())); + } + + private Future getRemainingProfiles( + String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + Map idToRequestProfile, + Map idToImpProfile) { + + return next.getProfiles( + accountId, + subtractSets(requestIds, idToRequestProfile.keySet()), + subtractSets(impIds, idToImpProfile.keySet()), + timeout) + .map(result -> StoredProfileResult.of( + combineMaps(idToRequestProfile, result.getIdToRequestProfile()), + combineMaps(idToImpProfile, result.getIdToImpProfile()), + result.getErrors())); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return applicationSettings.getStoredResponses(responseIds, timeout) + .compose(result -> next == null || result.getErrors().isEmpty() + ? Future.succeededFuture(result) + : getRemainingStoredResponses(responseIds, timeout, result.getIdToStoredResponses())); + } - return retriever.apply(subtractSets(responseIds, storedSeatBids.keySet()), timeout) + private Future getRemainingStoredResponses( + Set responseIds, + Timeout timeout, + Map storedSeatBids) { + + return next.getStoredResponses(subtractSets(responseIds, storedSeatBids.keySet()), timeout) .map(result -> StoredResponseDataResult.of( combineMaps(storedSeatBids, result.getIdToStoredResponses()), result.getErrors())); } + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return applicationSettings.getCategories(primaryAdServer, publisher, timeout) + .recover(throwable -> next != null + ? next.getCategories(primaryAdServer, publisher, timeout) + : Future.failedFuture(throwable)); + } + private static Set subtractSets(Set first, Set second) { final Set remaining = new HashSet<>(first); remaining.removeAll(second); diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java index 9c7408260d6..9653706d988 100644 --- a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -15,8 +15,8 @@ import org.prebid.server.settings.helper.ParametrizedQueryHelper; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; -import org.prebid.server.util.ObjectUtil; import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient; import org.prebid.server.vertx.database.DatabaseClient; @@ -29,14 +29,6 @@ import java.util.function.Function; import java.util.stream.IntStream; -/** - * Implementation of {@link ApplicationSettings}. - *

- * Reads an application settings from the database source. - *

- * In order to enable caching and reduce latency for read operations {@link DatabaseApplicationSettings} - * can be decorated by {@link CachingApplicationSettings}. - */ public class DatabaseApplicationSettings implements ApplicationSettings { private final DatabaseClient databaseClient; @@ -100,10 +92,6 @@ public DatabaseApplicationSettings(DatabaseClient databaseClient, this.selectStoredResponsesQuery = Objects.requireNonNull(selectStoredResponsesQuery); } - /** - * Runs a process to get account by id from database - * and returns {@link Future}<{@link Account}>. - */ @Override public Future getAccountById(String accountId, Timeout timeout) { return databaseClient.executeQuery( @@ -111,17 +99,12 @@ public Future getAccountById(String accountId, Timeout timeout) { Collections.singletonList(accountId), result -> mapToModelOrError(result, this::toAccount), timeout) - .compose(result -> failedIfNull(result, accountId, "Account")); - } - - @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return Future.failedFuture(new PreBidException("Not supported")); + .compose(result -> result != null + ? Future.succeededFuture(result) + : Future.failedFuture(new PreBidException("Account not found: " + accountId))); } /** - * Transforms the first row of {@link RowSet} to required object or returns null. - *

* Note: mapper should never throw exception in case of using * {@link CircuitBreakerSecuredDatabaseClient}. */ @@ -132,18 +115,8 @@ private T mapToModelOrError(RowSet rowSet, Function mapper) { : null; } - /** - * Returns succeeded {@link Future} if given value is not equal to NULL, - * otherwise failed {@link Future} with {@link PreBidException}. - */ - private static Future failedIfNull(T value, String id, String errorPrefix) { - return value != null - ? Future.succeededFuture(value) - : Future.failedFuture(new PreBidException("%s not found: %s".formatted(errorPrefix, id))); - } - private Account toAccount(Row row) { - final String source = ObjectUtil.getIfNotNull(row.getValue(0), Object::toString); + final String source = Objects.toString(row.getValue(0), null); try { return source != null ? mapper.decodeValue(source, Account.class) : null; } catch (DecodeException e) { @@ -151,45 +124,76 @@ private Account toAccount(Row row) { } } - /** - * Runs a process to get stored requests by a collection of ids from database - * and returns {@link Future}<{@link StoredDataResult}>. - */ @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); } - /** - * Runs a process to get stored requests by a collection of amp ids from database - * and returns {@link Future}<{@link StoredDataResult}>. - */ @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return fetchStoredData(selectAmpStoredRequestsQuery, accountId, requestIds, Collections.emptySet(), timeout); } - /** - * Runs a process to get stored requests by a collection of video ids from database - * and returns {@link Future}<{@link StoredDataResult}>. - */ @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); } - /** - * Runs a process to get stored responses by a collection of ids from database - * and returns {@link Future}<{@link StoredResponseDataResult}>. - */ + private Future fetchStoredData(String query, + String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { + return Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())); + } + + final List idsQueryParameters = new ArrayList<>(); + IntStream.rangeClosed(1, StringUtils.countMatches(query, ParametrizedQueryHelper.REQUEST_ID_PLACEHOLDER)) + .forEach(i -> idsQueryParameters.addAll(requestIds)); + IntStream.rangeClosed(1, StringUtils.countMatches(query, ParametrizedQueryHelper.IMP_ID_PLACEHOLDER)) + .forEach(i -> idsQueryParameters.addAll(impIds)); + + final String parametrizedQuery = parametrizedQueryHelper + .replaceRequestAndImpIdPlaceholders(query, requestIds.size(), impIds.size()); + + return databaseClient.executeQuery( + parametrizedQuery, + idsQueryParameters, + result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds), + timeout); + } + + @Override + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + // TODO: query? + return Future.failedFuture("Not implemented"); + } + @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { - final String queryResolvedWithParameters = parametrizedQueryHelper.replaceStoredResponseIdPlaceholders( - selectStoredResponsesQuery, - responseIds.size()); + final String queryResolvedWithParameters = parametrizedQueryHelper + .replaceStoredResponseIdPlaceholders(selectStoredResponsesQuery, responseIds.size()); final List idsQueryParameters = new ArrayList<>(); final int responseIdPlaceholderCount = StringUtils.countMatches( @@ -198,37 +202,15 @@ public Future getStoredResponses(Set responseI IntStream.rangeClosed(1, responseIdPlaceholderCount) .forEach(i -> idsQueryParameters.addAll(responseIds)); - return databaseClient.executeQuery(queryResolvedWithParameters, idsQueryParameters, - result -> DatabaseStoredResponseResultMapper.map(result, responseIds), timeout); + return databaseClient.executeQuery( + queryResolvedWithParameters, + idsQueryParameters, + result -> DatabaseStoredResponseResultMapper.map(result, responseIds), + timeout); } - /** - * Fetches stored requests from database for the given query. - */ - private Future fetchStoredData(String query, String accountId, Set requestIds, - Set impIds, Timeout timeout) { - final Future future; - - if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { - future = Future.succeededFuture( - StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList())); - } else { - final List idsQueryParameters = new ArrayList<>(); - IntStream.rangeClosed(1, StringUtils.countMatches(query, ParametrizedQueryHelper.REQUEST_ID_PLACEHOLDER)) - .forEach(i -> idsQueryParameters.addAll(requestIds)); - IntStream.rangeClosed(1, StringUtils.countMatches(query, ParametrizedQueryHelper.IMP_ID_PLACEHOLDER)) - .forEach(i -> idsQueryParameters.addAll(impIds)); - - final String parametrizedQuery = parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders( - query, - requestIds.size(), - impIds.size()); - - future = databaseClient.executeQuery(parametrizedQuery, idsQueryParameters, - result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds), - timeout); - } - - return future; + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return Future.failedFuture(new PreBidException("Not supported")); } } diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index bfde0fc2e81..42d89a66286 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -13,6 +13,7 @@ import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountPriceFloorsConfig; import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.Map; @@ -46,6 +47,16 @@ public EnrichingApplicationSettings(boolean enforceValidAccount, this.defaultAccount = parseAccount(defaultAccountConfig, mapper); } + private static Account parseAccount(String accountConfig, JacksonMapper mapper) { + try { + return StringUtils.isNotBlank(accountConfig) + ? mapper.decodeValue(accountConfig, Account.class) + : null; + } catch (DecodeException e) { + throw new IllegalArgumentException("Could not parse default account configuration", e); + } + } + @Override public Future getAccountById(String accountId, Timeout timeout) { if (StringUtils.isNotBlank(accountId)) { @@ -59,6 +70,12 @@ public Future getAccountById(String accountId, Timeout timeout) { return recoverIfNeeded(new PreBidException("Unauthorized account: account id is empty"), StringUtils.EMPTY); } + private Account mergeAccounts(Account account) { + return defaultAccount == null + ? account + : jsonMerger.merge(account, defaultAccount, Account.class); + } + private AccountPriceFloorsConfig extractDefaultPriceFloors() { return Optional.ofNullable(defaultAccount) .map(Account::getAuction) @@ -66,14 +83,11 @@ private AccountPriceFloorsConfig extractDefaultPriceFloors() { .orElse(null); } - private static Account parseAccount(String accountConfig, JacksonMapper mapper) { - try { - return StringUtils.isNotBlank(accountConfig) - ? mapper.decodeValue(accountConfig, Account.class) - : null; - } catch (DecodeException e) { - throw new IllegalArgumentException("Could not parse default account configuration", e); - } + private Future recoverIfNeeded(Throwable throwable, String accountId) { + // In case of invalid account return failed future + return enforceValidAccount + ? Future.failedFuture(throwable) + : Future.succeededFuture(mergeAccounts(Account.empty(accountId))); } @Override @@ -85,16 +99,6 @@ public Future getStoredData(String accountId, return delegate.getStoredData(accountId, requestIds, impIds, timeout); } - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return delegate.getStoredResponses(responseIds, timeout); - } - - @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return delegate.getCategories(primaryAdServer, publisher, timeout); - } - @Override public Future getAmpStoredData(String accountId, Set requestIds, @@ -113,16 +117,22 @@ public Future getVideoStoredData(String accountId, return delegate.getVideoStoredData(accountId, requestIds, impIds, timeout); } - private Account mergeAccounts(Account account) { - return defaultAccount == null - ? account - : jsonMerger.merge(account, defaultAccount, Account.class); + @Override + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getProfiles(accountId, requestIds, impIds, timeout); } - private Future recoverIfNeeded(Throwable throwable, String accountId) { - // In case of invalid account return failed future - return enforceValidAccount - ? Future.failedFuture(throwable) - : Future.succeededFuture(mergeAccounts(Account.empty(accountId))); + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return delegate.getStoredResponses(responseIds, timeout); + } + + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return delegate.getCategories(primaryAdServer, publisher, timeout); } } diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java index 1a2f42e86c4..507c9333899 100644 --- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java @@ -6,6 +6,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.file.FileSystem; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.SetUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.timeout.Timeout; @@ -16,11 +17,11 @@ import org.prebid.server.settings.model.SettingsFile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.io.File; import java.io.IOException; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -51,65 +52,150 @@ public class FileApplicationSettings implements ApplicationSettings { private final Map storedIdToSeatBid; private final Map> fileToCategories; - public FileApplicationSettings(FileSystem fileSystem, String settingsFileName, String storedRequestsDir, - String storedImpsDir, String storedResponsesDir, String categoriesDir, + public FileApplicationSettings(FileSystem fileSystem, + String settingsFileName, + String storedRequestsDir, + String storedImpsDir, + String storedResponsesDir, + String categoriesDir, JacksonMapper jacksonMapper) { - final SettingsFile settingsFile = readSettingsFile(Objects.requireNonNull(fileSystem), + final SettingsFile settingsFile = readSettingsFile( + Objects.requireNonNull(fileSystem), Objects.requireNonNull(settingsFileName)); - accounts = toMap(settingsFile.getAccounts(), + accounts = toMap( + settingsFile.getAccounts(), Account::getId, Function.identity()); - this.storedIdToRequest = readStoredData(fileSystem, Objects.requireNonNull(storedRequestsDir)); - this.storedIdToImp = readStoredData(fileSystem, Objects.requireNonNull(storedImpsDir)); - this.storedIdToSeatBid = readStoredData(fileSystem, Objects.requireNonNull(storedResponsesDir)); - this.fileToCategories = readCategories(fileSystem, Objects.requireNonNull(categoriesDir), jacksonMapper); + storedIdToRequest = readStoredData(fileSystem, Objects.requireNonNull(storedRequestsDir)); + storedIdToImp = readStoredData(fileSystem, Objects.requireNonNull(storedImpsDir)); + storedIdToSeatBid = readStoredData(fileSystem, Objects.requireNonNull(storedResponsesDir)); + fileToCategories = readCategories(fileSystem, Objects.requireNonNull(categoriesDir), jacksonMapper); + } + + private static SettingsFile readSettingsFile(FileSystem fileSystem, String fileName) { + final Buffer buf = fileSystem.readFileBlocking(fileName); + try { + return new YAMLMapper().readValue(buf.getBytes(), SettingsFile.class); + } catch (IOException e) { + throw new IllegalArgumentException("Couldn't read file settings", e); + } + } + + private static Map toMap(List list, Function keyMapper, Function valueMapper) { + return list != null + ? list.stream().collect(Collectors.toMap(keyMapper, valueMapper)) + : Collections.emptyMap(); + } + + private static Map readStoredData(FileSystem fileSystem, String dir) { + return fileSystem.readDirBlocking(dir).stream() + .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) + .collect(Collectors.toMap( + filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), + filepath -> fileSystem.readFileBlocking(filepath).toString())); + } + + private static Map> readCategories(FileSystem fileSystem, + String dir, + JacksonMapper jacksonMapper) { + + return fileSystem.readDirBlocking(dir).stream() + .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) + .collect(Collectors.toMap( + filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), + filepath -> parseCategories(filepath, fileSystem.readFileBlocking(filepath), jacksonMapper))); + } + + private static Map parseCategories(String filepath, + Buffer categoriesBuffer, + JacksonMapper jacksonMapper) { + + try { + return jacksonMapper.decodeValue(categoriesBuffer, CATEGORY_FORMAT_REFERENCE); + } catch (DecodeException e) { + throw new PreBidException("Failed to decode categories for file " + filepath); + } } @Override public Future getAccountById(String accountId, Timeout timeout) { - return mapValueToFuture(accounts, accountId, "Account"); + final Account account = accounts.get(accountId); + return account != null + ? Future.succeededFuture(account) + : Future.failedFuture(new PreBidException("Account not found: " + accountId)); } - /** - * Creates {@link StoredDataResult} by checking if any ids are missed in storedRequest map - * and adding an error to list for each missed Id - * and returns {@link Future<{@link StoredDataResult }>} with all loaded files and errors list. - */ @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { - return Future.succeededFuture(CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds) - ? StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList()) - : StoredDataResult.of( + + return CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds) + + ? Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())) + + : Future.succeededFuture(StoredDataResult.of( existingStoredIdToJson(requestIds, storedIdToRequest), existingStoredIdToJson(impIds, storedIdToImp), Stream.of( errorsForMissedIds(requestIds, storedIdToRequest, StoredDataType.request), errorsForMissedIds(impIds, storedIdToImp, StoredDataType.imp)) - .flatMap(Collection::stream) + .flatMap(Function.identity()) .toList())); } @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { - return getStoredData(accountId, requestIds, Collections.emptySet(), timeout); + + return getStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return getStoredData(accountId, requestIds, impIds, timeout); } + @Override + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + // TODO: implement + return Future.failedFuture("Not implemented"); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return CollectionUtils.isEmpty(responseIds) + + ? Future.succeededFuture(StoredResponseDataResult.of(Collections.emptyMap(), Collections.emptyList())) + + : Future.succeededFuture(StoredResponseDataResult.of( + existingStoredIdToJson(responseIds, storedIdToSeatBid), + errorsForMissedIds(responseIds, storedIdToSeatBid, StoredDataType.seatbid).toList())); + } + @Override public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { final String filename = StringUtils.isNotBlank(publisher) ? "%s_%s".formatted(primaryAdServer, publisher) : primaryAdServer; + final Map categoryToId = fileToCategories.get(filename); return categoryToId != null ? Future.succeededFuture(extractCategoriesIds(categoryToId)) @@ -120,104 +206,24 @@ public Future> getCategories(String primaryAdServer, String private static Map extractCategoriesIds(Map categoryToId) { return categoryToId.entrySet().stream() .filter(catToCategory -> catToCategory.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, + .collect(Collectors.toMap( + Map.Entry::getKey, catToCategory -> catToCategory.getValue().getId())); } - /** - * Creates {@link StoredResponseDataResult} by checking if any ids are missed in storedResponse map - * and adding an error to list for each missed Id - * and returns {@link Future<{@link StoredResponseDataResult }>} with all loaded files and errors list. - */ - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return Future.succeededFuture(CollectionUtils.isEmpty(responseIds) - ? StoredResponseDataResult.of(Collections.emptyMap(), Collections.emptyList()) - : StoredResponseDataResult.of( - existingStoredIdToJson(responseIds, storedIdToSeatBid), - errorsForMissedIds(responseIds, storedIdToSeatBid, StoredDataType.seatbid))); - } - - private static Map toMap(List list, Function keyMapper, Function valueMapper) { - return list != null ? list.stream().collect(Collectors.toMap(keyMapper, valueMapper)) : Collections.emptyMap(); - } - - /** - * Reading YAML settings file. - */ - private static SettingsFile readSettingsFile(FileSystem fileSystem, String fileName) { - final Buffer buf = fileSystem.readFileBlocking(fileName); - try { - return new YAMLMapper().readValue(buf.getBytes(), SettingsFile.class); - } catch (IOException e) { - throw new IllegalArgumentException("Couldn't read file settings", e); - } - } - - /** - * Reads files with .json extension in configured directory and creates {@link Map} where key is a file name - * without .json extension and value is file content. - */ - private static Map readStoredData(FileSystem fileSystem, String dir) { - return fileSystem.readDirBlocking(dir).stream() - .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) - .collect(Collectors.toMap(filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), - filename -> fileSystem.readFileBlocking(filename).toString())); - } - - /** - * Reads files with .json extension in configured directory and creates {@link Map} where key is a file name - * without .json and value is file content parsed to a {@link Map} where key is category and value is - * {@link Category}. - */ - private static Map> readCategories(FileSystem fileSystem, String dir, - JacksonMapper jacksonMapper) { - return fileSystem.readDirBlocking(dir).stream() - .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) - .collect(Collectors.toMap(filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), - filename -> parseCategories(filename, fileSystem.readFileBlocking(filename), jacksonMapper))); - } - - /** - * Parses {@link Buffer} to a {@link Map} where key is category and value {@link Category}. - */ - private static Map parseCategories(String fileName, Buffer categoriesBuffer, - JacksonMapper jacksonMapper) { - try { - return jacksonMapper.decodeValue(categoriesBuffer, CATEGORY_FORMAT_REFERENCE); - } catch (DecodeException e) { - throw new PreBidException("Failed to decode categories for file " + fileName); - } - } - - private static Future mapValueToFuture(Map map, String id, String errorPrefix) { - final T value = map.get(id); - return value != null - ? Future.succeededFuture(value) - : Future.failedFuture(new PreBidException("%s not found: %s".formatted(errorPrefix, id))); - } - - /** - * Returns corresponding stored id with json. - */ private static Map existingStoredIdToJson(Set requestedIds, Map storedIdToJson) { + return requestedIds.stream() .filter(storedIdToJson::containsKey) .collect(Collectors.toMap(Function.identity(), storedIdToJson::get)); } - /** - * Returns errors for missed IDs. - */ - private static List errorsForMissedIds(Set ids, Map storedIdToJson, - StoredDataType type) { - final List missedIds = ids.stream() - .filter(id -> !storedIdToJson.containsKey(id)) - .toList(); + private static Stream errorsForMissedIds(Set ids, + Map storedIdToJson, + StoredDataType type) { - return missedIds.isEmpty() ? Collections.emptyList() : missedIds.stream() - .map(id -> "No stored %s found for id: %s".formatted(type, id)) - .toList(); + return SetUtils.difference(ids, storedIdToJson.keySet()).stream() + .map(id -> "No stored %s found for id: %s".formatted(type, id)); } } diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index 98517003baf..a9e7992f27d 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -18,6 +18,7 @@ import org.prebid.server.settings.model.Category; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import org.prebid.server.settings.proto.response.HttpAccountsResponse; import org.prebid.server.settings.proto.response.HttpFetcherResponse; @@ -77,19 +78,23 @@ public class HttpApplicationSettings implements ApplicationSettings { private final HttpClient httpClient; private final JacksonMapper mapper; - public HttpApplicationSettings(HttpClient httpClient, JacksonMapper mapper, String endpoint, String ampEndpoint, - String videoEndpoint, String categoryEndpoint) { - this.httpClient = Objects.requireNonNull(httpClient); - this.mapper = Objects.requireNonNull(mapper); + public HttpApplicationSettings(String endpoint, + String ampEndpoint, + String videoEndpoint, + String categoryEndpoint, + HttpClient httpClient, + JacksonMapper mapper) { + this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); this.ampEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(ampEndpoint)); this.videoEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(videoEndpoint)); this.categoryEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(categoryEndpoint)); + this.httpClient = Objects.requireNonNull(httpClient); + this.mapper = Objects.requireNonNull(mapper); } @Override public Future getAccountById(String accountId, Timeout timeout) { - return fetchAccountsByIds(Collections.singleton(accountId), timeout) .map(accounts -> accounts.stream() .findFirst() @@ -101,14 +106,14 @@ private Future> fetchAccountsByIds(Set accountIds, Timeout if (CollectionUtils.isEmpty(accountIds)) { return Future.succeededFuture(Collections.emptySet()); } + final long remainingTimeout = timeout.remaining(); if (timeout.remaining() <= 0) { return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); } return httpClient.get(accountsRequestUrlFrom(endpoint, accountIds), HttpUtil.headers(), remainingTimeout) - .compose(response -> processAccountsResponse(response, accountIds)) - .recover(Future::failedFuture); + .map(response -> processAccountsResponse(response, accountIds)); } private static String accountsRequestUrlFrom(String endpoint, Set accountIds) { @@ -122,12 +127,8 @@ private static String accountsRequestUrlFrom(String endpoint, Set accoun return url.toString(); } - private Future> processAccountsResponse(HttpClientResponse response, Set accountIds) { - return Future.succeededFuture( - toAccountsResult(response.getStatusCode(), response.getBody(), accountIds)); - } - - private Set toAccountsResult(int statusCode, String body, Set accountIds) { + private Set processAccountsResponse(HttpClientResponse httpClientResponse, Set accountIds) { + final int statusCode = httpClientResponse.getStatusCode(); if (statusCode != HttpResponseStatus.OK.code()) { throw new PreBidException("Error fetching accounts %s via http: unexpected response status %d" .formatted(accountIds, statusCode)); @@ -135,7 +136,7 @@ private Set toAccountsResult(int statusCode, String body, Set a final HttpAccountsResponse response; try { - response = mapper.decodeValue(body, HttpAccountsResponse.class); + response = mapper.decodeValue(httpClientResponse.getBody(), HttpAccountsResponse.class); } catch (DecodeException e) { throw new PreBidException("Error fetching accounts %s via http: failed to parse response: %s" .formatted(accountIds, e.getMessage())); @@ -145,86 +146,38 @@ private Set toAccountsResult(int statusCode, String body, Set a return MapUtils.isNotEmpty(accounts) ? new HashSet<>(accounts.values()) : Collections.emptySet(); } - /** - * Runs a process to get stored requests by a collection of ids from http service - * and returns {@link Future<{@link StoredDataResult }>} - */ @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return fetchStoredData(endpoint, requestIds, impIds, timeout); } - /** - * Runs a process to get stored requests by a collection of amp ids from http service - * and returns {@link Future<{@link StoredDataResult }>} - */ @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { - return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout); - } - /** - * Not supported and returns failed result. - */ - @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return fetchStoredData(videoEndpoint, requestIds, impIds, timeout); - } - - /** - * Not supported and returns failed result. - */ - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return Future.failedFuture(new PreBidException("Not supported")); + return fetchStoredData(ampEndpoint, requestIds, impIds, timeout); } @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - final String url = StringUtils.isNotEmpty(publisher) - ? "%s/%s/%s.json".formatted(categoryEndpoint, primaryAdServer, publisher) - : "%s/%s.json".formatted(categoryEndpoint, primaryAdServer); - final long remainingTimeout = timeout.remaining(); - if (remainingTimeout <= 0) { - return Future.failedFuture(new TimeoutException( - "Failed to fetch categories from url '%s'. Reason: Timeout exceeded".formatted(url))); - } - return httpClient.get(url, remainingTimeout) - .map(httpClientResponse -> processCategoryResponse(httpClientResponse, url)); - } - - private Map processCategoryResponse(HttpClientResponse httpClientResponse, String url) { - final int statusCode = httpClientResponse.getStatusCode(); - if (statusCode != 200) { - throw makeFailedCategoryFetchException(url, "Response status code is '%d'".formatted(statusCode)); - } - - final String body = httpClientResponse.getBody(); - if (StringUtils.isEmpty(body)) { - throw makeFailedCategoryFetchException(url, "Response body is null or empty"); - } - - final Map categories; - try { - categories = mapper.decodeValue(body, CATEGORY_RESPONSE_REFERENCE); - } catch (DecodeException e) { - throw makeFailedCategoryFetchException(url, "Failed to decode response body with error " + e.getMessage()); - } - return categories.entrySet().stream() - .filter(catToCategory -> catToCategory.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, - catToCategory -> catToCategory.getValue().getId())); - } + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { - private PreBidException makeFailedCategoryFetchException(String url, String reason) { - return new PreBidException("Failed to fetch categories from url '%s'. Reason: %s".formatted(url, reason)); + return fetchStoredData(videoEndpoint, requestIds, impIds, timeout); } - private Future fetchStoredData(String endpoint, Set requestIds, Set impIds, + private Future fetchStoredData(String endpoint, + Set requestIds, + Set impIds, Timeout timeout) { + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { return Future.succeededFuture( StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList())); @@ -236,10 +189,33 @@ private Future fetchStoredData(String endpoint, Set re } return httpClient.get(storeRequestUrlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout) - .compose(response -> processStoredDataResponse(response, requestIds, impIds)) + .map(response -> processStoredDataResponse(response, requestIds, impIds)) .recover(exception -> failStoredDataResponse(exception, requestIds, impIds)); } + private static Future failStoredDataResponse(Throwable throwable, + Set requestIds, + Set impIds) { + + return Future.succeededFuture(toFailedStoredDataResult(requestIds, impIds, throwable.getMessage())); + } + + private static StoredDataResult toFailedStoredDataResult(Set requestIds, + Set impIds, + String errorMessageFormat, + Object... args) { + + final String errorRequests = requestIds.isEmpty() ? "" : "stored requests for ids " + requestIds; + final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; + final String errorImps = impIds.isEmpty() ? "" : "stored imps for ids " + impIds; + + final String error = "Error fetching %s%s%s via HTTP: %s" + .formatted(errorRequests, separator, errorImps, errorMessageFormat.formatted(args)); + logger.info(error); + + return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.singletonList(error)); + } + private static String storeRequestUrlFrom(String endpoint, Set requestIds, Set impIds) { final StringBuilder url = new StringBuilder(endpoint); url.append(endpoint.contains("?") ? "&" : "?"); @@ -258,42 +234,16 @@ private static String storeRequestUrlFrom(String endpoint, Set requestId return url.toString(); } - private static String joinIds(Set ids) { - return String.join("\",\"", ids); - } - - private static Future failStoredDataResponse(Throwable throwable, Set requestIds, - Set impIds) { - return Future.succeededFuture( - toFailedStoredDataResult(requestIds, impIds, throwable.getMessage())); - } - - private Future processStoredDataResponse(HttpClientResponse response, Set requestIds, - Set impIds) { - return Future.succeededFuture( - toStoredDataResult(requestIds, impIds, response.getStatusCode(), response.getBody())); - } + private StoredDataResult processStoredDataResponse(HttpClientResponse httpClientResponse, + Set requestIds, + Set impIds) { - private static StoredDataResult toFailedStoredDataResult(Set requestIds, Set impIds, - String errorMessageFormat, Object... args) { - final String errorRequests = requestIds.isEmpty() ? "" - : "stored requests for ids " + requestIds; - final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; - final String errorImps = impIds.isEmpty() ? "" : "stored imps for ids " + impIds; - - final String error = "Error fetching %s%s%s via HTTP: %s" - .formatted(errorRequests, separator, errorImps, errorMessageFormat.formatted(args)); - - logger.info(error); - return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.singletonList(error)); - } - - private StoredDataResult toStoredDataResult(Set requestIds, Set impIds, - int statusCode, String body) { + final int statusCode = httpClientResponse.getStatusCode(); if (statusCode != HttpResponseStatus.OK.code()) { return toFailedStoredDataResult(requestIds, impIds, "HTTP status code %d", statusCode); } + final String body = httpClientResponse.getBody(); final HttpFetcherResponse response; try { response = mapper.decodeValue(body, HttpFetcherResponse.class); @@ -305,8 +255,7 @@ private StoredDataResult toStoredDataResult(Set requestIds, Set return parseResponse(requestIds, impIds, response); } - private StoredDataResult parseResponse(Set requestIds, Set impIds, - HttpFetcherResponse response) { + private StoredDataResult parseResponse(Set requestIds, Set impIds, HttpFetcherResponse response) { final List errors = new ArrayList<>(); final Map storedIdToRequest = @@ -318,10 +267,12 @@ private StoredDataResult parseResponse(Set requestIds, Set impId return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); } - private Map parseStoredDataOrAddError(Set ids, Map storedData, - StoredDataType type, List errors) { + private Map parseStoredDataOrAddError(Set ids, + Map storedData, + StoredDataType type, + List errors) { + final Map result = new HashMap<>(ids.size()); - final Set notParsedIds = new HashSet<>(); if (storedData != null) { for (Map.Entry entry : storedData.entrySet()) { @@ -332,7 +283,6 @@ private Map parseStoredDataOrAddError(Set ids, Map parseStoredDataOrAddError(Set ids, Map missedIds = new HashSet<>(ids); missedIds.removeAll(result.keySet()); - missedIds.removeAll(notParsedIds); - errors.addAll(missedIds.stream() - .map(id -> "Stored %s not found for id: %s".formatted(type, id)) - .toList()); + missedIds.forEach(id -> errors.add("Stored %s not found for id: %s".formatted(type, id))); } return result; } + + @Override + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + // TODO: change to success + return Future.failedFuture("Not supported"); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + // TODO: ??? + return Future.failedFuture(new PreBidException("Not supported")); + } + + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + final String url = StringUtils.isNotEmpty(publisher) + ? "%s/%s/%s.json".formatted(categoryEndpoint, primaryAdServer, publisher) + : "%s/%s.json".formatted(categoryEndpoint, primaryAdServer); + + final long remainingTimeout = timeout.remaining(); + if (remainingTimeout <= 0) { + return Future.failedFuture(new TimeoutException( + "Failed to fetch categories from url '%s'. Reason: Timeout exceeded".formatted(url))); + } + + return httpClient.get(url, remainingTimeout) + .map(httpClientResponse -> processCategoryResponse(httpClientResponse, url)); + } + + private Map processCategoryResponse(HttpClientResponse httpClientResponse, String url) { + final int statusCode = httpClientResponse.getStatusCode(); + if (statusCode != 200) { + throw makeFailedCategoryFetchException(url, "Response status code is '%d'".formatted(statusCode)); + } + + final String body = httpClientResponse.getBody(); + if (StringUtils.isEmpty(body)) { + throw makeFailedCategoryFetchException(url, "Response body is null or empty"); + } + + final Map categories; + try { + categories = mapper.decodeValue(body, CATEGORY_RESPONSE_REFERENCE); + } catch (DecodeException e) { + throw makeFailedCategoryFetchException(url, "Failed to decode response body with error " + e.getMessage()); + } + + return categories.entrySet().stream() + .filter(catToCategory -> catToCategory.getValue() != null) + .collect(Collectors.toMap( + Map.Entry::getKey, + catToCategory -> catToCategory.getValue().getId())); + } + + private PreBidException makeFailedCategoryFetchException(String url, String reason) { + return new PreBidException("Failed to fetch categories from url '%s'. Reason: %s".formatted(url, reason)); + } + + private static String joinIds(Set ids) { + // TODO: Add url encode + return String.join("\",\"", ids); + } } diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java index f1c8b107c5f..a4cbb89496c 100644 --- a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java @@ -13,6 +13,7 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import software.amazon.awssdk.core.BytesWrapper; import software.amazon.awssdk.core.async.AsyncResponseTransformer; @@ -132,17 +133,13 @@ private StoredDataResult buildStoredDataResult(Map storedIdToReq return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); } - private Set missingStoredDataIds(Map fileContents, Set responseIds) { - return SetUtils.difference(responseIds, fileContents.keySet()); - } - @Override public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, Timeout timeout) { - return getStoredData(accountId, requestIds, Collections.emptySet(), timeout); + return getStoredData(accountId, requestIds, impIds, timeout); } @Override @@ -154,6 +151,16 @@ public Future getVideoStoredData(String accountId, return getStoredData(accountId, requestIds, impIds, timeout); } + @Override + public Future getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + // TODO: change to success + return Future.failedFuture("Not supported"); + } + @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { return withTimeout(() -> getFileContents(storedResponsesDirectory, responseIds), timeout) @@ -224,4 +231,8 @@ private Future withTimeout(Supplier> futureFactory, Timeout tim return promise.future(); } + + private Set missingStoredDataIds(Map fileContents, Set requestedIds) { + return SetUtils.difference(requestedIds, fileContents.keySet()); + } } diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java index 41cb6224e7e..f747379b3e0 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java @@ -9,7 +9,6 @@ import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; import org.prebid.server.settings.model.StoredItem; -import org.prebid.server.util.ObjectUtil; import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient; import java.util.ArrayList; @@ -18,11 +17,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -/** - * Utility class for mapping {@link RowSet} to {@link StoredDataResult}. - */ public class DatabaseStoredDataResultMapper { private static final Logger logger = LoggerFactory.getLogger(DatabaseStoredDataResultMapper.class); @@ -31,14 +28,13 @@ private DatabaseStoredDataResultMapper() { } /** - * Maps {@link RowSet} to {@link StoredDataResult} and creates an error for each missing ID and add it to result. - * - * @param rowSet - incoming Row Set representing a result of SQL query - * @param accountId - an account ID extracted from request - * @param requestIds - a specified set of stored requests' IDs. Adds error for each ID missing in result set - * @param impIds - a specified set of stored imps' IDs. Adds error for each ID missing in result set - * @return - a {@link StoredDataResult} object - *

+ * Overloaded method for cases when no specific IDs are required, e.g. fetching all records. + */ + public static StoredDataResult map(RowSet resultSet) { + return map(resultSet, null, Collections.emptySet(), Collections.emptySet()); + } + + /** * Note: mapper should never throw exception in case of using * {@link CircuitBreakerSecuredDatabaseClient}. */ @@ -46,127 +42,123 @@ public static StoredDataResult map(RowSet rowSet, String accountId, Set requestIds, Set impIds) { - final Map storedIdToRequest; - final Map storedIdToImp; - final List errors = new ArrayList<>(); final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; + final List errors = new ArrayList<>(); if (rowIterator == null || !rowIterator.hasNext()) { - storedIdToRequest = Collections.emptyMap(); - storedIdToImp = Collections.emptyMap(); + handleEmptyResult(requestIds, impIds, errors); - if (requestIds.isEmpty() && impIds.isEmpty()) { - errors.add("No stored requests or imps were found"); - } else { - final String errorRequests = requestIds.isEmpty() ? "" - : "stored requests for ids " + requestIds; - final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; - final String errorImps = impIds.isEmpty() ? "" : "stored imps for ids " + impIds; + return StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.unmodifiableList(errors)); + } - errors.add("No %s%s%s were found".formatted(errorRequests, separator, errorImps)); - } - } else { - final Map> requestIdToStoredItems = new HashMap<>(); - final Map> impIdToStoredItems = new HashMap<>(); - - while (rowIterator.hasNext()) { - final Row row = rowIterator.next(); - if (row.toJson().size() < 4) { - final String message = "Error occurred while mapping stored request data: some columns are missing"; - logger.error(message); - errors.add(message); - return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), errors); - } - final String fetchedAccountId; - final String id; - final String data; - final String typeAsString; - try { - fetchedAccountId = ObjectUtil.getIfNotNull(row.getValue(0), Object::toString); - id = ObjectUtil.getIfNotNull(row.getValue(1), Object::toString); - data = ObjectUtil.getIfNotNull(row.getValue(2), Object::toString); - typeAsString = ObjectUtil.getIfNotNull(row.getValue(3), Object::toString); - } catch (ClassCastException e) { - final String message = "Error occurred while mapping stored request data"; - logger.error(message, e); - errors.add(message); - return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), errors); - } + final Map> requestIdToStoredItems = new HashMap<>(); + final Map> impIdToStoredItems = new HashMap<>(); - final StoredDataType type; - try { - type = StoredDataType.valueOf(typeAsString); - } catch (IllegalArgumentException e) { - logger.error("Stored request data with id={} has invalid type: ''{}'' and will be ignored.", e, - id, typeAsString); - continue; - } + while (rowIterator.hasNext()) { + final Row row = rowIterator.next(); + if (row.size() < 4) { + final String message = "Error occurred while mapping stored request data: some columns are missing"; + logger.error(message); + errors.add(message); - if (type == StoredDataType.request) { - addStoredItem(fetchedAccountId, id, data, requestIdToStoredItems); - } else { - addStoredItem(fetchedAccountId, id, data, impIdToStoredItems); - } + return StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.unmodifiableList(errors)); } - storedIdToRequest = storedItemsOrAddError(StoredDataType.request, accountId, requestIds, - requestIdToStoredItems, errors); - storedIdToImp = storedItemsOrAddError(StoredDataType.imp, accountId, impIds, - impIdToStoredItems, errors); + final String fetchedAccountId = Objects.toString(row.getValue(0), null); + final String id = Objects.toString(row.getValue(1), null); + final String data = Objects.toString(row.getValue(2), null); + final String typeAsString = Objects.toString(row.getValue(3), null); + + final StoredDataType type; + try { + type = StoredDataType.valueOf(typeAsString); + } catch (IllegalArgumentException e) { + logger.error("Stored request data with id={} has invalid type: ''{}'' and will be ignored.", + e, id, typeAsString); + continue; + } + + if (type == StoredDataType.request) { + addStoredItem(fetchedAccountId, id, data, requestIdToStoredItems); + } else { + addStoredItem(fetchedAccountId, id, data, impIdToStoredItems); + } } - return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); + return StoredDataResult.of( + storedItemsOrAddError( + StoredDataType.request, + accountId, + requestIds, + requestIdToStoredItems, + errors), + storedItemsOrAddError( + StoredDataType.imp, + accountId, + impIds, + impIdToStoredItems, + errors), + Collections.unmodifiableList(errors)); } - /** - * Overloaded method for cases when no specific IDs are required, e.g. fetching all records. - * - * @param resultSet - incoming {@link RowSet} representing a result of SQL query. - * @return - a {@link StoredDataResult} object. - */ - public static StoredDataResult map(RowSet resultSet) { - return map(resultSet, null, Collections.emptySet(), Collections.emptySet()); + private static void handleEmptyResult(Set requestIds, Set impIds, List errors) { + if (requestIds.isEmpty() && impIds.isEmpty()) { + errors.add("No stored requests or imps were found"); + } else { + final String errorRequests = requestIds.isEmpty() + ? "" + : "stored requests for ids " + requestIds; + final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; + final String errorImps = impIds.isEmpty() ? "" : "stored imps for ids " + impIds; + + errors.add("No %s%s%s were found".formatted(errorRequests, separator, errorImps)); + } } - private static void addStoredItem(String accountId, String id, String data, + private static void addStoredItem(String accountId, + String id, + String data, Map> idToStoredItems) { - final StoredItem storedItem = StoredItem.of(accountId, data); - final Set storedItems = idToStoredItems.get(id); - if (storedItems == null) { - idToStoredItems.put(id, new HashSet<>(Collections.singleton(storedItem))); - } else { - storedItems.add(storedItem); - } + idToStoredItems.computeIfAbsent(id, key -> new HashSet<>()).add(StoredItem.of(accountId, data)); } - /** - * Returns map of stored ID -> value or populates error. - */ private static Map storedItemsOrAddError(StoredDataType type, String accountId, Set searchIds, Map> foundIdToStoredItems, List errors) { + final Map result = new HashMap<>(); if (searchIds.isEmpty()) { - for (Map.Entry> entry : foundIdToStoredItems.entrySet()) { - entry.getValue().forEach(storedItem -> result.put(entry.getKey(), storedItem.getData())); - } - } else { - for (String id : searchIds) { - try { - final StoredItem resolvedStoredItem = StoredItemResolver.resolve(type, accountId, id, - foundIdToStoredItems.get(id)); - result.put(id, resolvedStoredItem.getData()); - } catch (PreBidException e) { - errors.add(e.getMessage()); + foundIdToStoredItems.forEach((id, storedItems) -> { + for (StoredItem storedItem : storedItems) { + result.put(id, storedItem.getData()); } + }); + + return Collections.unmodifiableMap(result); + } + + for (String id : searchIds) { + try { + final StoredItem resolvedStoredItem = StoredItemResolver + .resolve(type, accountId, id, foundIdToStoredItems.get(id)); + + result.put(id, resolvedStoredItem.getData()); + } catch (PreBidException e) { + errors.add(e.getMessage()); } } - return result; + return Collections.unmodifiableMap(result); } } diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java index 99896d72911..b3812804cfb 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java @@ -3,14 +3,15 @@ import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowIterator; import io.vertx.sqlclient.RowSet; +import org.apache.commons.collections4.SetUtils; import org.prebid.server.settings.model.StoredResponseDataResult; -import org.prebid.server.util.ObjectUtil; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; public class DatabaseStoredResponseResultMapper { @@ -19,34 +20,37 @@ private DatabaseStoredResponseResultMapper() { } public static StoredResponseDataResult map(RowSet rowSet, Set responseIds) { - final Map storedIdToResponse = new HashMap<>(responseIds.size()); + final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; final List errors = new ArrayList<>(); - final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; if (rowIterator == null || !rowIterator.hasNext()) { - handleEmptyResultError(responseIds, errors); - return StoredResponseDataResult.of(storedIdToResponse, errors); + handleEmptyResult(responseIds, errors); + return StoredResponseDataResult.of(Collections.emptyMap(), Collections.unmodifiableList(errors)); } + final Map storedIdToResponse = new HashMap<>(responseIds.size()); + while (rowIterator.hasNext()) { final Row row = rowIterator.next(); - if (row.toJson().size() < 2) { + if (row.size() < 2) { errors.add("Result set column number is less than expected"); - return StoredResponseDataResult.of(Collections.emptyMap(), errors); + return StoredResponseDataResult.of(Collections.emptyMap(), Collections.unmodifiableList(errors)); } - final String key = ObjectUtil.getIfNotNull(row.getValue(0), Object::toString); - final String value = ObjectUtil.getIfNotNull(row.getValue(1), Object::toString); - storedIdToResponse.put(key, value); + + storedIdToResponse.put( + Objects.toString(row.getValue(0), null), + Objects.toString(row.getValue(1), null)); } - errors.addAll(responseIds.stream().filter(id -> !storedIdToResponse.containsKey(id)) - .map(id -> "No stored response found for id: " + id) - .toList()); + SetUtils.difference(responseIds, storedIdToResponse.keySet()) + .forEach(id -> errors.add("No stored response found for id: " + id)); - return StoredResponseDataResult.of(storedIdToResponse, errors); + return StoredResponseDataResult.of( + Collections.unmodifiableMap(storedIdToResponse), + Collections.unmodifiableList(errors)); } - private static void handleEmptyResultError(Set responseIds, List errors) { + private static void handleEmptyResult(Set responseIds, List errors) { if (responseIds.isEmpty()) { errors.add("No stored responses found"); } else { diff --git a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java index 1d7660e0b76..64d79db1663 100644 --- a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java @@ -9,8 +9,7 @@ public interface ParametrizedQueryHelper { String replaceAccountIdPlaceholder(String query); - String replaceStoredResponseIdPlaceholders(String query, int idsNumber); - String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber); + String replaceStoredResponseIdPlaceholders(String query, int idsNumber); } diff --git a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryMySqlHelper.java b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryMySqlHelper.java index e6e6336afc8..e492b1ce385 100644 --- a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryMySqlHelper.java +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryMySqlHelper.java @@ -12,11 +12,6 @@ public String replaceAccountIdPlaceholder(String query) { return query.replace(ACCOUNT_ID_PLACEHOLDER, PARAMETER_PLACEHOLDER); } - @Override - public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { - return query.replaceAll(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber)); - } - @Override public String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber) { return query @@ -24,6 +19,11 @@ public String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumb .replace(IMP_ID_PLACEHOLDER, parameterHolders(impIdNumber)); } + @Override + public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { + return query.replace(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber)); + } + private static String parameterHolders(int paramsSize) { return paramsSize == 0 ? "NULL" diff --git a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java index c380b8e91df..69388e3423c 100644 --- a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java @@ -15,11 +15,6 @@ public String replaceAccountIdPlaceholder(String query) { return query.replace(ACCOUNT_ID_PLACEHOLDER, "$1"); } - @Override - public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { - return query.replaceAll(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber, 0)); - } - @Override public String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber) { final Matcher matcher = PLACEHOLDER_PATTERN.matcher(query); @@ -32,9 +27,15 @@ public String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumb i += paramsNumber; } matcher.appendTail(queryBuilder); + return queryBuilder.toString(); } + @Override + public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { + return query.replaceAll(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber, 0)); + } + private static String parameterHolders(int paramsSize, int start) { return paramsSize == 0 ? "NULL" diff --git a/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java index f173bcff45e..5adec2d9bbb 100644 --- a/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java +++ b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java @@ -1,18 +1,13 @@ package org.prebid.server.settings.helper; +import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.StoredDataResult; -/** - * Interface to satisfy obtaining of {@link StoredDataResult}. - * - * @param account ID - * @param set of stored request IDs - * @param set of stored imp IDs - * @param processing timeout - * @param result of fetching stored data - */ +import java.util.Set; + @FunctionalInterface -public interface StoredDataFetcher { +public interface StoredDataFetcher { - R apply(ACC account, REQS reqIds, IMPS impIds, T timeout); + Future apply(String account, Set reqIds, Set impIds, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java index 8e3c43058fc..a372ee3a417 100644 --- a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java +++ b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java @@ -44,6 +44,7 @@ public static StoredItem resolve(StoredDataType type, String accountId, String i throw new PreBidException( "Multiple stored %ss found for id: %s but no account was specified".formatted(type, id)); } + return storedItems.stream() .filter(storedItem -> Objects.equals(storedItem.getAccountId(), accountId)) .findAny() @@ -53,10 +54,13 @@ public static StoredItem resolve(StoredDataType type, String accountId, String i // only one stored item found final StoredItem storedItem = storedItems.iterator().next(); - if (StringUtils.isBlank(accountId) || storedItem.getAccountId() == null + if (StringUtils.isBlank(accountId) + || storedItem.getAccountId() == null || Objects.equals(accountId, storedItem.getAccountId())) { + return storedItem; } + throw new PreBidException("No stored %s found for id: %s for account: %s".formatted(type, id, accountId)); } } diff --git a/src/main/java/org/prebid/server/settings/helper/StoredResponseFetcher.java b/src/main/java/org/prebid/server/settings/helper/StoredResponseFetcher.java new file mode 100644 index 00000000000..3092841b06e --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/StoredResponseFetcher.java @@ -0,0 +1,13 @@ +package org.prebid.server.settings.helper; + +import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.StoredResponseDataResult; + +import java.util.Set; + +@FunctionalInterface +public interface StoredResponseFetcher { + + Future apply(Set responseIds, Timeout timeout); +} diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 4e883ba2495..6f00f19d63c 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -1,19 +1,19 @@ package org.prebid.server.spring.config; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.log.Logger; -import org.prebid.server.log.LoggerFactory; import io.vertx.core.Vertx; import io.vertx.core.file.FileSystem; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.UtilityClass; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.ActivitiesConfigResolver; import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; +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.settings.ApplicationSettings; @@ -43,12 +43,12 @@ import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; -import software.amazon.awssdk.core.exception.SdkClientException; import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; @@ -124,8 +124,13 @@ HttpApplicationSettings httpApplicationSettings( @Value("${settings.http.video-endpoint}") String videoEndpoint, @Value("${settings.http.category-endpoint}") String categoryEndpoint) { - return new HttpApplicationSettings(httpClient, mapper, endpoint, ampEndpoint, videoEndpoint, - categoryEndpoint); + return new HttpApplicationSettings( + endpoint, + ampEndpoint, + videoEndpoint, + categoryEndpoint, + httpClient, + mapper); } } @@ -250,7 +255,7 @@ protected static class S3ConfigurationProperties { * If accessKeyId and secretAccessKey are provided in the * configuration file then they will be used. Otherwise, the * DefaultCredentialsProvider will look for credentials in this order: - * + *

* - Java System Properties * - Environment Variables * - Web Identity Token @@ -309,7 +314,7 @@ S3AsyncClient s3AsyncClient(S3ConfigurationProperties s3ConfigurationProperties) private static AwsCredentialsProvider awsCredentialsProvider(S3ConfigurationProperties config) { final AwsCredentialsProvider credentialsProvider = config.useStaticCredentials() ? StaticCredentialsProvider.create( - AwsBasicCredentials.create(config.getAccessKeyId(), config.getSecretAccessKey())) + AwsBasicCredentials.create(config.getAccessKeyId(), config.getSecretAccessKey())) : DefaultCredentialsProvider.create(); try { diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index 4a2c731659d..8478978096b 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -389,30 +389,6 @@ public void getStoredDataShouldReturnResultWithNoErrorsIfAllIdsArePresent() { .isEqualTo(singletonMap("2", "value2")); } - @Test - public void getAmpStoredDataShouldIgnoreImpIdsArgument() { - // given - given(fileSystem.readDirBlocking(anyString())) - .willReturn(singletonList("/home/user/requests/1.json")) - .willReturn(singletonList("/home/user/imps/2.json")); - given(fileSystem.readFileBlocking(anyString())) - .willReturn(Buffer.buffer("accounts:")) // settings file - .willReturn(Buffer.buffer("value1")) // stored request - .willReturn(Buffer.buffer("value2")) // stored imp - .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); - - // when - final Future storedRequestResult = - applicationSettings.getAmpStoredData(null, emptySet(), singleton("2"), null); - - // then - assertThat(storedRequestResult.result().getErrors()).isNotNull().isEmpty(); - assertThat(storedRequestResult.result().getStoredIdToImp()).isEmpty(); - } - @Test public void getStoredResponsesShouldReturnEmptyResultAndErrorsWhenResponseIdsAreEmpty() { // given diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index e3076ddbdfd..ddcdceeb95a 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -64,8 +64,13 @@ public class HttpApplicationSettingsTest extends VertxTest { @BeforeEach public void setUp() { - httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, AMP_ENDPOINT, - VIDEO_ENDPOINT, CATEGORY_ENDPOINT); + httpApplicationSettings = new HttpApplicationSettings( + ENDPOINT, + AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper); final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); @@ -76,24 +81,39 @@ public void setUp() { @Test public void creationShouldFailsOnInvalidEndpoint() { assertThatIllegalArgumentException() - .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, "invalid_url", AMP_ENDPOINT, - VIDEO_ENDPOINT, CATEGORY_ENDPOINT)) + .isThrownBy(() -> new HttpApplicationSettings( + "invalid_url", + AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper)) .withMessage("URL supplied is not valid: invalid_url"); } @Test public void creationShouldFailsOnInvalidAmpEndpoint() { assertThatIllegalArgumentException() - .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, "invalid_url", - VIDEO_ENDPOINT, CATEGORY_ENDPOINT)) + .isThrownBy(() -> new HttpApplicationSettings( + ENDPOINT, + "invalid_url", + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper)) .withMessage("URL supplied is not valid: invalid_url"); } @Test public void creationShouldFailsOnInvalidVideoEndpoint() { assertThatIllegalArgumentException() - .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, AMP_ENDPOINT, - "invalid_url", CATEGORY_ENDPOINT)) + .isThrownBy(() -> new HttpApplicationSettings( + ENDPOINT, + AMP_ENDPOINT, + "invalid_url", + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper)) .withMessage("URL supplied is not valid: invalid_url"); } @@ -242,8 +262,13 @@ public void getStoredDataShouldSendHttpRequestWithExpectedNewParams() { public void getStoredDataShouldSendHttpRequestWithExpectedAppendedParams() { // given givenHttpClientReturnsResponse(200, null); - httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, - "http://some-domain?param1=value1", AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT); + httpApplicationSettings = new HttpApplicationSettings( + "http://some-domain?param1=value1", + AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper); // when httpApplicationSettings.getStoredData(null, singleton("id1"), singleton("id2"), timeout); @@ -390,20 +415,6 @@ public void getStoredDataShouldReturnExpectedResult() throws JsonProcessingExcep .containsOnly(tuple("id2", "{\"field2\":\"field-value2\"}")); } - @Test - public void getAmpStoredDataShouldIgnoreImpIdsArgument() { - // given - givenHttpClientReturnsResponse(200, null); - - // when - httpApplicationSettings.getAmpStoredData(null, singleton("id1"), singleton("id2"), timeout); - - // then - final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(httpClient).get(captor.capture(), any(), anyLong()); - assertThat(captor.getValue()).doesNotContain("imp-ids"); - } - @Test public void getCategoriesShouldBuildUrlFromEndpointAdServerAndPublisher() { // given diff --git a/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java b/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java index 220bc03ee6a..3b323ce86da 100644 --- a/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java +++ b/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java @@ -1,6 +1,5 @@ package org.prebid.server.settings.helper; -import io.vertx.core.json.JsonObject; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowIterator; import io.vertx.sqlclient.RowSet; @@ -13,7 +12,6 @@ import java.util.Arrays; import java.util.Iterator; -import java.util.stream.IntStream; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; @@ -323,9 +321,7 @@ private void givenRowSet(Row... rows) { private Row givenRow(Object... values) { final Row row = mock(Row.class, withSettings().strictness(LENIENT)); given(row.getValue(anyInt())).willAnswer(invocation -> values[(Integer) invocation.getArgument(0)]); - final JsonObject json = new JsonObject(); - IntStream.range(0, values.length).forEach(i -> json.put(String.valueOf(i), values[i])); - given(row.toJson()).willReturn(json); + given(row.size()).willReturn(values.length); return row; } diff --git a/src/test/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapperTest.java b/src/test/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapperTest.java index 237a09cac85..124f8638125 100644 --- a/src/test/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapperTest.java +++ b/src/test/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapperTest.java @@ -1,6 +1,5 @@ package org.prebid.server.settings.helper; -import io.vertx.core.json.JsonObject; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowIterator; import io.vertx.sqlclient.RowSet; @@ -15,7 +14,6 @@ import java.util.Arrays; import java.util.Iterator; import java.util.Set; -import java.util.stream.IntStream; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; @@ -110,9 +108,7 @@ private void givenRowSet(Row... rows) { private Row givenRow(Object... values) { final Row row = mock(Row.class, withSettings().strictness(LENIENT)); given(row.getValue(anyInt())).willAnswer(invocation -> values[(Integer) invocation.getArgument(0)]); - final JsonObject json = new JsonObject(); - IntStream.range(0, values.length).forEach(i -> json.put(String.valueOf(i), values[i])); - given(row.toJson()).willReturn(json); + given(row.size()).willReturn(values.length); return row; } From 3b526a6af13f463a197b3fd4b6da34a53b470466 Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 22 Jul 2025 20:17:08 +0200 Subject: [PATCH 04/24] Add `getProfiles` implementation for `DatabaseApplicationSettings`. Refactoring --- .../auction/VideoStoredRequestProcessor.java | 10 +- .../externalortb/ProfilesProcessor.java | 25 ++- .../externalortb/StoredRequestProcessor.java | 16 +- .../SettingsCacheNotificationHandler.java | 20 ++- .../server/settings/ApplicationSettings.java | 40 ++--- .../settings/CacheNotificationListener.java | 4 +- .../settings/CachingApplicationSettings.java | 102 +++++------ .../CompositeApplicationSettings.java | 134 ++++++-------- .../settings/DatabaseApplicationSettings.java | 104 +++++++---- .../EnrichingApplicationSettings.java | 34 ++-- .../settings/FileApplicationSettings.java | 34 ++-- .../settings/HttpApplicationSettings.java | 74 ++++---- .../settings/S3ApplicationSettings.java | 50 +++--- .../prebid/server/settings/SettingsCache.java | 30 ++-- .../helper/DatabaseProfilesResultMapper.java | 167 ++++++++++++++++++ .../DatabaseStoredDataResultMapper.java | 24 +-- .../settings/helper/StoredDataFetcher.java | 4 +- .../settings/helper/StoredItemResolver.java | 9 +- .../prebid/server/settings/model/Profile.java | 3 +- .../settings/model/StoredDataResult.java | 6 +- .../server/settings/model/StoredItem.java | 4 +- .../settings/model/StoredProfileResult.java | 16 -- .../DatabasePeriodicRefreshService.java | 8 +- .../service/HttpPeriodicRefreshService.java | 4 +- .../service/S3PeriodicRefreshService.java | 8 +- .../spring/config/SettingsConfiguration.java | 42 +++-- .../admin/AdminEndpointsConfiguration.java | 16 +- 27 files changed, 590 insertions(+), 398 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java delete mode 100644 src/main/java/org/prebid/server/settings/model/StoredProfileResult.java diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java index f6bcb5599af..f3beeb57e6c 100644 --- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java @@ -129,9 +129,9 @@ private static BidRequest readBidRequest(String defaultBidRequestPath, : null; } - private StoredDataResult updateMetrics(StoredDataResult storedDataResult, - Set requestIds, - Set impIds) { + private StoredDataResult updateMetrics(StoredDataResult storedDataResult, + Set requestIds, + Set impIds) { requestIds.forEach( id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id))); @@ -142,7 +142,7 @@ private StoredDataResult updateMetrics(StoredDataResult storedDataResult, return storedDataResult; } - private WithPodErrors toBidRequestWithPodErrors(StoredDataResult storedResult, + private WithPodErrors toBidRequestWithPodErrors(StoredDataResult storedResult, BidRequestVideo videoRequest, String storedBidRequestId) { @@ -161,7 +161,7 @@ private WithPodErrors toBidRequestWithPodErrors(StoredDataResult sto private BidRequestVideo mergeBidRequest(BidRequestVideo originalRequest, String storedRequestId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId); if (enforceStoredRequest && StringUtils.isBlank(storedRequest)) { diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index 9d81563d7b7..56b74886d99 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -10,6 +10,7 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; @@ -19,7 +20,7 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.Profile; -import org.prebid.server.settings.model.StoredProfileResult; +import org.prebid.server.settings.model.StoredDataResult; import java.util.ArrayList; import java.util.Collection; @@ -74,8 +75,8 @@ public Future process(Account account, BidRequest bidRequest) { return fetchProfiles(account.getId(), profilesIds, timeoutMillis(bidRequest)) .map(profiles -> mergeResults( - applyRequestProfiles(profilesIds.request(), profiles.getIdToRequestProfile(), bidRequest), - applyImpsProfiles(profilesIds.imps(), profiles.getIdToImpProfile(), imps))) + applyRequestProfiles(profilesIds.request(), profiles.getStoredIdToRequest(), bidRequest), + applyImpsProfiles(profilesIds.imps(), profiles.getStoredIdToImp(), imps))) .recover(e -> Future.failedFuture( new InvalidRequestException("Error during processing profiles: " + e.getMessage()))); } @@ -119,9 +120,9 @@ private long timeoutMillis(BidRequest bidRequest) { return tmax != null && tmax > 0 ? tmax : defaultTimeoutMillis; } - private Future fetchProfiles(String accountId, - AllProfilesIds allProfilesIds, - long timeoutMillis) { + private Future> fetchProfiles(String accountId, + AllProfilesIds allProfilesIds, + long timeoutMillis) { final Set requestProfilesIds = new HashSet<>(allProfilesIds.request()); final Set impProfilesIds = allProfilesIds.imps().stream() @@ -168,11 +169,19 @@ private T applyProfiles(List profilesIds, private ObjectNode mergeProfile(ObjectNode original, Profile profile, String profileId) { return switch (profile.getMergePrecedence()) { - case REQUEST -> merge(original, profile.getBody(), profileId); - case PROFILE -> merge(profile.getBody(), original, profileId); + case REQUEST -> merge(original, parse(profile.getBody()), profileId); + case PROFILE -> merge(parse(profile.getBody()), original, profileId); }; } + private ObjectNode parse(String body) { + try { + return mapper.decodeValue(body, ObjectNode.class); + } catch (DecodeException e) { + throw new InvalidProfileException("Can't parse profile: " + e.getMessage()); + } + } + private ObjectNode merge(ObjectNode takePrecedence, ObjectNode other, String profileId) { try { return (ObjectNode) jsonMerger.merge(takePrecedence, other); diff --git a/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java index e02b17b157f..a725c02b228 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java @@ -104,7 +104,7 @@ private Future processAuctionStoredRequest(String accountId return Future.succeededFuture(AuctionStoredResult.of(false, bidRequest)); } - final Future storedDataFuture = + final Future> storedDataFuture = applicationSettings.getStoredData(accountId, requestIds, impIds, timeout(bidRequest)) .onSuccess(storedDataResult -> updateStoredResultMetrics(storedDataResult, requestIds, impIds)); @@ -121,7 +121,7 @@ public Future processAmpRequest(String accountId, String ampRequestI } private Future processAmpStoredRequest(String accountId, String ampRequestId, BidRequest bidRequest) { - final Future ampStoredDataFuture = applicationSettings.getAmpStoredData( + final Future> ampStoredDataFuture = applicationSettings.getAmpStoredData( accountId, Collections.singleton(ampRequestId), Collections.emptySet(), timeout(bidRequest)) .onSuccess(storedDataResult -> updateStoredResultMetrics( storedDataResult, Collections.singleton(ampRequestId), Collections.emptySet())); @@ -172,7 +172,7 @@ private static Future stripToInvalidRequestException(Throwable cause) { "Stored request processing failed: " + cause.getMessage())); } - private void updateStoredResultMetrics(StoredDataResult storedDataResult, + private void updateStoredResultMetrics(StoredDataResult storedDataResult, Set requestIds, Set impIds) { @@ -192,7 +192,7 @@ private static BidRequest readBidRequest(String defaultBidRequestPath, : null; } - private VideoStoredDataResult makeVideoStoredDataResult(StoredDataResult storedDataResult, + private VideoStoredDataResult makeVideoStoredDataResult(StoredDataResult storedDataResult, Map storedIdToImpId, List errors) { @@ -232,7 +232,7 @@ private Video parseVideoFromImp(String storedJson) { return null; } - private Future storedRequestsToBidRequest(Future storedDataFuture, + private Future storedRequestsToBidRequest(Future> storedDataFuture, BidRequest bidRequest, String storedBidRequestId, Map impsToStoredRequestId) { @@ -253,7 +253,7 @@ private Future storedRequestsToBidRequest(Future s private BidRequest mergeBidRequestAndImps(BidRequest bidRequest, String storedRequestId, Map impToStoredId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { final BidRequest mergedWithStoredRequest = mergeBidRequest(bidRequest, storedRequestId, storedDataResult); @@ -272,7 +272,7 @@ private BidRequest mergeDefaultRequest(BidRequest bidRequest) { */ private BidRequest mergeBidRequest(BidRequest originalRequest, String storedRequestId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId); return StringUtils.isNotBlank(storedRequestId) @@ -286,7 +286,7 @@ private BidRequest mergeBidRequest(BidRequest originalRequest, */ private BidRequest mergeImps(BidRequest bidRequest, Map impToStoredId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { if (impToStoredId.isEmpty()) { return bidRequest; diff --git a/src/main/java/org/prebid/server/handler/admin/SettingsCacheNotificationHandler.java b/src/main/java/org/prebid/server/handler/admin/SettingsCacheNotificationHandler.java index 9b89009a962..fd652ebb8ff 100644 --- a/src/main/java/org/prebid/server/handler/admin/SettingsCacheNotificationHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/SettingsCacheNotificationHandler.java @@ -19,15 +19,17 @@ */ public class SettingsCacheNotificationHandler implements Handler { - private final CacheNotificationListener cacheNotificationListener; - private final JacksonMapper mapper; private final String endpoint; + private final CacheNotificationListener cacheNotificationListener; + private final JacksonMapper mapper; + + public SettingsCacheNotificationHandler(String endpoint, + CacheNotificationListener cacheNotificationListener, + JacksonMapper mapper) { - public SettingsCacheNotificationHandler(CacheNotificationListener cacheNotificationListener, JacksonMapper mapper, - String endpoint) { + this.endpoint = Objects.requireNonNull(endpoint); this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener); this.mapper = Objects.requireNonNull(mapper); - this.endpoint = Objects.requireNonNull(endpoint); } @Override @@ -94,14 +96,18 @@ private void doFail(RoutingContext routingContext) { } private void respondWithBadRequest(RoutingContext routingContext, String body) { - HttpUtil.executeSafely(routingContext, endpoint, + HttpUtil.executeSafely( + routingContext, + endpoint, response -> response .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) .end(body)); } private void respondWith(RoutingContext routingContext, HttpResponseStatus status) { - HttpUtil.executeSafely(routingContext, endpoint, + HttpUtil.executeSafely( + routingContext, + endpoint, response -> response .setStatusCode(status.code()) .end()); diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index 64856f9d4fd..2f3ca855668 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -3,8 +3,8 @@ import io.vertx.core.Future; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.Map; @@ -14,25 +14,25 @@ public interface ApplicationSettings { Future getAccountById(String accountId, Timeout timeout); - Future getStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout); - - Future getAmpStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout); - - Future getVideoStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout); - - Future getProfiles(String accountId, - Set requestIds, - Set impIds, - Timeout timeout); + Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); + + Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); + + Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); + + Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); Future getStoredResponses(Set responseIds, Timeout timeout); diff --git a/src/main/java/org/prebid/server/settings/CacheNotificationListener.java b/src/main/java/org/prebid/server/settings/CacheNotificationListener.java index e8650dec1b8..5a5e8e88d75 100644 --- a/src/main/java/org/prebid/server/settings/CacheNotificationListener.java +++ b/src/main/java/org/prebid/server/settings/CacheNotificationListener.java @@ -3,9 +3,9 @@ import java.util.List; import java.util.Map; -public interface CacheNotificationListener { +public interface CacheNotificationListener { - void save(Map requests, Map imps); + void save(Map requests, Map imps); void invalidate(List requests, List imps); } diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 95275aa076d..e0fcfa2fa31 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -11,9 +11,9 @@ import org.prebid.server.settings.helper.StoredDataFetcher; import org.prebid.server.settings.helper.StoredItemResolver; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredItem; -import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.Collections; @@ -35,15 +35,17 @@ public class CachingApplicationSettings implements ApplicationSettings { private final Map accountToErrorCache; private final Map adServerPublisherToErrorCache; private final Map> categoryConfigCache; - private final SettingsCache cache; - private final SettingsCache ampCache; - private final SettingsCache videoCache; + private final SettingsCache cache; + private final SettingsCache ampCache; + private final SettingsCache videoCache; + private final SettingsCache profileCache; private final Metrics metrics; public CachingApplicationSettings(ApplicationSettings delegate, - SettingsCache cache, - SettingsCache ampCache, - SettingsCache videoCache, + SettingsCache cache, + SettingsCache ampCache, + SettingsCache videoCache, + SettingsCache profileCache, Metrics metrics, int ttl, int size, @@ -64,6 +66,7 @@ public CachingApplicationSettings(ApplicationSettings delegate, this.cache = Objects.requireNonNull(cache); this.ampCache = Objects.requireNonNull(ampCache); this.videoCache = Objects.requireNonNull(videoCache); + this.profileCache = Objects.requireNonNull(profileCache); this.metrics = Objects.requireNonNull(metrics); } @@ -119,53 +122,63 @@ private static Future cacheAndReturnFailedFuture(Throwable throwable, } @Override - public Future getStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return getStoredDataFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); } @Override - public Future getAmpStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return getStoredDataFromCacheOrDelegate( ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); } @Override - public Future getVideoStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return getStoredDataFromCacheOrDelegate( videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); } - private static Future getStoredDataFromCacheOrDelegate(SettingsCache cache, - String accountId, - Set requestIds, - Set impIds, - Timeout timeout, - StoredDataFetcher retriever) { + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate( + profileCache, accountId, requestIds, impIds, timeout, delegate::getProfiles); + } + + private static Future> getStoredDataFromCacheOrDelegate(SettingsCache cache, + String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher retriever) { // empty string account ID doesn't make sense final String normalizedAccountId = StringUtils.stripToNull(accountId); - final Map> requestCache = cache.getRequestCache(); - final Map> impCache = cache.getImpCache(); + final Map>> requestCache = cache.getRequestCache(); + final Map>> impCache = cache.getImpCache(); final Set missedRequestIds = new HashSet<>(); - final Map storedIdToRequest = getStoredDataFromCacheOrAddMissedIds( + final Map storedIdToRequest = getStoredDataFromCacheOrAddMissedIds( normalizedAccountId, requestIds, requestCache, missedRequestIds); final Set missedImpIds = new HashSet<>(); - final Map storedIdToImp = getStoredDataFromCacheOrAddMissedIds( + final Map storedIdToImp = getStoredDataFromCacheOrAddMissedIds( normalizedAccountId, impIds, impCache, missedImpIds); if (missedRequestIds.isEmpty() && missedImpIds.isEmpty()) { @@ -177,15 +190,15 @@ private static Future getStoredDataFromCacheOrDelegate(Setting } return retriever.apply(normalizedAccountId, missedRequestIds, missedImpIds, timeout).map(result -> { - final Map storedIdToRequestFromDelegate = result.getStoredIdToRequest(); + final Map storedIdToRequestFromDelegate = result.getStoredIdToRequest(); storedIdToRequest.putAll(storedIdToRequestFromDelegate); - for (Map.Entry entry : storedIdToRequestFromDelegate.entrySet()) { + for (Map.Entry entry : storedIdToRequestFromDelegate.entrySet()) { cache.saveRequestCache(normalizedAccountId, entry.getKey(), entry.getValue()); } - final Map storedIdToImpFromDelegate = result.getStoredIdToImp(); + final Map storedIdToImpFromDelegate = result.getStoredIdToImp(); storedIdToImp.putAll(storedIdToImpFromDelegate); - for (Map.Entry entry : storedIdToImpFromDelegate.entrySet()) { + for (Map.Entry entry : storedIdToImpFromDelegate.entrySet()) { cache.saveImpCache(normalizedAccountId, entry.getKey(), entry.getValue()); } @@ -196,16 +209,16 @@ private static Future getStoredDataFromCacheOrDelegate(Setting }); } - private static Map getStoredDataFromCacheOrAddMissedIds(String accountId, - Set ids, - Map> cache, - Set missedIds) { + private static Map getStoredDataFromCacheOrAddMissedIds(String accountId, + Set ids, + Map>> cache, + Set missedIds) { - final Map idToStoredItem = new HashMap<>(ids.size()); + final Map idToStoredItem = new HashMap<>(ids.size()); for (String id : ids) { try { - final StoredItem resolvedStoredItem = StoredItemResolver.resolve(null, accountId, id, cache.get(id)); + final StoredItem resolvedStoredItem = StoredItemResolver.resolve(null, accountId, id, cache.get(id)); idToStoredItem.put(id, resolvedStoredItem.getData()); } catch (PreBidException e) { missedIds.add(id); @@ -215,23 +228,12 @@ private static Map getStoredDataFromCacheOrAddMissedIds(String a return idToStoredItem; } - @Override - public Future getProfiles(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - // TODO: add cache - return delegate.getProfiles(accountId, requestIds, impIds, timeout); - } - @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { return delegate.getStoredResponses(responseIds, timeout); } @Override - // TODO: ??? public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { final String compoundKey = StringUtils.isNotBlank(publisher) ? "%s_%s".formatted(primaryAdServer, publisher) diff --git a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java index a95929a436f..b3e27b6f807 100644 --- a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java @@ -6,7 +6,6 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.HashMap; @@ -45,37 +44,37 @@ public Future getAccountById(String accountId, Timeout timeout) { } @Override - public Future getStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return proxy.getStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getAmpStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return proxy.getAmpStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getVideoStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return proxy.getVideoStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getProfiles(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return proxy.getProfiles(accountId, requestIds, impIds, timeout); } @@ -109,10 +108,10 @@ public Future getAccountById(String accountId, Timeout timeout) { } @Override - public Future getStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return getStoredDataOrDelegate( accountId, @@ -124,10 +123,10 @@ public Future getStoredData(String accountId, } @Override - public Future getAmpStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return getStoredDataOrDelegate( accountId, @@ -139,10 +138,10 @@ public Future getAmpStoredData(String accountId, } @Override - public Future getVideoStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return getStoredDataOrDelegate( accountId, @@ -153,12 +152,27 @@ public Future getVideoStoredData(String accountId, next != null ? next::getVideoStoredData : null); } - private static Future getStoredDataOrDelegate(String accountId, - Set requestIds, - Set impIds, - Timeout timeout, - StoredDataFetcher retriever, - StoredDataFetcher nextRetriever) { + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, + applicationSettings::getProfiles, + next != null ? next::getProfiles : null); + } + + private static Future> getStoredDataOrDelegate(String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher retriever, + StoredDataFetcher nextRetriever) { return retriever.apply(accountId, requestIds, impIds, timeout) .compose(retrieverResult -> nextRetriever == null || retrieverResult.getErrors().isEmpty() @@ -173,13 +187,13 @@ private static Future getStoredDataOrDelegate(String accountId nextRetriever)); } - private static Future getRemainingStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout, - Map storedIdToRequest, - Map storedIdToImp, - StoredDataFetcher retriever) { + private static Future> getRemainingStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + Map storedIdToRequest, + Map storedIdToImp, + StoredDataFetcher retriever) { return retriever.apply( accountId, @@ -192,42 +206,6 @@ private static Future getRemainingStoredData(String accountId, result.getErrors())); } - @Override - public Future getProfiles(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - return applicationSettings.getProfiles(accountId, requestIds, impIds, timeout) - .compose(result -> next == null || result.getErrors().isEmpty() - ? Future.succeededFuture(result) - : getRemainingProfiles( - accountId, - requestIds, - impIds, - timeout, - result.getIdToRequestProfile(), - result.getIdToImpProfile())); - } - - private Future getRemainingProfiles( - String accountId, - Set requestIds, - Set impIds, - Timeout timeout, - Map idToRequestProfile, - Map idToImpProfile) { - - return next.getProfiles( - accountId, - subtractSets(requestIds, idToRequestProfile.keySet()), - subtractSets(impIds, idToImpProfile.keySet()), - timeout) - .map(result -> StoredProfileResult.of( - combineMaps(idToRequestProfile, result.getIdToRequestProfile()), - combineMaps(idToImpProfile, result.getIdToImpProfile()), - result.getErrors())); - } @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java index 9653706d988..0fa02b99751 100644 --- a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -10,12 +10,13 @@ import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.helper.DatabaseProfilesResultMapper; import org.prebid.server.settings.helper.DatabaseStoredDataResultMapper; import org.prebid.server.settings.helper.DatabaseStoredResponseResultMapper; import org.prebid.server.settings.helper.ParametrizedQueryHelper; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.settings.model.StoredProfileResult; import org.prebid.server.settings.model.StoredResponseDataResult; import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient; import org.prebid.server.vertx.database.DatabaseClient; @@ -64,6 +65,16 @@ public class DatabaseApplicationSettings implements ApplicationSettings { */ private final String selectAmpStoredRequestsQuery; + /** + * Query to select profiles by ids, for example: + *

+     * SELECT accountId, profileId, profile, mergePrecedence, type
+     *   FROM profiles
+     *   WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)
+     * 
+ */ + private final String selectProfilesQuery; + /** * Query to select stored responses by ids, for example: *
@@ -80,6 +91,7 @@ public DatabaseApplicationSettings(DatabaseClient databaseClient,
                                        String selectAccountQuery,
                                        String selectStoredRequestsQuery,
                                        String selectAmpStoredRequestsQuery,
+                                       String selectProfilesQuery,
                                        String selectStoredResponsesQuery) {
 
         this.databaseClient = Objects.requireNonNull(databaseClient);
@@ -89,6 +101,7 @@ public DatabaseApplicationSettings(DatabaseClient databaseClient,
                 Objects.requireNonNull(selectAccountQuery));
         this.selectStoredRequestsQuery = Objects.requireNonNull(selectStoredRequestsQuery);
         this.selectAmpStoredRequestsQuery = Objects.requireNonNull(selectAmpStoredRequestsQuery);
+        this.selectProfilesQuery = Objects.requireNonNull(selectProfilesQuery);
         this.selectStoredResponsesQuery = Objects.requireNonNull(selectStoredResponsesQuery);
     }
 
@@ -125,37 +138,66 @@ private Account toAccount(Row row) {
     }
 
     @Override
-    public Future getStoredData(String accountId,
-                                                  Set requestIds,
-                                                  Set impIds,
-                                                  Timeout timeout) {
-
-        return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout);
+    public Future> getStoredData(String accountId,
+                                                          Set requestIds,
+                                                          Set impIds,
+                                                          Timeout timeout) {
+
+        return fetchStoredData(
+                selectStoredRequestsQuery,
+                requestIds,
+                impIds,
+                result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds),
+                timeout);
     }
 
     @Override
-    public Future getAmpStoredData(String accountId,
-                                                     Set requestIds,
-                                                     Set impIds,
-                                                     Timeout timeout) {
-
-        return fetchStoredData(selectAmpStoredRequestsQuery, accountId, requestIds, Collections.emptySet(), timeout);
+    public Future> getAmpStoredData(String accountId,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
+
+        return fetchStoredData(
+                selectAmpStoredRequestsQuery,
+                requestIds,
+                impIds,
+                result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds),
+                timeout);
     }
 
     @Override
-    public Future getVideoStoredData(String accountId,
-                                                       Set requestIds,
-                                                       Set impIds,
-                                                       Timeout timeout) {
+    public Future> getVideoStoredData(String accountId,
+                                                               Set requestIds,
+                                                               Set impIds,
+                                                               Timeout timeout) {
+
+        return fetchStoredData(
+                selectStoredRequestsQuery,
+                requestIds,
+                impIds,
+                result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds),
+                timeout);
+    }
 
-        return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout);
+    @Override
+    public Future> getProfiles(String accountId,
+                                                         Set requestIds,
+                                                         Set impIds,
+                                                         Timeout timeout) {
+
+        return fetchStoredData(
+                selectProfilesQuery,
+                requestIds,
+                impIds,
+                result -> DatabaseProfilesResultMapper.map(result, accountId, requestIds, impIds),
+                timeout);
     }
 
-    private Future fetchStoredData(String query,
-                                                     String accountId,
-                                                     Set requestIds,
-                                                     Set impIds,
-                                                     Timeout timeout) {
+    private  Future> fetchStoredData(String query,
+                                                            Set requestIds,
+                                                            Set impIds,
+                                                            Function, StoredDataResult> mapper,
+                                                            Timeout timeout) {
 
         if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) {
             return Future.succeededFuture(StoredDataResult.of(
@@ -173,21 +215,7 @@ private Future fetchStoredData(String query,
         final String parametrizedQuery = parametrizedQueryHelper
                 .replaceRequestAndImpIdPlaceholders(query, requestIds.size(), impIds.size());
 
-        return databaseClient.executeQuery(
-                parametrizedQuery,
-                idsQueryParameters,
-                result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds),
-                timeout);
-    }
-
-    @Override
-    public Future getProfiles(String accountId,
-                                                   Set requestIds,
-                                                   Set impIds,
-                                                   Timeout timeout) {
-
-        // TODO: query?
-        return Future.failedFuture("Not implemented");
+        return databaseClient.executeQuery(parametrizedQuery, idsQueryParameters, mapper, timeout);
     }
 
     @Override
diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java
index 42d89a66286..5d1875c742c 100644
--- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java
@@ -12,8 +12,8 @@
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.AccountAuctionConfig;
 import org.prebid.server.settings.model.AccountPriceFloorsConfig;
+import org.prebid.server.settings.model.Profile;
 import org.prebid.server.settings.model.StoredDataResult;
-import org.prebid.server.settings.model.StoredProfileResult;
 import org.prebid.server.settings.model.StoredResponseDataResult;
 
 import java.util.Map;
@@ -91,37 +91,37 @@ private Future recoverIfNeeded(Throwable throwable, String accountId) {
     }
 
     @Override
-    public Future getStoredData(String accountId,
-                                                  Set requestIds,
-                                                  Set impIds,
-                                                  Timeout timeout) {
+    public Future> getStoredData(String accountId,
+                                                          Set requestIds,
+                                                          Set impIds,
+                                                          Timeout timeout) {
 
         return delegate.getStoredData(accountId, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getAmpStoredData(String accountId,
-                                                     Set requestIds,
-                                                     Set impIds,
-                                                     Timeout timeout) {
+    public Future> getAmpStoredData(String accountId,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
 
         return delegate.getAmpStoredData(accountId, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getVideoStoredData(String accountId,
-                                                       Set requestIds,
-                                                       Set impIds,
-                                                       Timeout timeout) {
+    public Future> getVideoStoredData(String accountId,
+                                                               Set requestIds,
+                                                               Set impIds,
+                                                               Timeout timeout) {
 
         return delegate.getVideoStoredData(accountId, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getProfiles(String accountId,
-                                                   Set requestIds,
-                                                   Set impIds,
-                                                   Timeout timeout) {
+    public Future> getProfiles(String accountId,
+                                                         Set requestIds,
+                                                         Set impIds,
+                                                         Timeout timeout) {
 
         return delegate.getProfiles(accountId, requestIds, impIds, timeout);
     }
diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java
index 507c9333899..efb64ddd061 100644
--- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java
@@ -14,10 +14,10 @@
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.Category;
+import org.prebid.server.settings.model.Profile;
 import org.prebid.server.settings.model.SettingsFile;
 import org.prebid.server.settings.model.StoredDataResult;
 import org.prebid.server.settings.model.StoredDataType;
-import org.prebid.server.settings.model.StoredProfileResult;
 import org.prebid.server.settings.model.StoredResponseDataResult;
 
 import java.io.File;
@@ -129,10 +129,10 @@ public Future getAccountById(String accountId, Timeout timeout) {
     }
 
     @Override
-    public Future getStoredData(String accountId,
-                                                  Set requestIds,
-                                                  Set impIds,
-                                                  Timeout timeout) {
+    public Future> getStoredData(String accountId,
+                                                          Set requestIds,
+                                                          Set impIds,
+                                                          Timeout timeout) {
 
         return CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)
 
@@ -152,28 +152,28 @@ public Future getStoredData(String accountId,
     }
 
     @Override
-    public Future getAmpStoredData(String accountId,
-                                                     Set requestIds,
-                                                     Set impIds,
-                                                     Timeout timeout) {
+    public Future> getAmpStoredData(String accountId,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
 
         return getStoredData(accountId, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getVideoStoredData(String accountId,
-                                                       Set requestIds,
-                                                       Set impIds,
-                                                       Timeout timeout) {
+    public Future> getVideoStoredData(String accountId,
+                                                               Set requestIds,
+                                                               Set impIds,
+                                                               Timeout timeout) {
 
         return getStoredData(accountId, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getProfiles(String accountId,
-                                                   Set requestIds,
-                                                   Set impIds,
-                                                   Timeout timeout) {
+    public Future> getProfiles(String accountId,
+                                                         Set requestIds,
+                                                         Set impIds,
+                                                         Timeout timeout) {
 
         // TODO: implement
         return Future.failedFuture("Not implemented");
diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java
index a9e7992f27d..5c154b49055 100644
--- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java
@@ -16,9 +16,9 @@
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.Category;
+import org.prebid.server.settings.model.Profile;
 import org.prebid.server.settings.model.StoredDataResult;
 import org.prebid.server.settings.model.StoredDataType;
-import org.prebid.server.settings.model.StoredProfileResult;
 import org.prebid.server.settings.model.StoredResponseDataResult;
 import org.prebid.server.settings.proto.response.HttpAccountsResponse;
 import org.prebid.server.settings.proto.response.HttpFetcherResponse;
@@ -147,36 +147,36 @@ private Set processAccountsResponse(HttpClientResponse httpClientRespon
     }
 
     @Override
-    public Future getStoredData(String accountId,
-                                                  Set requestIds,
-                                                  Set impIds,
-                                                  Timeout timeout) {
+    public Future> getStoredData(String accountId,
+                                                          Set requestIds,
+                                                          Set impIds,
+                                                          Timeout timeout) {
 
         return fetchStoredData(endpoint, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getAmpStoredData(String accountId,
-                                                     Set requestIds,
-                                                     Set impIds,
-                                                     Timeout timeout) {
+    public Future> getAmpStoredData(String accountId,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
 
         return fetchStoredData(ampEndpoint, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getVideoStoredData(String accountId,
-                                                       Set requestIds,
-                                                       Set impIds,
-                                                       Timeout timeout) {
+    public Future> getVideoStoredData(String accountId,
+                                                               Set requestIds,
+                                                               Set impIds,
+                                                               Timeout timeout) {
 
         return fetchStoredData(videoEndpoint, requestIds, impIds, timeout);
     }
 
-    private Future fetchStoredData(String endpoint,
-                                                     Set requestIds,
-                                                     Set impIds,
-                                                     Timeout timeout) {
+    private Future> fetchStoredData(String endpoint,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
 
         if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) {
             return Future.succeededFuture(
@@ -193,17 +193,17 @@ private Future fetchStoredData(String endpoint,
                 .recover(exception -> failStoredDataResponse(exception, requestIds, impIds));
     }
 
-    private static Future failStoredDataResponse(Throwable throwable,
-                                                                   Set requestIds,
-                                                                   Set impIds) {
+    private static Future> failStoredDataResponse(Throwable throwable,
+                                                                           Set requestIds,
+                                                                           Set impIds) {
 
         return Future.succeededFuture(toFailedStoredDataResult(requestIds, impIds, throwable.getMessage()));
     }
 
-    private static StoredDataResult toFailedStoredDataResult(Set requestIds,
-                                                             Set impIds,
-                                                             String errorMessageFormat,
-                                                             Object... args) {
+    private static StoredDataResult toFailedStoredDataResult(Set requestIds,
+                                                                     Set impIds,
+                                                                     String errorMessageFormat,
+                                                                     Object... args) {
 
         final String errorRequests = requestIds.isEmpty() ? "" : "stored requests for ids " + requestIds;
         final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and ";
@@ -234,9 +234,9 @@ private static String storeRequestUrlFrom(String endpoint, Set requestId
         return url.toString();
     }
 
-    private StoredDataResult processStoredDataResponse(HttpClientResponse httpClientResponse,
-                                                       Set requestIds,
-                                                       Set impIds) {
+    private StoredDataResult processStoredDataResponse(HttpClientResponse httpClientResponse,
+                                                               Set requestIds,
+                                                               Set impIds) {
 
         final int statusCode = httpClientResponse.getStatusCode();
         if (statusCode != HttpResponseStatus.OK.code()) {
@@ -255,7 +255,7 @@ private StoredDataResult processStoredDataResponse(HttpClientResponse httpClient
         return parseResponse(requestIds, impIds, response);
     }
 
-    private StoredDataResult parseResponse(Set requestIds, Set impIds, HttpFetcherResponse response) {
+    private StoredDataResult parseResponse(Set requestIds, Set impIds, HttpFetcherResponse response) {
         final List errors = new ArrayList<>();
 
         final Map storedIdToRequest =
@@ -301,18 +301,19 @@ private Map parseStoredDataOrAddError(Set ids,
     }
 
     @Override
-    public Future getProfiles(String accountId,
-                                                   Set requestIds,
-                                                   Set impIds,
-                                                   Timeout timeout) {
-
-        // TODO: change to success
-        return Future.failedFuture("Not supported");
+    public Future> getProfiles(String accountId,
+                                                         Set requestIds,
+                                                         Set impIds,
+                                                         Timeout timeout) {
+
+        return Future.succeededFuture(StoredDataResult.of(
+                Collections.emptyMap(),
+                Collections.emptyMap(),
+                Collections.emptyList()));
     }
 
     @Override
     public Future getStoredResponses(Set responseIds, Timeout timeout) {
-        // TODO: ???
         return Future.failedFuture(new PreBidException("Not supported"));
     }
 
@@ -362,7 +363,6 @@ private PreBidException makeFailedCategoryFetchException(String url, String reas
     }
 
     private static String joinIds(Set ids) {
-        // TODO: Add url encode
         return String.join("\",\"", ids);
     }
 }
diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java
index a4cbb89496c..35d444ffaf3 100644
--- a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java
@@ -12,8 +12,8 @@
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.Profile;
 import org.prebid.server.settings.model.StoredDataResult;
-import org.prebid.server.settings.model.StoredProfileResult;
 import org.prebid.server.settings.model.StoredResponseDataResult;
 import software.amazon.awssdk.core.BytesWrapper;
 import software.amazon.awssdk.core.async.AsyncResponseTransformer;
@@ -101,10 +101,10 @@ private static void validateAccount(Account account, String requestedAccountId)
     }
 
     @Override
-    public Future getStoredData(String accountId,
-                                                  Set requestIds,
-                                                  Set impIds,
-                                                  Timeout timeout) {
+    public Future> getStoredData(String accountId,
+                                                          Set requestIds,
+                                                          Set impIds,
+                                                          Timeout timeout) {
 
         return withTimeout(
                 () -> Future.all(
@@ -118,10 +118,10 @@ public Future getStoredData(String accountId,
                         impIds));
     }
 
-    private StoredDataResult buildStoredDataResult(Map storedIdToRequest,
-                                                   Map storedIdToImp,
-                                                   Set requestIds,
-                                                   Set impIds) {
+    private StoredDataResult buildStoredDataResult(Map storedIdToRequest,
+                                                           Map storedIdToImp,
+                                                           Set requestIds,
+                                                           Set impIds) {
 
         final List errors = Stream.concat(
                         missingStoredDataIds(storedIdToImp, impIds).stream()
@@ -134,31 +134,33 @@ private StoredDataResult buildStoredDataResult(Map storedIdToReq
     }
 
     @Override
-    public Future getAmpStoredData(String accountId,
-                                                     Set requestIds,
-                                                     Set impIds,
-                                                     Timeout timeout) {
+    public Future> getAmpStoredData(String accountId,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
 
         return getStoredData(accountId, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getVideoStoredData(String accountId,
-                                                       Set requestIds,
-                                                       Set impIds,
-                                                       Timeout timeout) {
+    public Future> getVideoStoredData(String accountId,
+                                                               Set requestIds,
+                                                               Set impIds,
+                                                               Timeout timeout) {
 
         return getStoredData(accountId, requestIds, impIds, timeout);
     }
 
     @Override
-    public Future getProfiles(String accountId,
-                                                   Set requestIds,
-                                                   Set impIds,
-                                                   Timeout timeout) {
-
-        // TODO: change to success
-        return Future.failedFuture("Not supported");
+    public Future> getProfiles(String accountId,
+                                                         Set requestIds,
+                                                         Set impIds,
+                                                         Timeout timeout) {
+
+        return Future.succeededFuture(StoredDataResult.of(
+                Collections.emptyMap(),
+                Collections.emptyMap(),
+                Collections.emptyList()));
     }
 
     @Override
diff --git a/src/main/java/org/prebid/server/settings/SettingsCache.java b/src/main/java/org/prebid/server/settings/SettingsCache.java
index 3e2446742b1..1066d0a121c 100644
--- a/src/main/java/org/prebid/server/settings/SettingsCache.java
+++ b/src/main/java/org/prebid/server/settings/SettingsCache.java
@@ -17,10 +17,10 @@
 /**
  * Just a simple wrapper over in-memory caches for requests and imps.
  */
-public class SettingsCache implements CacheNotificationListener {
+public class SettingsCache implements CacheNotificationListener {
 
-    private final Map> requestCache;
-    private final Map> impCache;
+    private final Map>> requestCache;
+    private final Map>> impCache;
 
     public SettingsCache(int ttl, int size, int jitter) {
         if (ttl <= 0 || size <= 0) {
@@ -47,28 +47,28 @@ public static  Map createCache(int ttlSeconds, int size, int jitte
                 .asMap();
     }
 
-    Map> getRequestCache() {
+    Map>> getRequestCache() {
         return requestCache;
     }
 
-    Map> getImpCache() {
+    Map>> getImpCache() {
         return impCache;
     }
 
-    void saveRequestCache(String accountId, String requestId, String requestValue) {
-        saveCachedValue(requestCache, accountId, requestId, requestValue);
+    void saveRequestCache(String accountId, String requestId, T value) {
+        saveCachedValue(requestCache, accountId, requestId, value);
     }
 
-    void saveImpCache(String accountId, String impId, String impValue) {
-        saveCachedValue(impCache, accountId, impId, impValue);
+    void saveImpCache(String accountId, String impId, T value) {
+        saveCachedValue(impCache, accountId, impId, value);
     }
 
-    private static void saveCachedValue(Map> cache,
-                                        String accountId,
-                                        String id,
-                                        String value) {
+    private static  void saveCachedValue(Map>> cache,
+                                            String accountId,
+                                            String id,
+                                            T value) {
 
-        final Set values = ObjectUtils.defaultIfNull(cache.get(id), new HashSet<>());
+        final Set> values = ObjectUtils.defaultIfNull(cache.get(id), new HashSet<>());
         values.add(StoredItem.of(accountId, value));
         cache.put(id, values);
     }
@@ -79,7 +79,7 @@ private static void saveCachedValue(Map> cache,
      * TODO: account should be added to all services uses this method
      */
     @Override
-    public void save(Map requests, Map imps) {
+    public void save(Map requests, Map imps) {
         if (MapUtils.isNotEmpty(requests)) {
             requests.forEach((key, value) -> requestCache.put(key, Collections.singleton(StoredItem.of(null, value))));
         }
diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java
new file mode 100644
index 00000000000..79c14f75c8b
--- /dev/null
+++ b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java
@@ -0,0 +1,167 @@
+package org.prebid.server.settings.helper;
+
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowIterator;
+import io.vertx.sqlclient.RowSet;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.settings.model.Profile;
+import org.prebid.server.settings.model.StoredDataResult;
+import org.prebid.server.settings.model.StoredItem;
+import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class DatabaseProfilesResultMapper {
+
+    private static final Logger logger = LoggerFactory.getLogger(DatabaseProfilesResultMapper.class);
+
+    private DatabaseProfilesResultMapper() {
+    }
+
+    public static StoredDataResult map(RowSet resultSet) {
+        return map(resultSet, null, Collections.emptySet(), Collections.emptySet());
+    }
+
+    /**
+     * Note: mapper should never throw exception in case of using
+     * {@link CircuitBreakerSecuredDatabaseClient}.
+     */
+    public static StoredDataResult map(RowSet rowSet,
+                                                String accountId,
+                                                Set requestIds,
+                                                Set impIds) {
+
+        final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null;
+        final List errors = new ArrayList<>();
+
+        if (rowIterator == null || !rowIterator.hasNext()) {
+            handleEmptyResult(requestIds, impIds, errors);
+
+            return StoredDataResult.of(
+                    Collections.emptyMap(),
+                    Collections.emptyMap(),
+                    Collections.unmodifiableList(errors));
+        }
+
+        final Map>> requestIdToProfiles = new HashMap<>();
+        final Map>> impIdToProfiles = new HashMap<>();
+
+        while (rowIterator.hasNext()) {
+            final Row row = rowIterator.next();
+            if (row.size() < 5) {
+                final String message = "Error occurred while mapping profiles: some columns are missing";
+                logger.error(message);
+                errors.add(message);
+
+                return StoredDataResult.of(
+                        Collections.emptyMap(),
+                        Collections.emptyMap(),
+                        Collections.unmodifiableList(errors));
+            }
+
+            final String fetchedAccountId = Objects.toString(row.getValue(0), null);
+            final String id = Objects.toString(row.getValue(1), null);
+            final String profileBody = Objects.toString(row.getValue(2), null);
+            final String mergePrecedenceAsString = Objects.toString(row.getValue(3), null);
+            final String typeAsString = Objects.toString(row.getValue(4), null);
+
+            final Profile.Type type;
+            final Profile.MergePrecedence mergePrecedence;
+            try {
+                type = Profile.Type.valueOf(typeAsString);
+                mergePrecedence = Profile.MergePrecedence.valueOf(mergePrecedenceAsString);
+            } catch (IllegalArgumentException e) {
+                logger.error("Profile with id={} has invalid value: ''{}'' and will be ignored.",
+                        e, id, typeAsString);
+                continue;
+            }
+
+            final Profile profile = Profile.of(type, mergePrecedence, profileBody);
+
+            if (type == Profile.Type.REQUEST) {
+                addStoredItem(fetchedAccountId, id, profile, requestIdToProfiles);
+            } else {
+                addStoredItem(fetchedAccountId, id, profile, impIdToProfiles);
+            }
+        }
+
+        return StoredDataResult.of(
+                storedItemsOrAddError(
+                        Profile.Type.REQUEST,
+                        accountId,
+                        requestIds,
+                        requestIdToProfiles,
+                        errors),
+                storedItemsOrAddError(
+                        Profile.Type.IMP,
+                        accountId,
+                        impIds,
+                        impIdToProfiles,
+                        errors),
+                Collections.unmodifiableList(errors));
+    }
+
+    private static void handleEmptyResult(Set requestIds, Set impIds, List errors) {
+        if (requestIds.isEmpty() && impIds.isEmpty()) {
+            errors.add("No profiles were found");
+        } else {
+            final String errorRequests = requestIds.isEmpty()
+                    ? ""
+                    : "request profiles for ids " + requestIds;
+            final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and ";
+            final String errorImps = impIds.isEmpty() ? "" : "imp profiles for ids " + impIds;
+
+            errors.add("No %s%s%s were found".formatted(errorRequests, separator, errorImps));
+        }
+    }
+
+    private static void addStoredItem(String accountId,
+                                      String id,
+                                      Profile profile,
+                                      Map>> idToStoredItems) {
+
+        idToStoredItems.computeIfAbsent(id, key -> new HashSet<>()).add(StoredItem.of(accountId, profile));
+    }
+
+    private static Map storedItemsOrAddError(
+            Profile.Type type,
+            String accountId,
+            Set searchIds,
+            Map>> foundIdToStoredItems,
+            List errors) {
+
+        final Map result = new HashMap<>();
+
+        if (searchIds.isEmpty()) {
+            foundIdToStoredItems.forEach((id, storedItems) -> {
+                for (StoredItem storedItem : storedItems) {
+                    result.put(id, storedItem.getData());
+                }
+            });
+
+            return Collections.unmodifiableMap(result);
+        }
+
+        for (String id : searchIds) {
+            try {
+                final StoredItem resolvedStoredItem = StoredItemResolver
+                        .resolve(type.toString(), accountId, id, foundIdToStoredItems.get(id));
+
+                result.put(id, resolvedStoredItem.getData());
+            } catch (PreBidException e) {
+                errors.add(e.getMessage());
+            }
+        }
+
+        return Collections.unmodifiableMap(result);
+    }
+}
diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java
index f747379b3e0..a2c084569de 100644
--- a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java
+++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java
@@ -30,7 +30,7 @@ private DatabaseStoredDataResultMapper() {
     /**
      * Overloaded method for cases when no specific IDs are required, e.g. fetching all records.
      */
-    public static StoredDataResult map(RowSet resultSet) {
+    public static StoredDataResult map(RowSet resultSet) {
         return map(resultSet, null, Collections.emptySet(), Collections.emptySet());
     }
 
@@ -38,10 +38,10 @@ public static StoredDataResult map(RowSet resultSet) {
      * Note: mapper should never throw exception in case of using
      * {@link CircuitBreakerSecuredDatabaseClient}.
      */
-    public static StoredDataResult map(RowSet rowSet,
-                                       String accountId,
-                                       Set requestIds,
-                                       Set impIds) {
+    public static StoredDataResult map(RowSet rowSet,
+                                               String accountId,
+                                               Set requestIds,
+                                               Set impIds) {
 
         final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null;
         final List errors = new ArrayList<>();
@@ -55,8 +55,8 @@ public static StoredDataResult map(RowSet rowSet,
                     Collections.unmodifiableList(errors));
         }
 
-        final Map> requestIdToStoredItems = new HashMap<>();
-        final Map> impIdToStoredItems = new HashMap<>();
+        final Map>> requestIdToStoredItems = new HashMap<>();
+        final Map>> impIdToStoredItems = new HashMap<>();
 
         while (rowIterator.hasNext()) {
             final Row row = rowIterator.next();
@@ -125,7 +125,7 @@ private static void handleEmptyResult(Set requestIds, Set impIds
     private static void addStoredItem(String accountId,
                                       String id,
                                       String data,
-                                      Map> idToStoredItems) {
+                                      Map>> idToStoredItems) {
 
         idToStoredItems.computeIfAbsent(id, key -> new HashSet<>()).add(StoredItem.of(accountId, data));
     }
@@ -133,14 +133,14 @@ private static void addStoredItem(String accountId,
     private static Map storedItemsOrAddError(StoredDataType type,
                                                              String accountId,
                                                              Set searchIds,
-                                                             Map> foundIdToStoredItems,
+                                                             Map>> foundIdToStoredItems,
                                                              List errors) {
 
         final Map result = new HashMap<>();
 
         if (searchIds.isEmpty()) {
             foundIdToStoredItems.forEach((id, storedItems) -> {
-                for (StoredItem storedItem : storedItems) {
+                for (StoredItem storedItem : storedItems) {
                     result.put(id, storedItem.getData());
                 }
             });
@@ -150,8 +150,8 @@ private static Map storedItemsOrAddError(StoredDataType type,
 
         for (String id : searchIds) {
             try {
-                final StoredItem resolvedStoredItem = StoredItemResolver
-                        .resolve(type, accountId, id, foundIdToStoredItems.get(id));
+                final StoredItem resolvedStoredItem = StoredItemResolver
+                        .resolve(type.toString(), accountId, id, foundIdToStoredItems.get(id));
 
                 result.put(id, resolvedStoredItem.getData());
             } catch (PreBidException e) {
diff --git a/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java
index 5adec2d9bbb..4a0feeeb2bb 100644
--- a/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java
+++ b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java
@@ -7,7 +7,7 @@
 import java.util.Set;
 
 @FunctionalInterface
-public interface StoredDataFetcher {
+public interface StoredDataFetcher {
 
-    Future apply(String account, Set reqIds, Set impIds, Timeout timeout);
+    Future> apply(String account, Set reqIds, Set impIds, Timeout timeout);
 }
diff --git a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java
index a372ee3a417..40f14182717 100644
--- a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java
+++ b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java
@@ -3,7 +3,6 @@
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.settings.model.StoredDataType;
 import org.prebid.server.settings.model.StoredItem;
 
 import java.util.Objects;
@@ -32,7 +31,11 @@ private StoredItemResolver() {
      * 

* - Otherwise, reject stored item as if there hadn't been match. */ - public static StoredItem resolve(StoredDataType type, String accountId, String id, Set storedItems) { + public static StoredItem resolve(String type, + String accountId, + String id, + Set> storedItems) { + if (CollectionUtils.isEmpty(storedItems)) { throw new PreBidException("No stored %s found for id: %s".formatted(type, id)); } @@ -53,7 +56,7 @@ public static StoredItem resolve(StoredDataType type, String accountId, String i } // only one stored item found - final StoredItem storedItem = storedItems.iterator().next(); + final StoredItem storedItem = storedItems.iterator().next(); if (StringUtils.isBlank(accountId) || storedItem.getAccountId() == null || Objects.equals(accountId, storedItem.getAccountId())) { diff --git a/src/main/java/org/prebid/server/settings/model/Profile.java b/src/main/java/org/prebid/server/settings/model/Profile.java index 14dbc8e7326..ea9dc277d57 100644 --- a/src/main/java/org/prebid/server/settings/model/Profile.java +++ b/src/main/java/org/prebid/server/settings/model/Profile.java @@ -1,7 +1,6 @@ package org.prebid.server.settings.model; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; @Value(staticConstructor = "of") @@ -12,7 +11,7 @@ public class Profile { @JsonProperty("mergeprecedence") MergePrecedence mergePrecedence; - ObjectNode body; + String body; public enum Type { diff --git a/src/main/java/org/prebid/server/settings/model/StoredDataResult.java b/src/main/java/org/prebid/server/settings/model/StoredDataResult.java index 81b682d24fa..19840586be1 100644 --- a/src/main/java/org/prebid/server/settings/model/StoredDataResult.java +++ b/src/main/java/org/prebid/server/settings/model/StoredDataResult.java @@ -6,11 +6,11 @@ import java.util.Map; @Value(staticConstructor = "of") -public class StoredDataResult { +public class StoredDataResult { - Map storedIdToRequest; + Map storedIdToRequest; - Map storedIdToImp; + Map storedIdToImp; List errors; } diff --git a/src/main/java/org/prebid/server/settings/model/StoredItem.java b/src/main/java/org/prebid/server/settings/model/StoredItem.java index d84f0065bfa..dd7ee8095ff 100644 --- a/src/main/java/org/prebid/server/settings/model/StoredItem.java +++ b/src/main/java/org/prebid/server/settings/model/StoredItem.java @@ -6,9 +6,9 @@ * The model helps to reduce multiple rows found for single stored request/imp ID. */ @Value(staticConstructor = "of") -public class StoredItem { +public class StoredItem { String accountId; - String data; + T data; } diff --git a/src/main/java/org/prebid/server/settings/model/StoredProfileResult.java b/src/main/java/org/prebid/server/settings/model/StoredProfileResult.java deleted file mode 100644 index e3330a4262d..00000000000 --- a/src/main/java/org/prebid/server/settings/model/StoredProfileResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.settings.model; - -import lombok.Value; - -import java.util.List; -import java.util.Map; - -@Value(staticConstructor = "of") -public class StoredProfileResult { - - Map idToRequestProfile; - - Map idToImpProfile; - - List errors; -} diff --git a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java index bdd9e8258e0..7714243954d 100644 --- a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java +++ b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java @@ -69,7 +69,7 @@ public class DatabasePeriodicRefreshService implements Initializable { private final long refreshPeriod; private final long timeout; private final MetricName cacheType; - private final CacheNotificationListener cacheNotificationListener; + private final CacheNotificationListener cacheNotificationListener; private final Vertx vertx; private final DatabaseClient databaseClient; private final TimeoutFactory timeoutFactory; @@ -83,7 +83,7 @@ public DatabasePeriodicRefreshService(String initQuery, long refreshPeriod, long timeout, MetricName cacheType, - CacheNotificationListener cacheNotificationListener, + CacheNotificationListener cacheNotificationListener, Vertx vertx, DatabaseClient databaseClient, TimeoutFactory timeoutFactory, @@ -125,7 +125,7 @@ private void getAll() { .recover(exception -> handleFailure(exception, startTime, MetricName.initialize)); } - private Void handleResult(StoredDataResult storedDataResult, + private Void handleResult(StoredDataResult storedDataResult, Instant updateTime, long startTime, MetricName refreshType) { @@ -161,7 +161,7 @@ private void refresh() { .recover(exception -> handleFailure(exception, startTime, MetricName.update)); } - private StoredDataResult invalidate(StoredDataResult storedDataResult) { + private StoredDataResult invalidate(StoredDataResult storedDataResult) { final List invalidatedRequests = getInvalidatedKeys(storedDataResult.getStoredIdToRequest()); final List invalidatedImps = getInvalidatedKeys(storedDataResult.getStoredIdToImp()); diff --git a/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java index e1b3bbc068f..7669074d455 100644 --- a/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java +++ b/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java @@ -66,7 +66,7 @@ public class HttpPeriodicRefreshService implements Initializable { private final String refreshUrl; private final long refreshPeriod; private final long timeout; - private final CacheNotificationListener cacheNotificationListener; + private final CacheNotificationListener cacheNotificationListener; private final Vertx vertx; private final HttpClient httpClient; private final JacksonMapper mapper; @@ -76,7 +76,7 @@ public class HttpPeriodicRefreshService implements Initializable { public HttpPeriodicRefreshService(String refreshUrl, long refreshPeriod, long timeout, - CacheNotificationListener cacheNotificationListener, + CacheNotificationListener cacheNotificationListener, Vertx vertx, HttpClient httpClient, JacksonMapper mapper) { diff --git a/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java index d5a8ce7f873..35ff2a6f55a 100644 --- a/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java +++ b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java @@ -43,7 +43,7 @@ public class S3PeriodicRefreshService implements Initializable { private final String storedRequestsDirectory; private final String storedImpressionsDirectory; private final long refreshPeriod; - private final CacheNotificationListener cacheNotificationListener; + private final CacheNotificationListener cacheNotificationListener; private final MetricName cacheType; private final Clock clock; private final Metrics metrics; @@ -54,7 +54,7 @@ public S3PeriodicRefreshService(S3AsyncClient asyncClient, String storedRequestsDirectory, String storedImpressionsDirectory, long refreshPeriod, - CacheNotificationListener cacheNotificationListener, + CacheNotificationListener cacheNotificationListener, MetricName cacheType, Clock clock, Metrics metrics, @@ -84,7 +84,7 @@ public void initialize(Promise initializePromise) { } } - private Future fetchStoredDataResult(long startTime, MetricName metricName) { + private Future> fetchStoredDataResult(long startTime, MetricName metricName) { return Future.all( getFileContentsForDirectory(storedRequestsDirectory), getFileContentsForDirectory(storedImpressionsDirectory)) @@ -132,7 +132,7 @@ private static String stripFileName(String directory, String name) { .replace(JSON_SUFFIX, ""); } - private void handleResult(StoredDataResult storedDataResult, long startTime, MetricName refreshType) { + private void handleResult(StoredDataResult storedDataResult, long startTime, MetricName refreshType) { cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp()); metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime); } diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 6f00f19d63c..ab89325e118 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -26,6 +26,7 @@ import org.prebid.server.settings.S3ApplicationSettings; import org.prebid.server.settings.SettingsCache; import org.prebid.server.settings.helper.ParametrizedQueryHelper; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.service.DatabasePeriodicRefreshService; import org.prebid.server.settings.service.HttpPeriodicRefreshService; import org.prebid.server.settings.service.S3PeriodicRefreshService; @@ -95,6 +96,7 @@ DatabaseApplicationSettings databaseApplicationSettings( @Value("${settings.database.account-query}") String accountQuery, @Value("${settings.database.stored-requests-query}") String storedRequestsQuery, @Value("${settings.database.amp-stored-requests-query}") String ampStoredRequestsQuery, + @Value("${settings.database.profiles-query}") String profilesQuery, @Value("${settings.database.stored-responses-query}") String storedResponsesQuery, ParametrizedQueryHelper parametrizedQueryHelper, DatabaseClient databaseClient, @@ -107,6 +109,7 @@ DatabaseApplicationSettings databaseApplicationSettings( accountQuery, storedRequestsQuery, ampStoredRequestsQuery, + profilesQuery, storedResponsesQuery); } } @@ -154,7 +157,7 @@ static class HttpPeriodicRefreshServiceConfiguration { @Bean public HttpPeriodicRefreshService httpPeriodicRefreshService( @Value("${settings.in-memory-cache.http-update.endpoint}") String endpoint, - SettingsCache settingsCache, + SettingsCache settingsCache, JacksonMapper mapper) { return new HttpPeriodicRefreshService( @@ -164,7 +167,7 @@ public HttpPeriodicRefreshService httpPeriodicRefreshService( @Bean public HttpPeriodicRefreshService ampHttpPeriodicRefreshService( @Value("${settings.in-memory-cache.http-update.amp-endpoint}") String ampEndpoint, - SettingsCache ampSettingsCache, + SettingsCache ampSettingsCache, JacksonMapper mapper) { return new HttpPeriodicRefreshService( @@ -201,7 +204,7 @@ static class DatabasePeriodicRefreshServiceConfiguration { @Bean public DatabasePeriodicRefreshService databasePeriodicRefreshService( - @Qualifier("settingsCache") SettingsCache settingsCache, + @Qualifier("settingsCache") SettingsCache settingsCache, @Value("${settings.in-memory-cache.database-update.init-query}") String initQuery, @Value("${settings.in-memory-cache.database-update.update-query}") String updateQuery) { @@ -221,7 +224,7 @@ public DatabasePeriodicRefreshService databasePeriodicRefreshService( @Bean public DatabasePeriodicRefreshService ampDatabasePeriodicRefreshService( - @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache, + @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache, @Value("${settings.in-memory-cache.database-update.amp-init-query}") String ampInitQuery, @Value("${settings.in-memory-cache.database-update.amp-update-query}") String ampUpdateQuery) { @@ -353,7 +356,7 @@ public S3PeriodicRefreshService s3PeriodicRefreshService( S3AsyncClient s3AsyncClient, S3SettingsConfiguration.S3ConfigurationProperties s3ConfigurationProperties, @Value("${settings.in-memory-cache.s3-update.refresh-rate}") long refreshPeriod, - SettingsCache settingsCache, + SettingsCache settingsCache, Clock clock, Metrics metrics, Vertx vertx) { @@ -429,9 +432,10 @@ static class CachingSettingsConfiguration { CachingApplicationSettings cachingApplicationSettings( EnrichingApplicationSettings enrichingApplicationSettings, ApplicationSettingsCacheProperties cacheProperties, - @Qualifier("settingsCache") SettingsCache cache, - @Qualifier("ampSettingsCache") SettingsCache ampCache, - @Qualifier("videoSettingCache") SettingsCache videoCache, + @Qualifier("settingsCache") SettingsCache cache, + @Qualifier("ampSettingsCache") SettingsCache ampCache, + @Qualifier("videoSettingCache") SettingsCache videoCache, + @Qualifier("profileSettingCache") SettingsCache profilesCache, Metrics metrics) { return new CachingApplicationSettings( @@ -439,6 +443,7 @@ CachingApplicationSettings cachingApplicationSettings( cache, ampCache, videoCache, + profilesCache, metrics, cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize(), @@ -463,8 +468,8 @@ static class CacheConfiguration { @Bean @Qualifier("settingsCache") - SettingsCache settingsCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache( + SettingsCache settingsCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize(), cacheProperties.getJitterSeconds()); @@ -472,8 +477,8 @@ SettingsCache settingsCache(ApplicationSettingsCacheProperties cacheProperties) @Bean @Qualifier("ampSettingsCache") - SettingsCache ampSettingsCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache( + SettingsCache ampSettingsCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize(), cacheProperties.getJitterSeconds()); @@ -481,8 +486,17 @@ SettingsCache ampSettingsCache(ApplicationSettingsCacheProperties cachePropertie @Bean @Qualifier("videoSettingCache") - SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache( + SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); + } + + @Bean + @Qualifier("profileSettingCache") + SettingsCache profileSettingCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize(), cacheProperties.getJitterSeconds()); diff --git a/src/main/java/org/prebid/server/spring/config/server/admin/AdminEndpointsConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/admin/AdminEndpointsConfiguration.java index 31f50e8db6d..a5938aab396 100644 --- a/src/main/java/org/prebid/server/spring/config/server/admin/AdminEndpointsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/server/admin/AdminEndpointsConfiguration.java @@ -76,34 +76,34 @@ AdminResource currencyConversionRatesEndpoint( @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" + " and ${admin-endpoints.storedrequest.enabled} == true") AdminResource cacheNotificationEndpoint( - SettingsCache settingsCache, - JacksonMapper mapper, @Value("${admin-endpoints.storedrequest.path}") String path, @Value("${admin-endpoints.storedrequest.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.storedrequest.protected}") boolean isProtected) { + @Value("${admin-endpoints.storedrequest.protected}") boolean isProtected, + SettingsCache settingsCache, + JacksonMapper mapper) { return new AdminResourceWrapper( path, isOnApplicationPort, isProtected, - new SettingsCacheNotificationHandler(settingsCache, mapper, path)); + new SettingsCacheNotificationHandler(path, settingsCache, mapper)); } @Bean @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" + " and ${admin-endpoints.storedrequest-amp.enabled} == true") AdminResource ampCacheNotificationEndpoint( - SettingsCache ampSettingsCache, - JacksonMapper mapper, @Value("${admin-endpoints.storedrequest-amp.path}") String path, @Value("${admin-endpoints.storedrequest-amp.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.storedrequest-amp.protected}") boolean isProtected) { + @Value("${admin-endpoints.storedrequest-amp.protected}") boolean isProtected, + SettingsCache ampSettingsCache, + JacksonMapper mapper) { return new AdminResourceWrapper( path, isOnApplicationPort, isProtected, - new SettingsCacheNotificationHandler(ampSettingsCache, mapper, path)); + new SettingsCacheNotificationHandler(path, ampSettingsCache, mapper)); } @Bean From 69756893e88dce43593da7143104caa2bec5615b Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 22 Jul 2025 20:52:40 +0200 Subject: [PATCH 05/24] Apply profiles limit --- .../externalortb/ProfilesProcessor.java | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index 56b74886d99..6b3cfe8151c 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -19,6 +19,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountProfilesConfig; import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; @@ -63,11 +65,16 @@ public ProfilesProcessor(int maxProfiles, public Future process(Account account, BidRequest bidRequest) { final List imps = bidRequest.getImp(); - final AllProfilesIds profilesIds = truncate(new AllProfilesIds( - requestProfilesIds(bidRequest), - imps.stream() - .map(this::impProfilesIds) - .toList())); + final AllProfilesIds profilesIds = truncate( + new AllProfilesIds( + requestProfilesIds(bidRequest), + imps.stream() + .map(this::impProfilesIds) + .toList()), + Optional.ofNullable(account.getAuction()) + .map(AccountAuctionConfig::getProfiles) + .map(AccountProfilesConfig::getLimit) + .orElse(maxProfiles)); if (profilesIds.isEmpty()) { return Future.succeededFuture(bidRequest); @@ -105,14 +112,21 @@ private ExtImpPrebid parseImpExt(JsonNode jsonNode) { } } - private AllProfilesIds truncate(AllProfilesIds profilesIds) { - // TODO: - // 1. How to limit for multiple imps (each contains profiles)? - // 2. Which of these approaches is correct? - // - limit -> fetch - // - fetch -> limit (don't count invalid profiles) - // - fetch -> limit (count invalid profiles) - return profilesIds; + private static AllProfilesIds truncate(AllProfilesIds profilesIds, int maxProfiles) { + final List requestProfiles = profilesIds.request(); + final int impProfilesLimit = maxProfiles - requestProfiles.size(); + + return impProfilesLimit > 0 + ? new AllProfilesIds( + requestProfiles, + profilesIds.imps().stream() + .map(impProfiles -> truncate(impProfiles, impProfilesLimit)) + .toList()) + : new AllProfilesIds(truncate(requestProfiles, maxProfiles), Collections.emptyList()); + } + + private static List truncate(List list, int maxSize) { + return list.size() > maxSize ? list.subList(0, maxSize) : list; } private long timeoutMillis(BidRequest bidRequest) { @@ -168,13 +182,14 @@ private T applyProfiles(List profilesIds, } private ObjectNode mergeProfile(ObjectNode original, Profile profile, String profileId) { + final ObjectNode profileBody = parseProfile(profile.getBody()); return switch (profile.getMergePrecedence()) { - case REQUEST -> merge(original, parse(profile.getBody()), profileId); - case PROFILE -> merge(parse(profile.getBody()), original, profileId); + case REQUEST -> merge(original, profileBody, profileId); + case PROFILE -> merge(profileBody, original, profileId); }; } - private ObjectNode parse(String body) { + private ObjectNode parseProfile(String body) { try { return mapper.decodeValue(body, ObjectNode.class); } catch (DecodeException e) { From 96108990a6e24cac54222cbf385ec2cb03f3d569 Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 22 Jul 2025 23:22:06 +0200 Subject: [PATCH 06/24] Refactor --- .../settings/CachingApplicationSettings.java | 35 +++++++++---------- .../settings/helper/StoredItemResolver.java | 6 +--- .../settings/model/AccountProfilesConfig.java | 5 --- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index e0fcfa2fa31..195fe895db8 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -127,7 +127,7 @@ public Future> getStoredData(String accountId, Set impIds, Timeout timeout) { - return getStoredDataFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); + return getFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); } @Override @@ -136,8 +136,7 @@ public Future> getAmpStoredData(String accountId, Set impIds, Timeout timeout) { - return getStoredDataFromCacheOrDelegate( - ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); + return getFromCacheOrDelegate(ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); } @Override @@ -146,8 +145,7 @@ public Future> getVideoStoredData(String accountId, Set impIds, Timeout timeout) { - return getStoredDataFromCacheOrDelegate( - videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); + return getFromCacheOrDelegate(videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); } @Override @@ -156,16 +154,15 @@ public Future> getProfiles(String accountId, Set impIds, Timeout timeout) { - return getStoredDataFromCacheOrDelegate( - profileCache, accountId, requestIds, impIds, timeout, delegate::getProfiles); + return getFromCacheOrDelegate(profileCache, accountId, requestIds, impIds, timeout, delegate::getProfiles); } - private static Future> getStoredDataFromCacheOrDelegate(SettingsCache cache, - String accountId, - Set requestIds, - Set impIds, - Timeout timeout, - StoredDataFetcher retriever) { + private static Future> getFromCacheOrDelegate(SettingsCache cache, + String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher retriever) { // empty string account ID doesn't make sense final String normalizedAccountId = StringUtils.stripToNull(accountId); @@ -174,11 +171,11 @@ private static Future> getStoredDataFromCacheOrDelegate( final Map>> impCache = cache.getImpCache(); final Set missedRequestIds = new HashSet<>(); - final Map storedIdToRequest = getStoredDataFromCacheOrAddMissedIds( + final Map storedIdToRequest = getFromCacheOrAddMissedIds( normalizedAccountId, requestIds, requestCache, missedRequestIds); final Set missedImpIds = new HashSet<>(); - final Map storedIdToImp = getStoredDataFromCacheOrAddMissedIds( + final Map storedIdToImp = getFromCacheOrAddMissedIds( normalizedAccountId, impIds, impCache, missedImpIds); if (missedRequestIds.isEmpty() && missedImpIds.isEmpty()) { @@ -209,10 +206,10 @@ private static Future> getStoredDataFromCacheOrDelegate( }); } - private static Map getStoredDataFromCacheOrAddMissedIds(String accountId, - Set ids, - Map>> cache, - Set missedIds) { + private static Map getFromCacheOrAddMissedIds(String accountId, + Set ids, + Map>> cache, + Set missedIds) { final Map idToStoredItem = new HashMap<>(ids.size()); diff --git a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java index 40f14182717..623dda6747c 100644 --- a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java +++ b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java @@ -31,11 +31,7 @@ private StoredItemResolver() { *

* - Otherwise, reject stored item as if there hadn't been match. */ - public static StoredItem resolve(String type, - String accountId, - String id, - Set> storedItems) { - + public static StoredItem resolve(String type, String accountId, String id, Set> storedItems) { if (CollectionUtils.isEmpty(storedItems)) { throw new PreBidException("No stored %s found for id: %s".formatted(type, id)); } diff --git a/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java b/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java index fcfce2c47d3..73d710a072d 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java @@ -8,11 +8,6 @@ public class AccountProfilesConfig { Integer limit; - // TODO: need confirmation to move property - // TODO: Decide - // - skip all profiles on any invalid - // - skip only invalid - // TODO: metrics @JsonAlias("fail-on-unknown") Boolean failOnUnknown; } From ba7af8a3f6d855abff9ade85cd37497452f796dc Mon Sep 17 00:00:00 2001 From: Danylo Date: Wed, 30 Jul 2025 18:20:41 +0200 Subject: [PATCH 07/24] Add `getProfiles` implementation for `FileApplicationSettings`. --- .../settings/FileApplicationSettings.java | 122 ++++++++++++++---- .../spring/config/SettingsConfiguration.java | 12 +- 2 files changed, 108 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java index efb64ddd061..d28497a83a5 100644 --- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java @@ -12,17 +12,21 @@ import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.helper.StoredItemResolver; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.Category; import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.SettingsFile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; +import org.prebid.server.settings.model.StoredItem; import org.prebid.server.settings.model.StoredResponseDataResult; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -49,6 +53,7 @@ public class FileApplicationSettings implements ApplicationSettings { private final Map accounts; private final Map storedIdToRequest; private final Map storedIdToImp; + private final Map>> profileIdToProfile; private final Map storedIdToSeatBid; private final Map> fileToCategories; @@ -56,6 +61,7 @@ public FileApplicationSettings(FileSystem fileSystem, String settingsFileName, String storedRequestsDir, String storedImpsDir, + String profilesDir, String storedResponsesDir, String categoriesDir, JacksonMapper jacksonMapper) { @@ -71,6 +77,7 @@ public FileApplicationSettings(FileSystem fileSystem, storedIdToRequest = readStoredData(fileSystem, Objects.requireNonNull(storedRequestsDir)); storedIdToImp = readStoredData(fileSystem, Objects.requireNonNull(storedImpsDir)); + profileIdToProfile = readProfiles(fileSystem, Objects.requireNonNull(profilesDir), jacksonMapper); storedIdToSeatBid = readStoredData(fileSystem, Objects.requireNonNull(storedResponsesDir)); fileToCategories = readCategories(fileSystem, Objects.requireNonNull(categoriesDir), jacksonMapper); } @@ -98,6 +105,34 @@ private static Map readStoredData(FileSystem fileSystem, String filepath -> fileSystem.readFileBlocking(filepath).toString())); } + private static Map>> readProfiles(FileSystem fileSystem, + String dir, + JacksonMapper jacksonMapper) { + + return fileSystem.readDirBlocking(dir).stream() + .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) + .map(filepath -> readProfile(fileSystem, filepath, jacksonMapper)) + .collect(Collectors.groupingBy( + Map.Entry::getKey, + Collectors.mapping(Map.Entry::getValue, Collectors.toSet()))); + } + + private static Map.Entry> readProfile(FileSystem fileSystem, + String profileFilePath, + JacksonMapper jacksonMapper) { + + final String profileFileName = StringUtils.removeEnd(new File(profileFilePath).getName(), JSON_SUFFIX); + final String[] accountIdAndProfileId = profileFileName.split("-"); + if (accountIdAndProfileId.length != 2) { + throw new IllegalArgumentException("Invalid name of profile file: " + profileFileName); + } + + final String profileAsString = fileSystem.readFileBlocking(profileFilePath).toString(); + final Profile profile = jacksonMapper.decodeValue(profileAsString, Profile.class); + + return Map.entry(accountIdAndProfileId[1], StoredItem.of(accountIdAndProfileId[0], profile)); + } + private static Map> readCategories(FileSystem fileSystem, String dir, JacksonMapper jacksonMapper) { @@ -134,20 +169,22 @@ public Future> getStoredData(String accountId, Set impIds, Timeout timeout) { - return CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds) + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { + return Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())); + } - ? Future.succeededFuture(StoredDataResult.of( - Collections.emptyMap(), - Collections.emptyMap(), - Collections.emptyList())) + final Map storedRequests = existingStoredIdToJson(requestIds, storedIdToRequest); + final Map storedImps = existingStoredIdToJson(impIds, storedIdToImp); - : Future.succeededFuture(StoredDataResult.of( - existingStoredIdToJson(requestIds, storedIdToRequest), - existingStoredIdToJson(impIds, storedIdToImp), - Stream.of( - errorsForMissedIds(requestIds, storedIdToRequest, StoredDataType.request), - errorsForMissedIds(impIds, storedIdToImp, StoredDataType.imp)) - .flatMap(Function.identity()) + return Future.succeededFuture(StoredDataResult.of( + storedRequests, + storedImps, + Stream.concat( + errorsForMissedIds(requestIds, storedRequests.keySet(), StoredDataType.request.name()), + errorsForMissedIds(impIds, storedImps.keySet(), StoredDataType.imp.name())) .toList())); } @@ -175,19 +212,59 @@ public Future> getProfiles(String accountId, Set impIds, Timeout timeout) { - // TODO: implement - return Future.failedFuture("Not implemented"); + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { + return Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())); + } + + final List errors = new ArrayList<>(); + final Map requestProfiles = getProfiles(accountId, requestIds, Profile.Type.REQUEST, errors); + final Map impProfiles = getProfiles(accountId, requestIds, Profile.Type.IMP, errors); + + return Future.succeededFuture(StoredDataResult.of( + requestProfiles, + impProfiles, + Collections.unmodifiableList(errors))); + } + + private Map getProfiles(String accountId, + Set ids, + Profile.Type type, + List errors) { + + final Map result = new HashMap<>(); + + for (String id : ids) { + final Set> profiles = SetUtils.predicatedSet( + profileIdToProfile.getOrDefault(id, Collections.emptySet()), + storedItem -> storedItem.getData().getType() == type); + + try { + final StoredItem profile = StoredItemResolver + .resolve(type.toString(), accountId, id, profiles); + + result.put(id, profile.getData()); + } catch (PreBidException e) { + errors.add(e.getMessage()); + } + } + + return Collections.unmodifiableMap(result); } @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { - return CollectionUtils.isEmpty(responseIds) + if (CollectionUtils.isEmpty(responseIds)) { + return Future.succeededFuture(StoredResponseDataResult.of(Collections.emptyMap(), Collections.emptyList())); + } - ? Future.succeededFuture(StoredResponseDataResult.of(Collections.emptyMap(), Collections.emptyList())) + final Map storedResponses = existingStoredIdToJson(responseIds, storedIdToSeatBid); - : Future.succeededFuture(StoredResponseDataResult.of( - existingStoredIdToJson(responseIds, storedIdToSeatBid), - errorsForMissedIds(responseIds, storedIdToSeatBid, StoredDataType.seatbid).toList())); + return Future.succeededFuture(StoredResponseDataResult.of( + storedResponses, + errorsForMissedIds(responseIds, storedResponses.keySet(), StoredDataType.seatbid.name()).toList())); } @Override @@ -219,11 +296,8 @@ private static Map existingStoredIdToJson(Set requestedI .collect(Collectors.toMap(Function.identity(), storedIdToJson::get)); } - private static Stream errorsForMissedIds(Set ids, - Map storedIdToJson, - StoredDataType type) { - - return SetUtils.difference(ids, storedIdToJson.keySet()).stream() + private static Stream errorsForMissedIds(Set requestedIds, Set foundIds, String type) { + return SetUtils.difference(requestedIds, foundIds).stream() .map(id -> "No stored %s found for id: %s".formatted(type, id)); } } diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index ab89325e118..96e8862e218 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -77,13 +77,21 @@ FileApplicationSettings fileApplicationSettings( @Value("${settings.filesystem.settings-filename}") String settingsFileName, @Value("${settings.filesystem.stored-requests-dir}") String storedRequestsDir, @Value("${settings.filesystem.stored-imps-dir}") String storedImpsDir, + @Value("${settings.filesystem.profiles-dir}") String profilesDir, @Value("${settings.filesystem.stored-responses-dir}") String storedResponsesDir, @Value("${settings.filesystem.categories-dir}") String categoriesDir, FileSystem fileSystem, JacksonMapper jacksonMapper) { - return new FileApplicationSettings(fileSystem, settingsFileName, storedRequestsDir, storedImpsDir, - storedResponsesDir, categoriesDir, jacksonMapper); + return new FileApplicationSettings( + fileSystem, + settingsFileName, + storedRequestsDir, + storedImpsDir, + profilesDir, + storedResponsesDir, + categoriesDir, + jacksonMapper); } } From d5a4652fd8f30816f663204cfd3100a4d4bc24d6 Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 5 Aug 2025 14:17:40 +0200 Subject: [PATCH 08/24] Add missed parts --- .../externalortb/ProfilesProcessor.java | 10 ++- .../requestfactory/AmpRequestFactory.java | 9 +- .../requestfactory/AuctionRequestFactory.java | 6 +- .../requestfactory/Ortb2RequestFactory.java | 86 ++++++++++--------- .../settings/HttpApplicationSettings.java | 5 +- .../settings/S3ApplicationSettings.java | 4 +- .../spring/config/ServiceConfiguration.java | 26 ++++++ src/main/resources/application.yaml | 3 + 8 files changed, 101 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index 6b3cfe8151c..36a67c1d64f 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -171,7 +171,9 @@ private T applyProfiles(List profilesIds, ObjectNode result = mapper.mapper().valueToTree(original); for (String profileId : profilesIds) { final Profile profile = idToProfile.get(profileId); - result = mergeProfile(result, profile, profileId); + result = profile != null + ? mergeProfile(result, profile, profileId) + : result; } try { @@ -182,18 +184,18 @@ private T applyProfiles(List profilesIds, } private ObjectNode mergeProfile(ObjectNode original, Profile profile, String profileId) { - final ObjectNode profileBody = parseProfile(profile.getBody()); + final ObjectNode profileBody = parseProfile(profile.getBody(), profileId); return switch (profile.getMergePrecedence()) { case REQUEST -> merge(original, profileBody, profileId); case PROFILE -> merge(profileBody, original, profileId); }; } - private ObjectNode parseProfile(String body) { + private ObjectNode parseProfile(String body, String profileId) { try { return mapper.decodeValue(body, ObjectNode.class); } catch (DecodeException e) { - throw new InvalidProfileException("Can't parse profile: " + e.getMessage()); + throw new InvalidProfileException("Can't parse profile %s: %s".formatted(profileId, e.getMessage())); } } diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index c93c5496d16..3da8e5f4762 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -23,6 +23,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.OrtbTypesResolver; import org.prebid.server.auction.PriceGranularity; +import org.prebid.server.auction.externalortb.ProfilesProcessor; import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AmpGppService; import org.prebid.server.auction.model.AuctionContext; @@ -90,6 +91,7 @@ public class AmpRequestFactory { private final Ortb2RequestFactory ortb2RequestFactory; private final StoredRequestProcessor storedRequestProcessor; + private final ProfilesProcessor profilesProcessor; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final AmpGppService gppService; private final OrtbTypesResolver ortbTypesResolver; @@ -103,6 +105,7 @@ public class AmpRequestFactory { public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, AmpGppService gppService, OrtbTypesResolver ortbTypesResolver, @@ -116,6 +119,7 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.profilesProcessor = Objects.requireNonNull(profilesProcessor); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.gppService = Objects.requireNonNull(gppService); this.ortbTypesResolver = Objects.requireNonNull(ortbTypesResolver); @@ -401,12 +405,13 @@ private Future updateBidRequest(AuctionContext auctionContext) { new InvalidRequestException("AMP requests require the stored request id in AMP tag_id")); } - final Account account = auctionContext.getAccount(); - final String accountId = account != null ? account.getId() : null; + final Account account = ObjectUtils.defaultIfNull(auctionContext.getAccount(), Account.empty(null)); + final String accountId = account.getId(); final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); return storedRequestProcessor.processAmpRequest(accountId, storedRequestId, receivedBidRequest) + .compose(bidRequest -> profilesProcessor.process(account, bidRequest)) .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest)) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 3f419ff97db..cbf47dbf0e2 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -11,6 +11,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.OrtbTypesResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AuctionGppService; import org.prebid.server.auction.model.AuctionContext; @@ -40,6 +41,7 @@ public class AuctionRequestFactory { private final long maxRequestSize; private final Ortb2RequestFactory ortb2RequestFactory; private final StoredRequestProcessor storedRequestProcessor; + private final ProfilesProcessor profilesProcessor; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final AuctionGppService gppService; private final CookieDeprecationService cookieDeprecationService; @@ -58,6 +60,7 @@ public class AuctionRequestFactory { public AuctionRequestFactory(long maxRequestSize, Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, AuctionGppService gppService, CookieDeprecationService cookieDeprecationService, @@ -74,6 +77,7 @@ public AuctionRequestFactory(long maxRequestSize, this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.profilesProcessor = Objects.requireNonNull(profilesProcessor); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.gppService = Objects.requireNonNull(gppService); this.cookieDeprecationService = Objects.requireNonNull(cookieDeprecationService); @@ -252,7 +256,7 @@ private Future updateBidRequest(AuctionStoredResult auctionStoredRes final boolean hasStoredBidRequest = auctionStoredResult.hasStoredBidRequest(); - return Future.succeededFuture(auctionStoredResult.bidRequest()) + return profilesProcessor.process(auctionContext.getAccount(), auctionStoredResult.bidRequest()) .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, hasStoredBidRequest)) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 9a674df3479..7e4719ed44c 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -24,8 +24,10 @@ import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.debug.DebugContext; @@ -101,6 +103,7 @@ public class Ortb2RequestFactory { private final TimeoutResolver timeoutResolver; private final TimeoutFactory timeoutFactory; private final StoredRequestProcessor storedRequestProcessor; + private final ProfilesProcessor profilesProcessor; private final ApplicationSettings applicationSettings; private final IpAddressHelper ipAddressHelper; private final HookStageExecutor hookStageExecutor; @@ -116,6 +119,7 @@ public Ortb2RequestFactory(int timeoutAdjustmentFactor, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, ApplicationSettings applicationSettings, IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, @@ -135,6 +139,7 @@ public Ortb2RequestFactory(int timeoutAdjustmentFactor, this.timeoutResolver = Objects.requireNonNull(timeoutResolver); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.profilesProcessor = Objects.requireNonNull(profilesProcessor); this.applicationSettings = Objects.requireNonNull(applicationSettings); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); @@ -470,11 +475,48 @@ private Timeout timeout(BidRequest bidRequest, long startTime) { private Future findAccountIdFrom(BidRequest bidRequest, boolean isLookupStoredRequest) { final String accountId = accountIdFrom(bidRequest); - return StringUtils.isNotBlank(accountId) || !isLookupStoredRequest - ? Future.succeededFuture(accountId) - : storedRequestProcessor.processAuctionRequest(accountId, bidRequest) - // TODO: add profiles? - .map(storedAuctionResult -> accountIdFrom(storedAuctionResult.bidRequest())); + if (StringUtils.isNotBlank(accountId) || !isLookupStoredRequest) { + return Future.succeededFuture(accountId); + } + + return accountIdFromStored(bidRequest) + .compose(id -> StringUtils.isBlank(accountId) + ? accountIdFromProfiles(bidRequest) + : Future.succeededFuture(id)); + } + + private String accountIdFrom(BidRequest bidRequest) { + final App app = bidRequest.getApp(); + final Publisher appPublisher = app != null ? app.getPublisher() : null; + final Site site = bidRequest.getSite(); + final Publisher sitePublisher = site != null ? site.getPublisher() : null; + final Dooh dooh = bidRequest.getDooh(); + final Publisher doohPublisher = dooh != null ? dooh.getPublisher() : null; + + final Publisher publisher = ObjectUtils.firstNonNull(appPublisher, doohPublisher, sitePublisher); + final String publisherId = publisher != null ? resolvePublisherId(publisher) : null; + return ObjectUtils.defaultIfNull(publisherId, StringUtils.EMPTY); + } + + private String resolvePublisherId(Publisher publisher) { + final String parentAccountId = parentAccountIdFromExtPublisher(publisher.getExt()); + return ObjectUtils.defaultIfNull(parentAccountId, publisher.getId()); + } + + private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { + final ExtPublisherPrebid extPublisherPrebid = extPublisher != null ? extPublisher.getPrebid() : null; + return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; + } + + private Future accountIdFromStored(BidRequest bidRequest) { + return storedRequestProcessor.processAuctionRequest(null, bidRequest) + .map(AuctionStoredResult::bidRequest) + .map(this::accountIdFrom); + } + + private Future accountIdFromProfiles(BidRequest bidRequest) { + return profilesProcessor.process(Account.empty(null), bidRequest) + .map(this::accountIdFrom); } private String validateIfAccountBlocklisted(String accountId) { @@ -509,40 +551,6 @@ private Future ensureAccountActive(Account account) { : Future.succeededFuture(account); } - /** - * Extracts publisher id either from {@link BidRequest}.app.publisher or {@link BidRequest}.site.publisher. - * If neither is present returns empty string. - */ - private String accountIdFrom(BidRequest bidRequest) { - final App app = bidRequest.getApp(); - final Publisher appPublisher = app != null ? app.getPublisher() : null; - final Site site = bidRequest.getSite(); - final Publisher sitePublisher = site != null ? site.getPublisher() : null; - final Dooh dooh = bidRequest.getDooh(); - final Publisher doohPublisher = dooh != null ? dooh.getPublisher() : null; - - final Publisher publisher = ObjectUtils.firstNonNull(appPublisher, doohPublisher, sitePublisher); - final String publisherId = publisher != null ? resolvePublisherId(publisher) : null; - return ObjectUtils.defaultIfNull(publisherId, StringUtils.EMPTY); - } - - /** - * Resolves what value should be used as a publisher id - either taken from publisher.ext.parentAccount - * or publisher.id in this respective priority. - */ - private String resolvePublisherId(Publisher publisher) { - final String parentAccountId = parentAccountIdFromExtPublisher(publisher.getExt()); - return ObjectUtils.defaultIfNull(parentAccountId, publisher.getId()); - } - - /** - * Parses publisher.ext and returns parentAccount value from it. Returns null if any parsing error occurs. - */ - private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { - final ExtPublisherPrebid extPublisherPrebid = extPublisher != null ? extPublisher.getPrebid() : null; - return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; - } - private Future wrapFailure(Throwable exception, String accountId, HttpRequestContext httpRequest) { if (exception instanceof UnauthorizedAccountException) { return Future.failedFuture(exception); diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index bd14b5acaa9..af39b9d206d 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -38,6 +38,7 @@ import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Implementation of {@link ApplicationSettings}. @@ -331,7 +332,9 @@ public Future> getProfiles(String accountId, return Future.succeededFuture(StoredDataResult.of( Collections.emptyMap(), Collections.emptyMap(), - Collections.emptyList())); + Stream.concat(requestIds.stream(), impIds.stream()) + .map(id -> "Profile not found for id: " + id) + .toList())); } @Override diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java index 35d444ffaf3..c9b71c6e7ad 100644 --- a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java @@ -160,7 +160,9 @@ public Future> getProfiles(String accountId, return Future.succeededFuture(StoredDataResult.of( Collections.emptyMap(), Collections.emptyMap(), - Collections.emptyList())); + Stream.concat(requestIds.stream(), impIds.stream()) + .map(id -> "Profile not found for id: " + id) + .toList())); } @Override diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index e2841fd36a9..d32d83fd6f6 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -36,6 +36,7 @@ import org.prebid.server.auction.categorymapping.BasicCategoryMappingService; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.categorymapping.NoOpCategoryMappingService; +import org.prebid.server.auction.externalortb.ProfilesProcessor; import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.gpp.AmpGppService; @@ -433,6 +434,7 @@ Ortb2RequestFactory openRtb2RequestFactory( TimeoutResolver auctionTimeoutResolver, TimeoutFactory timeoutFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, ApplicationSettings applicationSettings, IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, @@ -451,6 +453,7 @@ Ortb2RequestFactory openRtb2RequestFactory( auctionTimeoutResolver, timeoutFactory, storedRequestProcessor, + profilesProcessor, applicationSettings, ipAddressHelper, hookStageExecutor, @@ -463,6 +466,7 @@ AuctionRequestFactory auctionRequestFactory( @Value("${auction.max-request-size}") @Min(0) int maxRequestSize, Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, AuctionGppService auctionGppService, CookieDeprecationService cookieDeprecationService, @@ -479,6 +483,7 @@ AuctionRequestFactory auctionRequestFactory( maxRequestSize, ortb2RequestFactory, storedRequestProcessor, + profilesProcessor, bidRequestOrtbVersionConversionManager, auctionGppService, cookieDeprecationService, @@ -513,6 +518,7 @@ IdGenerator sourceIdGenerator() { @Bean AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, AmpGppService ampGppService, OrtbTypesResolver ortbTypesResolver, @@ -527,6 +533,7 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, return new AmpRequestFactory( ortb2RequestFactory, storedRequestProcessor, + profilesProcessor, bidRequestOrtbVersionConversionManager, ampGppService, ortbTypesResolver, @@ -987,6 +994,25 @@ StoredRequestProcessor storedRequestProcessor( jsonMerger); } + @Bean + ProfilesProcessor profilesProcessor(@Value("${auction.profiles.limit}") int maxProfiles, + @Value("${auction.profiles.timeout-ms}") long defaultTimeoutMillis, + ApplicationSettings applicationSettings, + TimeoutFactory timeoutFactory, + Metrics metrics, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + return new ProfilesProcessor( + maxProfiles, + defaultTimeoutMillis, + applicationSettings, + timeoutFactory, + metrics, + mapper, + jsonMerger); + } + @Bean WinningBidComparatorFactory winningBidComparatorFactory() { return new WinningBidComparatorFactory(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f52895024d2..55822047954 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -111,6 +111,9 @@ auction: percent: 100 tmax-upstream-response-time: 30 stored-requests-timeout-ms: 100 + profiles: + limit: 4 + timeout-ms: 100 timeout-notification: timeout-ms: 200 log-result: false From bbb9e2738e1a0ebaeba972b717290a3fffb982d8 Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 5 Aug 2025 19:30:17 +0200 Subject: [PATCH 09/24] Add metrics --- .../externalortb/ProfilesProcessor.java | 96 ++++++++++++++----- .../requestfactory/AmpRequestFactory.java | 6 +- .../requestfactory/AuctionRequestFactory.java | 2 +- .../requestfactory/Ortb2RequestFactory.java | 13 ++- .../prebid/server/metric/AccountMetrics.java | 6 ++ .../org/prebid/server/metric/Metrics.java | 10 ++ .../prebid/server/metric/ProfileMetrics.java | 25 +++++ .../spring/config/ServiceConfiguration.java | 4 + 8 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 src/main/java/org/prebid/server/metric/ProfileMetrics.java diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index 36a67c1d64f..93ad977e7bf 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -6,6 +6,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import io.vertx.core.Future; +import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.exception.InvalidProfileException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.timeout.Timeout; @@ -13,6 +14,9 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.ConditionalLogger; +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.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -37,8 +41,13 @@ public class ProfilesProcessor { + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(ProfilesProcessor.class)); + private final int maxProfiles; private final long defaultTimeoutMillis; + private final boolean failOnUnknown; + private final double logSamplingRate; private final ApplicationSettings applicationSettings; private final TimeoutFactory timeoutFactory; private final Metrics metrics; @@ -47,6 +56,8 @@ public class ProfilesProcessor { public ProfilesProcessor(int maxProfiles, long defaultTimeoutMillis, + boolean failOnUnknown, + double logSamplingRate, ApplicationSettings applicationSettings, TimeoutFactory timeoutFactory, Metrics metrics, @@ -55,6 +66,8 @@ public ProfilesProcessor(int maxProfiles, this.maxProfiles = maxProfiles; this.defaultTimeoutMillis = defaultTimeoutMillis; + this.failOnUnknown = failOnUnknown; + this.logSamplingRate = logSamplingRate; this.applicationSettings = Objects.requireNonNull(applicationSettings); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.metrics = Objects.requireNonNull(metrics); @@ -62,32 +75,46 @@ public ProfilesProcessor(int maxProfiles, this.jsonMerger = Objects.requireNonNull(jsonMerger); } - public Future process(Account account, BidRequest bidRequest) { - final List imps = bidRequest.getImp(); - - final AllProfilesIds profilesIds = truncate( - new AllProfilesIds( - requestProfilesIds(bidRequest), - imps.stream() - .map(this::impProfilesIds) - .toList()), - Optional.ofNullable(account.getAuction()) - .map(AccountAuctionConfig::getProfiles) - .map(AccountProfilesConfig::getLimit) - .orElse(maxProfiles)); - + public Future process(AuctionContext auctionContext, BidRequest bidRequest) { + final AllProfilesIds profilesIds = profilesIds(bidRequest, auctionContext); if (profilesIds.isEmpty()) { return Future.succeededFuture(bidRequest); } - return fetchProfiles(account.getId(), profilesIds, timeoutMillis(bidRequest)) + final String accountId = Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getId) + .orElse(null); + + return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest)) + .map(profiles -> emitMetrics(accountId, profiles, auctionContext)) .map(profiles -> mergeResults( applyRequestProfiles(profilesIds.request(), profiles.getStoredIdToRequest(), bidRequest), - applyImpsProfiles(profilesIds.imps(), profiles.getStoredIdToImp(), imps))) + applyImpsProfiles(profilesIds.imps(), profiles.getStoredIdToImp(), bidRequest.getImp()))) .recover(e -> Future.failedFuture( new InvalidRequestException("Error during processing profiles: " + e.getMessage()))); } + private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auctionContext) { + final AllProfilesIds initialProfilesIds = new AllProfilesIds( + requestProfilesIds(bidRequest), + bidRequest.getImp().stream().map(this::impProfilesIds).toList()); + + final AllProfilesIds profilesIds = truncate( + initialProfilesIds, + Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getProfiles) + .map(AccountProfilesConfig::getLimit) + .orElse(maxProfiles)); + + if (auctionContext.getDebugContext().isDebugEnabled() && !profilesIds.equals(initialProfilesIds)) { + auctionContext.getDebugWarnings().add("Profiles exceeded the limit."); + metrics.updateProfileMetric(MetricName.err); // TODO + } + + return profilesIds; + } + private static List requestProfilesIds(BidRequest bidRequest) { return Optional.ofNullable(bidRequest) .map(BidRequest::getExt) @@ -108,7 +135,7 @@ private ExtImpPrebid parseImpExt(JsonNode jsonNode) { try { return mapper.mapper().treeToValue(jsonNode, ExtImpPrebid.class); } catch (JsonProcessingException e) { - throw new InvalidProfileException(e.getMessage()); + throw new InvalidRequestException(e.getMessage()); } } @@ -145,11 +172,27 @@ private Future> fetchProfiles(String accountId, final Timeout timeout = timeoutFactory.create(timeoutMillis); return applicationSettings.getProfiles(accountId, requestProfilesIds, impProfilesIds, timeout) - .compose(profiles -> profiles.getErrors().isEmpty() + .compose(profiles -> profiles.getErrors().isEmpty() || !failOnUnknown ? Future.succeededFuture(profiles) : Future.failedFuture(new InvalidProfileException(profiles.getErrors()))); } + private StoredDataResult emitMetrics(String accountId, + StoredDataResult fetchResult, + AuctionContext auctionContext) { + + if (!fetchResult.getErrors().isEmpty()) { + metrics.updateProfileMetric(MetricName.missing); + + if (auctionContext.getDebugContext().isDebugEnabled()) { + metrics.updateAccountProfileMetric(accountId, MetricName.missing); + auctionContext.getDebugWarnings().addAll(fetchResult.getErrors()); + } + } + + return fetchResult; + } + private BidRequest applyRequestProfiles(List profilesIds, Map idToRequestProfile, BidRequest bidRequest) { @@ -170,10 +213,19 @@ private T applyProfiles(List profilesIds, ObjectNode result = mapper.mapper().valueToTree(original); for (String profileId : profilesIds) { - final Profile profile = idToProfile.get(profileId); - result = profile != null - ? mergeProfile(result, profile, profileId) - : result; + try { + final Profile profile = idToProfile.get(profileId); + result = profile != null + ? mergeProfile(result, profile, profileId) + : result; + } catch (InvalidProfileException e) { + metrics.updateProfileMetric(MetricName.invalid); + conditionalLogger.error(e.getMessage(), logSamplingRate); + + if (failOnUnknown) { + throw new InvalidProfileException(e.getMessage()); + } + } } try { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index 3da8e5f4762..e1c5e4240ce 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -405,13 +405,13 @@ private Future updateBidRequest(AuctionContext auctionContext) { new InvalidRequestException("AMP requests require the stored request id in AMP tag_id")); } - final Account account = ObjectUtils.defaultIfNull(auctionContext.getAccount(), Account.empty(null)); - final String accountId = account.getId(); + final Account account = auctionContext.getAccount(); + final String accountId = account != null ? account.getId() : null; final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); return storedRequestProcessor.processAmpRequest(accountId, storedRequestId, receivedBidRequest) - .compose(bidRequest -> profilesProcessor.process(account, bidRequest)) + .compose(bidRequest -> profilesProcessor.process(auctionContext, bidRequest)) .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest)) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index cbf47dbf0e2..d873af3e696 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -256,7 +256,7 @@ private Future updateBidRequest(AuctionStoredResult auctionStoredRes final boolean hasStoredBidRequest = auctionStoredResult.hasStoredBidRequest(); - return profilesProcessor.process(auctionContext.getAccount(), auctionStoredResult.bidRequest()) + return profilesProcessor.process(auctionContext, auctionStoredResult.bidRequest()) .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, hasStoredBidRequest)) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 7e4719ed44c..1f65958e57d 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -186,7 +186,7 @@ private Future fetchAccount(AuctionContext auctionContext, boolean isLo final Timeout timeout = auctionContext.getTimeoutContext().getTimeout(); final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); - return findAccountIdFrom(bidRequest, isLookupStoredRequest) + return findAccountIdFrom(auctionContext, bidRequest, isLookupStoredRequest) .map(this::validateIfAccountBlocklisted) .compose(accountId -> loadAccount(timeout, httpRequest, accountId)); } @@ -473,7 +473,10 @@ private Timeout timeout(BidRequest bidRequest, long startTime) { return timeoutFactory.create(startTime, timeout); } - private Future findAccountIdFrom(BidRequest bidRequest, boolean isLookupStoredRequest) { + private Future findAccountIdFrom(AuctionContext auctionContext, + BidRequest bidRequest, + boolean isLookupStoredRequest) { + final String accountId = accountIdFrom(bidRequest); if (StringUtils.isNotBlank(accountId) || !isLookupStoredRequest) { return Future.succeededFuture(accountId); @@ -481,7 +484,7 @@ private Future findAccountIdFrom(BidRequest bidRequest, boolean isLookup return accountIdFromStored(bidRequest) .compose(id -> StringUtils.isBlank(accountId) - ? accountIdFromProfiles(bidRequest) + ? accountIdFromProfiles(auctionContext, bidRequest) : Future.succeededFuture(id)); } @@ -514,8 +517,8 @@ private Future accountIdFromStored(BidRequest bidRequest) { .map(this::accountIdFrom); } - private Future accountIdFromProfiles(BidRequest bidRequest) { - return profilesProcessor.process(Account.empty(null), bidRequest) + private Future accountIdFromProfiles(AuctionContext auctionContext, BidRequest bidRequest) { + return profilesProcessor.process(auctionContext, bidRequest) .map(this::accountIdFrom); } diff --git a/src/main/java/org/prebid/server/metric/AccountMetrics.java b/src/main/java/org/prebid/server/metric/AccountMetrics.java index f2cdb29f328..6bd479b7ea3 100644 --- a/src/main/java/org/prebid/server/metric/AccountMetrics.java +++ b/src/main/java/org/prebid/server/metric/AccountMetrics.java @@ -23,6 +23,7 @@ class AccountMetrics extends UpdatableMetrics { private final ResponseMetrics responseMetrics; private final HooksMetrics hooksMetrics; private final ActivitiesMetrics activitiesMetrics; + private final ProfileMetrics profileMetrics; AccountMetrics(MetricRegistry metricRegistry, CounterType counterType, String account) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), @@ -36,6 +37,7 @@ class AccountMetrics extends UpdatableMetrics { responseMetrics = new ResponseMetrics(metricRegistry, counterType, createPrefix(account)); hooksMetrics = new HooksMetrics(metricRegistry, counterType, createPrefix(account)); activitiesMetrics = new ActivitiesMetrics(metricRegistry, counterType, createPrefix(account)); + profileMetrics = new ProfileMetrics(metricRegistry, counterType, createPrefix(account)); } private static String createPrefix(String account) { @@ -73,4 +75,8 @@ HooksMetrics hooks() { ActivitiesMetrics activities() { return activitiesMetrics; } + + ProfileMetrics profiles() { + return profileMetrics; + } } diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 9010a7018c5..c10d3301391 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -59,6 +59,7 @@ public class Metrics extends UpdatableMetrics { private final CurrencyRatesMetrics currencyRatesMetrics; private final Map settingsCacheMetrics; private final HooksMetrics hooksMetrics; + private final ProfileMetrics profileMetrics; public Metrics(MetricRegistry metricRegistry, CounterType counterType, @@ -97,6 +98,7 @@ public Metrics(MetricRegistry metricRegistry, currencyRatesMetrics = new CurrencyRatesMetrics(metricRegistry, counterType); settingsCacheMetrics = new HashMap<>(); hooksMetrics = new HooksMetrics(metricRegistry, counterType); + profileMetrics = new ProfileMetrics(metricRegistry, counterType); } RequestsMetrics requests() { @@ -727,6 +729,14 @@ public void updateAccountActivityProcessedRulesCount(String account) { forAccount(account).activities().incCounter(MetricName.processed_rules_count); } + public void updateProfileMetric(MetricName metricName) { + profileMetrics.incCounter(metricName); + } + + public void updateAccountProfileMetric(String account, MetricName metricName) { + forAccount(account).profiles().incCounter(metricName); + } + private static class HookMetricMapper { private static final EnumMap STATUS_TO_METRIC = diff --git a/src/main/java/org/prebid/server/metric/ProfileMetrics.java b/src/main/java/org/prebid/server/metric/ProfileMetrics.java new file mode 100644 index 00000000000..88e8b24db82 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/ProfileMetrics.java @@ -0,0 +1,25 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; +import java.util.function.Function; + +class ProfileMetrics extends UpdatableMetrics { + + ProfileMetrics(MetricRegistry metricRegistry, CounterType counterType) { + this(metricRegistry, counterType, StringUtils.EMPTY); + } + + ProfileMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix) + ".")); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%sprofile.%s".formatted(prefix, metricName); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index d32d83fd6f6..9021beab946 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -997,6 +997,8 @@ StoredRequestProcessor storedRequestProcessor( @Bean ProfilesProcessor profilesProcessor(@Value("${auction.profiles.limit}") int maxProfiles, @Value("${auction.profiles.timeout-ms}") long defaultTimeoutMillis, + @Value("${auction.profiles.fail-on-unknown:true}") boolean failOnUnknown, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate, ApplicationSettings applicationSettings, TimeoutFactory timeoutFactory, Metrics metrics, @@ -1006,6 +1008,8 @@ ProfilesProcessor profilesProcessor(@Value("${auction.profiles.limit}") int maxP return new ProfilesProcessor( maxProfiles, defaultTimeoutMillis, + failOnUnknown, + logSamplingRate, applicationSettings, timeoutFactory, metrics, From fc4d6a9a5dcab6d440d0180d70d6108f2f9a672a Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 8 Aug 2025 16:07:54 +0200 Subject: [PATCH 10/24] Add metrics --- .../externalortb/ProfilesProcessor.java | 26 ++++++++++--------- .../org/prebid/server/metric/MetricName.java | 5 +++- .../prebid/server/metric/ProfileMetrics.java | 11 +++++--- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index 93ad977e7bf..b88321f4cf6 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -86,7 +86,7 @@ public Future process(AuctionContext auctionContext, BidRequest bidR .orElse(null); return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest)) - .map(profiles -> emitMetrics(accountId, profiles, auctionContext)) + .compose(profiles -> emitMetrics(accountId, profiles, auctionContext)) .map(profiles -> mergeResults( applyRequestProfiles(profilesIds.request(), profiles.getStoredIdToRequest(), bidRequest), applyImpsProfiles(profilesIds.imps(), profiles.getStoredIdToImp(), bidRequest.getImp()))) @@ -109,7 +109,7 @@ private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auction if (auctionContext.getDebugContext().isDebugEnabled() && !profilesIds.equals(initialProfilesIds)) { auctionContext.getDebugWarnings().add("Profiles exceeded the limit."); - metrics.updateProfileMetric(MetricName.err); // TODO + metrics.updateProfileMetric(MetricName.limit_exceeded); } return profilesIds; @@ -171,26 +171,28 @@ private Future> fetchProfiles(String accountId, .collect(Collectors.toSet()); final Timeout timeout = timeoutFactory.create(timeoutMillis); - return applicationSettings.getProfiles(accountId, requestProfilesIds, impProfilesIds, timeout) - .compose(profiles -> profiles.getErrors().isEmpty() || !failOnUnknown - ? Future.succeededFuture(profiles) - : Future.failedFuture(new InvalidProfileException(profiles.getErrors()))); + return applicationSettings.getProfiles(accountId, requestProfilesIds, impProfilesIds, timeout); } - private StoredDataResult emitMetrics(String accountId, - StoredDataResult fetchResult, - AuctionContext auctionContext) { + private Future> emitMetrics(String accountId, + StoredDataResult fetchResult, + AuctionContext auctionContext) { - if (!fetchResult.getErrors().isEmpty()) { + final List errors = fetchResult.getErrors(); + if (!errors.isEmpty()) { metrics.updateProfileMetric(MetricName.missing); if (auctionContext.getDebugContext().isDebugEnabled()) { metrics.updateAccountProfileMetric(accountId, MetricName.missing); - auctionContext.getDebugWarnings().addAll(fetchResult.getErrors()); + auctionContext.getDebugWarnings().addAll(errors); + } + + if (failOnUnknown) { + return Future.failedFuture(new InvalidProfileException(errors)); } } - return fetchResult; + return Future.succeededFuture(fetchResult); } private BidRequest applyRequestProfiles(List profilesIds, diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index ab32c446226..4d562aa4122 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -156,7 +156,10 @@ public enum MetricName { // activity disallowed_count("disallowed.count"), - processed_rules_count("processedrules.count"); + processed_rules_count("processedrules.count"), + + // profiles + limit_exceeded; private final String name; diff --git a/src/main/java/org/prebid/server/metric/ProfileMetrics.java b/src/main/java/org/prebid/server/metric/ProfileMetrics.java index 88e8b24db82..3601938b1b1 100644 --- a/src/main/java/org/prebid/server/metric/ProfileMetrics.java +++ b/src/main/java/org/prebid/server/metric/ProfileMetrics.java @@ -1,7 +1,6 @@ package org.prebid.server.metric; import com.codahale.metrics.MetricRegistry; -import org.apache.commons.lang3.StringUtils; import java.util.Objects; import java.util.function.Function; @@ -9,17 +8,21 @@ class ProfileMetrics extends UpdatableMetrics { ProfileMetrics(MetricRegistry metricRegistry, CounterType counterType) { - this(metricRegistry, counterType, StringUtils.EMPTY); + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), nameCreator()); } ProfileMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { super( Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), - nameCreator(Objects.requireNonNull(prefix) + ".")); + nameCreator(Objects.requireNonNull(prefix))); + } + + private static Function nameCreator() { + return "profiles.%s"::formatted; } private static Function nameCreator(String prefix) { - return metricName -> "%sprofile.%s".formatted(prefix, metricName); + return metricName -> "%s.profiles.%s".formatted(prefix, metricName); } } From d2448c7e0c8d0ba9059f8caf18f8adb37ba132b4 Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 8 Aug 2025 16:52:55 +0200 Subject: [PATCH 11/24] Refactor --- .../auction/requestfactory/Ortb2RequestFactory.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 1f65958e57d..63223799fdc 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -477,7 +477,7 @@ private Future findAccountIdFrom(AuctionContext auctionContext, BidRequest bidRequest, boolean isLookupStoredRequest) { - final String accountId = accountIdFrom(bidRequest); + final String accountId = accountIdFromBidRequest(bidRequest); if (StringUtils.isNotBlank(accountId) || !isLookupStoredRequest) { return Future.succeededFuture(accountId); } @@ -488,7 +488,7 @@ private Future findAccountIdFrom(AuctionContext auctionContext, : Future.succeededFuture(id)); } - private String accountIdFrom(BidRequest bidRequest) { + private String accountIdFromBidRequest(BidRequest bidRequest) { final App app = bidRequest.getApp(); final Publisher appPublisher = app != null ? app.getPublisher() : null; final Site site = bidRequest.getSite(); @@ -514,12 +514,12 @@ private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { private Future accountIdFromStored(BidRequest bidRequest) { return storedRequestProcessor.processAuctionRequest(null, bidRequest) .map(AuctionStoredResult::bidRequest) - .map(this::accountIdFrom); + .map(this::accountIdFromBidRequest); } private Future accountIdFromProfiles(AuctionContext auctionContext, BidRequest bidRequest) { return profilesProcessor.process(auctionContext, bidRequest) - .map(this::accountIdFrom); + .map(this::accountIdFromBidRequest); } private String validateIfAccountBlocklisted(String accountId) { From 5717e39a6092d4ed7702692e696f78aa87817a65 Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 8 Aug 2025 20:18:54 +0200 Subject: [PATCH 12/24] Fix units --- sample/configs/prebid-config.yaml | 1 + .../externalortb/ProfilesProcessor.java | 3 +- .../requestfactory/Ortb2RequestFactory.java | 4 +- .../settings/DatabaseApplicationSettings.java | 2 +- .../settings/HttpApplicationSettings.java | 2 +- .../requestfactory/AmpRequestFactoryTest.java | 17 +- .../AuctionRequestFactoryTest.java | 8 + .../Ortb2RequestFactoryTest.java | 7 + .../SettingsCacheNotificationHandlerTest.java | 4 +- .../CachingApplicationSettingsTest.java | 15 +- .../CompositeApplicationSettingsTest.java | 63 ++++-- .../DatabaseApplicationSettingsTest.java | 53 +++-- .../settings/FileApplicationSettingsTest.java | 198 ++++++++++++++---- .../settings/HttpApplicationSettingsTest.java | 89 +++++--- .../settings/S3ApplicationSettingsTest.java | 13 +- .../helper/StoredItemResolverTest.java | 47 +++-- .../server/it/test-application.properties | 1 + 17 files changed, 372 insertions(+), 155 deletions(-) diff --git a/sample/configs/prebid-config.yaml b/sample/configs/prebid-config.yaml index 93a53b7952b..0d8b5c90ff2 100644 --- a/sample/configs/prebid-config.yaml +++ b/sample/configs/prebid-config.yaml @@ -24,6 +24,7 @@ settings: settings-filename: sample/configs/sample-app-settings.yaml stored-requests-dir: sample stored-imps-dir: sample + profiles-dir: sample stored-responses-dir: sample categories-dir: gdpr: diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index b88321f4cf6..cf017196781 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -6,6 +6,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.exception.InvalidProfileException; import org.prebid.server.exception.InvalidRequestException; @@ -83,7 +84,7 @@ public Future process(AuctionContext auctionContext, BidRequest bidR final String accountId = Optional.ofNullable(auctionContext.getAccount()) .map(Account::getId) - .orElse(null); + .orElse(StringUtils.EMPTY); return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest)) .compose(profiles -> emitMetrics(accountId, profiles, auctionContext)) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 63223799fdc..d66aabcc8e3 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -483,7 +483,7 @@ private Future findAccountIdFrom(AuctionContext auctionContext, } return accountIdFromStored(bidRequest) - .compose(id -> StringUtils.isBlank(accountId) + .compose(id -> StringUtils.isBlank(id) ? accountIdFromProfiles(auctionContext, bidRequest) : Future.succeededFuture(id)); } @@ -512,7 +512,7 @@ private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { } private Future accountIdFromStored(BidRequest bidRequest) { - return storedRequestProcessor.processAuctionRequest(null, bidRequest) + return storedRequestProcessor.processAuctionRequest(StringUtils.EMPTY, bidRequest) .map(AuctionStoredResult::bidRequest) .map(this::accountIdFromBidRequest); } diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java index 0fa02b99751..371edfcb0e3 100644 --- a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -160,7 +160,7 @@ public Future> getAmpStoredData(String accountId, return fetchStoredData( selectAmpStoredRequestsQuery, requestIds, - impIds, + Collections.emptySet(), result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds), timeout); } diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index af39b9d206d..c0d899508c4 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -176,7 +176,7 @@ public Future> getAmpStoredData(String accountId, Set impIds, Timeout timeout) { - return fetchStoredData(ampEndpoint, requestIds, impIds, timeout); + return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout); } @Override diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index aea0f8cb8c7..90ef1467b04 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -30,6 +30,7 @@ import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.OrtbTypesResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AmpGppService; import org.prebid.server.auction.model.AuctionContext; @@ -72,12 +73,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.function.UnaryOperator; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; -import static java.util.function.Function.identity; +import static java.util.function.UnaryOperator.identity; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -103,6 +104,8 @@ public class AmpRequestFactoryTest extends VertxTest { @Mock(strictness = LENIENT) private StoredRequestProcessor storedRequestProcessor; @Mock(strictness = LENIENT) + private ProfilesProcessor profilesProcessor; + @Mock(strictness = LENIENT) private BidRequestOrtbVersionConversionManager ortbVersionConversionManager; @Mock(strictness = LENIENT) private AmpGppService ampGppService; @@ -139,6 +142,9 @@ public void setUp() { given(ortbVersionConversionManager.convertToAuctionSupportedVersion(any())) .willAnswer(invocation -> invocation.getArgument(0)); + given(profilesProcessor.process(any(), any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(1))); + given(ampGppService.contextFrom(any())).willReturn(Future.succeededFuture()); given(ampGppService.updateBidRequest(any(), any())) .willAnswer(invocation -> invocation.getArgument(0)); @@ -191,6 +197,7 @@ public void setUp() { target = new AmpRequestFactory( ortb2RequestFactory, storedRequestProcessor, + profilesProcessor, ortbVersionConversionManager, ampGppService, ortbTypesResolver, @@ -1736,9 +1743,9 @@ public void shouldUpdateTimeout() { .isEqualTo(10000L); } - private void givenBidRequest( - Function storedBidRequestBuilderCustomizer, - Imp... imps) { + private void givenBidRequest(UnaryOperator storedBidRequestBuilderCustomizer, + Imp... imps) { + final List impList = imps.length > 0 ? asList(imps) : null; given(storedRequestProcessor.processAmpRequest(any(), anyString(), any())) diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index f043a9b3f73..2e8579fecec 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -30,6 +30,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.OrtbTypesResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AuctionGppService; import org.prebid.server.auction.model.AuctionContext; @@ -89,6 +90,8 @@ public class AuctionRequestFactoryTest extends VertxTest { @Mock(strictness = LENIENT) private StoredRequestProcessor storedRequestProcessor; @Mock(strictness = LENIENT) + private ProfilesProcessor profilesProcessor; + @Mock(strictness = LENIENT) private BidRequestOrtbVersionConversionManager ortbVersionConversionManager; @Mock(strictness = LENIENT) private AuctionGppService auctionGppService; @@ -146,6 +149,9 @@ public void setUp() { .debugContext(DebugContext.of(true, true, null)) .build(); + given(profilesProcessor.process(any(), any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(1))); + given(ortbVersionConversionManager.convertToAuctionSupportedVersion(any())) .willAnswer(invocation -> invocation.getArgument(0)); @@ -207,6 +213,7 @@ public void setUp() { Integer.MAX_VALUE, ortb2RequestFactory, storedRequestProcessor, + profilesProcessor, ortbVersionConversionManager, auctionGppService, cookieDeprecationService, @@ -243,6 +250,7 @@ public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() { 1, ortb2RequestFactory, storedRequestProcessor, + profilesProcessor, ortbVersionConversionManager, auctionGppService, cookieDeprecationService, diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index 035c189afa7..649216852f7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -28,6 +28,7 @@ import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionStoredResult; @@ -123,6 +124,8 @@ public class Ortb2RequestFactoryTest extends VertxTest { @Mock private StoredRequestProcessor storedRequestProcessor; @Mock(strictness = LENIENT) + private ProfilesProcessor profilesProcessor; + @Mock(strictness = LENIENT) private ApplicationSettings applicationSettings; @Mock private IpAddressHelper ipAddressHelper; @@ -150,6 +153,9 @@ public void setUp() { given(timeoutResolver.limitToMax(any())).willReturn(2000L); + given(profilesProcessor.process(any(), any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(1))); + given(hookStageExecutor.executeEntrypointStage(any(), any(), any(), any())) .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( false, @@ -1787,6 +1793,7 @@ private void givenTarget(int timeoutAdjustmentFactor) { timeoutResolver, timeoutFactory, storedRequestProcessor, + profilesProcessor, applicationSettings, ipAddressHelper, hookStageExecutor, diff --git a/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java b/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java index fda7030dcff..8379cf6de94 100644 --- a/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java @@ -31,7 +31,7 @@ public class SettingsCacheNotificationHandlerTest extends VertxTest { @Mock - private CacheNotificationListener cacheNotificationListener; + private CacheNotificationListener cacheNotificationListener; private SettingsCacheNotificationHandler handler; @Mock(strictness = LENIENT) @@ -45,7 +45,7 @@ public class SettingsCacheNotificationHandlerTest extends VertxTest { @BeforeEach public void setUp() { - handler = new SettingsCacheNotificationHandler(cacheNotificationListener, jacksonMapper, "endpoint"); + handler = new SettingsCacheNotificationHandler("endpoint", cacheNotificationListener, jacksonMapper); given(routingContext.request()).willReturn(httpRequest); given(routingContext.response()).willReturn(httpResponse); diff --git a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java index 1767491959a..125a15f49c8 100644 --- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java @@ -58,9 +58,10 @@ public void setUp() { target = new CachingApplicationSettings( delegateSettings, - new SettingsCache(360, 100, 0), - new SettingsCache(360, 100, 0), - new SettingsCache(360, 100, 0), + new SettingsCache<>(360, 100, 0), + new SettingsCache<>(360, 100, 0), + new SettingsCache<>(360, 100, 0), + new SettingsCache<>(360, 100, 0), metrics, 360, 100, @@ -357,7 +358,7 @@ public void getStoredDataShouldReturnResultOnSuccessiveCalls() { singletonMap("reqid", "json"), singletonMap("impid", "json2"), emptyList()))); // when - final Future future = + final Future> future = target.getStoredData("1001", singleton("reqid"), singleton("impid"), timeout); // second call target.getStoredData("1001", singleton("reqid"), singleton("impid"), timeout); @@ -378,7 +379,7 @@ public void getStoredDataShouldPropagateFailure() { .willReturn(Future.failedFuture(new InvalidRequestException("error"))); // when - final Future future = + final Future> future = target.getStoredData(null, singleton("id"), emptySet(), timeout); // then @@ -396,7 +397,7 @@ public void getStoredDataShouldReturnResultWithErrorsOnNotSuccessiveCallToCacheA emptyMap(), emptyMap(), singletonList("error")))); // when - final Future future = + final Future> future = target.getStoredData(null, singleton("id"), emptySet(), timeout); // then @@ -416,7 +417,7 @@ public void getStoredDataShouldReturnResultWithErrorIfAccountDiffers() { // when target.getStoredData("1001", singleton("reqid"), emptySet(), timeout); // second call - final Future future = + final Future> future = target.getStoredData("1002", singleton("reqid"), emptySet(), timeout); // then diff --git a/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java index 21f0f9d31a5..890f73e0e21 100644 --- a/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java @@ -173,11 +173,13 @@ public void getStoredDataShouldReturnResultFromFirstDelegateIfPresent() { // given given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( - StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key2", "value2"), + StoredDataResult.of( + singletonMap("key1", "value1"), + singletonMap("key2", "value2"), emptyList()))); // when - final Future future = + final Future> future = compositeApplicationSettings.getStoredData(null, singleton("key1"), singleton("key2"), null); // then @@ -200,11 +202,13 @@ public void getStoredDataShouldReturnResultFromFromSecondDelegateIfFirstDelegate given(delegate2.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( - StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key2", "value2"), + StoredDataResult.of( + singletonMap("key1", "value1"), + singletonMap("key2", "value2"), emptyList()))); // when - final Future future = + final Future> future = compositeApplicationSettings.getStoredData(null, singleton("key1"), singleton("key2"), null); // then @@ -229,7 +233,7 @@ public void getStoredDataShouldReturnEmptyResultIfAllDelegatesFail() { StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error2")))); // when - final Future future = + final Future> future = compositeApplicationSettings.getStoredData(null, singleton("key1"), emptySet(), null); // then @@ -244,12 +248,17 @@ public void getStoredDataShouldPassOnlyMissingIdsToSecondDelegateIfFirstDelegate // given given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( - StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key3", "value3"), + StoredDataResult.of( + singletonMap("key1", "value1"), + singletonMap("key3", "value3"), singletonList("error1")))); // when - compositeApplicationSettings.getStoredData(null, new HashSet<>(asList("key1", "key2")), - new HashSet<>(asList("key3", "key4")), null); + compositeApplicationSettings.getStoredData( + null, + new HashSet<>(asList("key1", "key2")), + new HashSet<>(asList("key3", "key4")), + null); // then @SuppressWarnings("unchecked") final ArgumentCaptor> requestCaptor = ArgumentCaptor.forClass( @@ -268,18 +277,25 @@ public void getStoredDataShouldReturnResultConsequentlyFromAllDelegates() { // given given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( - StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key3", "value3"), + StoredDataResult.of( + singletonMap("key1", "value1"), + singletonMap("key3", "value3"), asList("key2 not found", "key4 not found")))); given(delegate2.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( - StoredDataResult.of(singletonMap("key2", "value2"), singletonMap("key4", "value4"), + StoredDataResult.of( + singletonMap("key2", "value2"), + singletonMap("key4", "value4"), emptyList()))); // when - final Future future = - compositeApplicationSettings.getStoredData(null, new HashSet<>(asList("key1", "key2")), - new HashSet<>(asList("key3", "key4")), null); + final Future> future = + compositeApplicationSettings.getStoredData( + null, + new HashSet<>(asList("key1", "key2")), + new HashSet<>(asList("key3", "key4")), + null); // then assertThat(future.succeeded()).isTrue(); @@ -302,7 +318,7 @@ public void getAmpStoredDataShouldReturnResultFromFirstDelegateIfPresent() { StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), emptyList()))); // when - final Future future = + final Future> future = compositeApplicationSettings.getAmpStoredData(null, singleton("key1"), emptySet(), null); // then @@ -326,7 +342,7 @@ public void getAmpStoredDataShouldReturnResultFromFromSecondDelegateIfFirstDeleg StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), emptyList()))); // when - final Future future = + final Future> future = compositeApplicationSettings.getAmpStoredData(null, singleton("key1"), emptySet(), null); // then @@ -349,7 +365,7 @@ public void getAmpStoredDataShouldReturnEmptyResultIfAllDelegatesFail() { StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error2")))); // when - final Future future = + final Future> future = compositeApplicationSettings.getAmpStoredData(null, singleton("key1"), emptySet(), null); // then @@ -367,8 +383,8 @@ public void getAmpStoredDataShouldPassOnlyMissingIdsToSecondDelegateIfFirstDeleg StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), singletonList("error1")))); // when - compositeApplicationSettings.getAmpStoredData(null, new HashSet<>(asList("key1", "key2")), emptySet(), - null); + compositeApplicationSettings.getAmpStoredData( + null, new HashSet<>(asList("key1", "key2")), emptySet(), null); // then @SuppressWarnings("unchecked") final ArgumentCaptor> requestCaptor = ArgumentCaptor.forClass( @@ -384,7 +400,9 @@ public void getAmpStoredDataShouldReturnResultConsequentlyFromAllDelegates() { // given given(delegate1.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( - StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), + StoredDataResult.of( + singletonMap("key1", "value1"), + emptyMap(), singletonList("key2 not found")))); given(delegate2.getAmpStoredData(any(), anySet(), anySet(), any())) @@ -392,8 +410,8 @@ public void getAmpStoredDataShouldReturnResultConsequentlyFromAllDelegates() { StoredDataResult.of(singletonMap("key2", "value2"), emptyMap(), emptyList()))); // when - final Future future = compositeApplicationSettings.getAmpStoredData(null, - new HashSet<>(asList("key1", "key2")), emptySet(), null); + final Future> future = compositeApplicationSettings.getAmpStoredData( + null, new HashSet<>(asList("key1", "key2")), emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -479,8 +497,7 @@ public void getStoredResponsesShouldPassOnlyMissingIdsToSecondDelegateIfFirstDel compositeApplicationSettings.getStoredResponses(new HashSet<>(asList("key1", "key2")), null); // then - @SuppressWarnings("unchecked") final ArgumentCaptor> responseCaptor = ArgumentCaptor.forClass( - Set.class); + final ArgumentCaptor> responseCaptor = ArgumentCaptor.forClass(Set.class); verify(delegate2).getStoredResponses(responseCaptor.capture(), any()); assertThat(responseCaptor.getValue()).hasSize(1).containsOnly("key2"); diff --git a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java index 86c72604150..c0a9730c48f 100644 --- a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java @@ -38,15 +38,25 @@ public class DatabaseApplicationSettingsTest extends VertxTest { private static final String SELECT_ACCOUNT_QUERY = "SELECT config FROM accounts_account where uuid = %ACCOUNT_ID% LIMIT 1"; - private static final String SELECT_QUERY = - "SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests " - + "WHERE reqid IN (%REQUEST_ID_LIST%) " - + "UNION ALL " - + "SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps " - + "WHERE impid IN (%IMP_ID_LIST%)"; - - private static final String SELECT_RESPONSE_QUERY = "SELECT responseId, responseData FROM stored_responses " - + "WHERE responseId IN (%RESPONSE_ID_LIST%)"; + private static final String SELECT_QUERY = """ + SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests \ + WHERE reqid IN (%REQUEST_ID_LIST%) \ + UNION ALL \ + SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps \ + WHERE impid IN (%IMP_ID_LIST%) + """; + + private static final String SELECT_PROFILES_QUERY = """ + SELECT accountId, profileId, profile, mergePrecedence, type \ + FROM profiles \ + WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%) + """; + + private static final String SELECT_RESPONSE_QUERY = """ + SELECT responseId, responseData \ + FROM stored_responses \ + WHERE responseId IN (%RESPONSE_ID_LIST%) + """; @Mock private ParametrizedQueryHelper parametrizedQueryHelper; @@ -69,6 +79,7 @@ public void setUp() { SELECT_ACCOUNT_QUERY, SELECT_QUERY, SELECT_QUERY, + SELECT_PROFILES_QUERY, SELECT_RESPONSE_QUERY); } @@ -115,7 +126,7 @@ public void getStoredDataShouldReturnExpectedResult() { given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_QUERY, 2, 2)) .willReturn("query"); - final StoredDataResult givenStoredDataResult = StoredDataResult.of( + final StoredDataResult givenStoredDataResult = StoredDataResult.of( Map.of("1", "value1", "2", "value2"), Map.of("4", "value4", "5", "value5"), emptyList()); @@ -123,7 +134,7 @@ public void getStoredDataShouldReturnExpectedResult() { .willReturn(Future.succeededFuture(givenStoredDataResult)); // when - final Future future = target.getStoredData( + final Future> future = target.getStoredData( "1001", new HashSet<>(asList("1", "2")), new HashSet<>(asList("4", "5")), timeout); // then @@ -137,7 +148,7 @@ public void getAmpStoredDataShouldReturnExpectedResult() { given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_QUERY, 2, 0)) .willReturn("query"); - final StoredDataResult givenStoredDataResult = StoredDataResult.of( + final StoredDataResult givenStoredDataResult = StoredDataResult.of( Map.of("1", "value1", "2", "value2"), Map.of(), emptyList()); @@ -145,7 +156,7 @@ public void getAmpStoredDataShouldReturnExpectedResult() { .willReturn(Future.succeededFuture(givenStoredDataResult)); // when - final Future future = target.getAmpStoredData( + final Future> future = target.getAmpStoredData( "1001", new HashSet<>(asList("1", "2")), new HashSet<>(asList("4", "5")), timeout); // then @@ -159,7 +170,7 @@ public void getVideoStoredDataShouldReturnExpectedResult() { given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_QUERY, 2, 2)) .willReturn("query"); - final StoredDataResult givenStoredDataResult = StoredDataResult.of( + final StoredDataResult givenStoredDataResult = StoredDataResult.of( Map.of("1", "value1", "2", "value2"), Map.of("4", "value4", "5", "value5"), emptyList()); @@ -167,7 +178,7 @@ public void getVideoStoredDataShouldReturnExpectedResult() { .willReturn(Future.succeededFuture(givenStoredDataResult)); // when - final Future future = target.getVideoStoredData("1001", + final Future> future = target.getVideoStoredData("1001", new HashSet<>(asList("1", "2")), new HashSet<>(asList("4", "5")), timeout); // then @@ -181,7 +192,7 @@ public void getStoredDataShouldReturnResultWithError() { given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_QUERY, 2, 0)) .willReturn("query"); - final StoredDataResult givenStoredDataResult = StoredDataResult.of( + final StoredDataResult givenStoredDataResult = StoredDataResult.of( Map.of("1", "value1"), Map.of(), List.of("No stored request found for id: 3")); @@ -189,7 +200,7 @@ public void getStoredDataShouldReturnResultWithError() { .willReturn(Future.succeededFuture(givenStoredDataResult)); // when - final Future future = + final Future> future = target.getStoredData("1001", new HashSet<>(asList("1", "3")), emptySet(), timeout); // then @@ -203,7 +214,7 @@ public void getAmpStoredDataShouldReturnResultWithError() { given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_QUERY, 2, 0)) .willReturn("query"); - final StoredDataResult givenStoredDataResult = StoredDataResult.of( + final StoredDataResult givenStoredDataResult = StoredDataResult.of( Map.of("1", "value1"), Map.of(), List.of("No stored request found for id: 3")); @@ -211,7 +222,7 @@ public void getAmpStoredDataShouldReturnResultWithError() { .willReturn(Future.succeededFuture(givenStoredDataResult)); // when - final Future future = + final Future> future = target.getAmpStoredData("1001", new HashSet<>(asList("1", "3")), emptySet(), timeout); // then @@ -225,7 +236,7 @@ public void getVideoStoredDataShouldReturnResultWithError() { given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_QUERY, 2, 0)) .willReturn("query"); - final StoredDataResult givenStoredDataResult = StoredDataResult.of( + final StoredDataResult givenStoredDataResult = StoredDataResult.of( Map.of("1", "value1"), Map.of(), List.of("No stored request found for id: 3")); @@ -233,7 +244,7 @@ public void getVideoStoredDataShouldReturnResultWithError() { .willReturn(Future.succeededFuture(givenStoredDataResult)); // when - final Future future = + final Future> future = target.getVideoStoredData("1001", new HashSet<>(asList("1", "3")), emptySet(), timeout); // then diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index 8478978096b..506132da036 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -61,8 +61,15 @@ public void creationShouldFailIfFileCouldNotBeParsed() { // when and then assertThatIllegalArgumentException() - .isThrownBy(() -> new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", - "ignore", jacksonMapper)); + .isThrownBy(() -> new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper)); } @Test @@ -70,9 +77,15 @@ public void getAccountByIdShouldReturnEmptyWhenAccountsAreMissing() { // given given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("domains:")); - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future account = applicationSettings.getAccountById("123", null); @@ -131,9 +144,15 @@ public void getAccountByIdShouldReturnPresentAccount() { + "}" + "]")); - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future account = applicationSettings.getAccountById("123", null); @@ -190,9 +209,15 @@ public void getAccountByIdShouldReturnEmptyForUnknownAccount() { given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts: [ {id: '123'}, {id: '456'} ]")); - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", - "ignore", jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future account = applicationSettings.getAccountById("789", null); @@ -207,17 +232,26 @@ public void initializeCategoriesShouldThrowExceptionWhenFileCantBeParsed() { given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/1.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/categories/iab_1.json")); given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value1")) // stored request .willReturn(Buffer.buffer("value2")) // stored imp + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value2")) // stored response .willReturn(Buffer.buffer("{\"iab-1\": 1}")); // categories // when and then - assertThatThrownBy(() -> new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", - "ignore", jacksonMapper)) + assertThatThrownBy(() -> new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper)) .isInstanceOf(PreBidException.class) .hasMessage("Failed to decode categories for file /home/user/categories/iab_1.json"); } @@ -228,16 +262,25 @@ public void getCategoriesShouldReturnResultFoundByAdServerAndPublisherSuccessful given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/1.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/categories/iab_1.json")); given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value1")) // stored request .willReturn(Buffer.buffer("value2")) // stored imp + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value2")) // stored response .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final ApplicationSettings applicationSettings = new FileApplicationSettings(fileSystem, "ignore", "ignore", - "ignore", "ignore", "ignore", jacksonMapper); + final ApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future> result = applicationSettings.getCategories("iab", "1", null); @@ -253,16 +296,25 @@ public void getCategoriesShouldReturnResultFoundByAdServerAndWithoutPublisherSuc given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/1.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/categories/iab.json")); given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value1")) // stored request .willReturn(Buffer.buffer("value2")) // stored imp + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value2")) // stored response .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final ApplicationSettings applicationSettings = new FileApplicationSettings(fileSystem, "ignore", "ignore", - "ignore", "ignore", "ignore", jacksonMapper); + final ApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future> result = applicationSettings.getCategories("iab", null, null); @@ -278,16 +330,25 @@ public void getCategoriesShouldReturnFailedFutureWhenFileWasNotFound() { given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/1.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/categories/iab_1.json")); given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value1")) // stored request .willReturn(Buffer.buffer("value2")) // stored imp + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value2")) // stored response .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final ApplicationSettings applicationSettings = new FileApplicationSettings(fileSystem, "ignore", "ignore", - "ignore", "ignore", "ignore", jacksonMapper); + final ApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future> result = applicationSettings.getCategories("iab", "2", null); @@ -304,19 +365,27 @@ public void getStoredDataShouldReturnResultWithNotFoundErrorForNonExistingReques given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/1.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/categories/iab_1.json")); given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value1")) // stored request .willReturn(Buffer.buffer("value2")) // stored imp + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when - final Future storedRequestResult = + final Future> storedRequestResult = applicationSettings.getStoredData(null, singleton("2"), emptySet(), null); // then @@ -332,21 +401,29 @@ public void getStoredDataShouldReturnResultWithNotFoundErrorForNonExistingImpId( given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/1.json")) .willReturn(singletonList("/home/user/imps/1.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/responses/3.json")); given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value1")) // stored request .willReturn(Buffer.buffer("value2")) // stored imp + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value3")) // stored response .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when - final Future storedRequestResult = + final Future> storedRequestResult = applicationSettings.getStoredData(null, emptySet(), singleton("2"), null); // then @@ -362,21 +439,29 @@ public void getStoredDataShouldReturnResultWithNoErrorsIfAllIdsArePresent() { given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/1.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/responses/3.json")); given(fileSystem.readFileBlocking(anyString())) .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value1")) // stored request .willReturn(Buffer.buffer("value2")) // stored imp + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value3")) // stored response .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when - final Future storedRequestResult = + final Future> storedRequestResult = applicationSettings.getStoredData(null, singleton("1"), singleton("2"), null); // then @@ -395,6 +480,7 @@ public void getStoredResponsesShouldReturnEmptyResultAndErrorsWhenResponseIdsAre given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/3.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/responses/1.json")) .willReturn(singletonList("/home/user/categories")); @@ -402,11 +488,18 @@ public void getStoredResponsesShouldReturnEmptyResultAndErrorsWhenResponseIdsAre .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value3")) // requests .willReturn(Buffer.buffer("value2")) // imps + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value1")) // responses .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future storedResponsesResult = @@ -425,6 +518,7 @@ public void getStoredResponsesShouldReturnResultWithMissingIdsIfNotAllIdsArePres given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/3.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/responses/1.json")) .willReturn(singletonList("/home/user/categories/iab_1.json")); @@ -432,12 +526,19 @@ public void getStoredResponsesShouldReturnResultWithMissingIdsIfNotAllIdsArePres .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value3")) // requests .willReturn(Buffer.buffer("value2")) // imps + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value1")) // responses .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future storedResponsesResult = @@ -458,6 +559,7 @@ public void getStoredResponsesShouldReturnResultWithoutErrorsIfAllIdsArePresent( given(fileSystem.readDirBlocking(anyString())) .willReturn(singletonList("/home/user/requests/3.json")) .willReturn(singletonList("/home/user/imps/2.json")) + .willReturn(singletonList("/home/user/profiles/3-3.json")) .willReturn(singletonList("/home/user/responses/1.json")) .willReturn(singletonList("/home/user/categories/iab_1.json")); @@ -465,12 +567,19 @@ public void getStoredResponsesShouldReturnResultWithoutErrorsIfAllIdsArePresent( .willReturn(Buffer.buffer("accounts:")) // settings file .willReturn(Buffer.buffer("value3")) // requests .willReturn(Buffer.buffer("value2")) // imps + .willReturn(Buffer.buffer("{}")) // profile .willReturn(Buffer.buffer("value1")) // responses .willReturn(Buffer.buffer("{\"iab-1\": {\"id\": \"id\"}}")); // categories - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", - jacksonMapper); + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); // when final Future storedResponsesResult = @@ -491,7 +600,14 @@ public void storedDataInitializationShouldNotReadFromNonJsonFiles() { given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("accounts:")); // settings file // when - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore", "ignore", + new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", jacksonMapper); // then diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index 60627cf2571..0a46bc83d16 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -68,8 +68,14 @@ public class HttpApplicationSettingsTest extends VertxTest { @BeforeEach public void setUp() { - httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, AMP_ENDPOINT, - VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false); + httpApplicationSettings = new HttpApplicationSettings( + false, + ENDPOINT, + AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper); final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); @@ -80,24 +86,42 @@ public void setUp() { @Test public void creationShouldFailsOnInvalidEndpoint() { assertThatIllegalArgumentException() - .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, "invalid_url", AMP_ENDPOINT, - VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false)) + .isThrownBy(() -> new HttpApplicationSettings( + false, + "invalid_url", + AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper)) .withMessage("URL supplied is not valid: invalid_url"); } @Test public void creationShouldFailsOnInvalidAmpEndpoint() { assertThatIllegalArgumentException() - .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, "invalid_url", - VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false)) + .isThrownBy(() -> new HttpApplicationSettings( + false, + ENDPOINT, + "invalid_url", + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper)) .withMessage("URL supplied is not valid: invalid_url"); } @Test public void creationShouldFailsOnInvalidVideoEndpoint() { assertThatIllegalArgumentException() - .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, AMP_ENDPOINT, - "invalid_url", CATEGORY_ENDPOINT, false)) + .isThrownBy(() -> new HttpApplicationSettings( + false, + ENDPOINT, + AMP_ENDPOINT, + "invalid_url", + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper)) .withMessage("URL supplied is not valid: invalid_url"); } @@ -132,8 +156,14 @@ public void getAccountByIdShouldReturnFetchedAccount() throws JsonProcessingExce public void getAccountByIdShouldReturnFetchedAccountWithRfc3986CompatibleParams() throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, null); - httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, - ENDPOINT, AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT, true); + httpApplicationSettings = new HttpApplicationSettings( + true, + ENDPOINT, + AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper); final Account account = Account.builder() .id("someId") @@ -235,8 +265,8 @@ public void getStoredResponsesShouldReturnFailedFutureWithNotSupportedReason() { @Test public void getStoredDataShouldReturnEmptyResultIfEmptyRequestsIdsGiven() { // when - final Future future = httpApplicationSettings.getStoredData(null, emptySet(), - emptySet(), null); + final Future> future = + httpApplicationSettings.getStoredData(null, emptySet(), emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -249,7 +279,7 @@ public void getStoredDataShouldReturnEmptyResultIfEmptyRequestsIdsGiven() { @Test public void getStoredDataShouldReturnResultWithErrorIfTimeoutAlreadyExpired() { // when - final Future future = + final Future> future = httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), expiredTimeout); // then @@ -282,8 +312,14 @@ public void getStoredDataShouldSendHttpRequestWithExpectedNewParams() { public void getStoredDataShouldSendHttpRequestWithExpectedAppendedParams() { // given givenHttpClientReturnsResponse(200, null); - httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, - "http://some-domain.com?param1=value1", AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false); + httpApplicationSettings = new HttpApplicationSettings( + false, + "http://some-domain.com?param1=value1", + AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper); // when httpApplicationSettings.getStoredData(null, singleton("id1"), singleton("id2"), timeout); @@ -299,8 +335,13 @@ public void getStoredDataShouldSendHttpRequestWithExpectedAppendedParams() { public void getStoredDataShouldSendHttpRequestWithRfc3986CompatibleParams() throws URISyntaxException { // given givenHttpClientReturnsResponse(200, null); - httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, - ENDPOINT, AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT, true); + httpApplicationSettings = new HttpApplicationSettings( + true, + ENDPOINT, AMP_ENDPOINT, + VIDEO_ENDPOINT, + CATEGORY_ENDPOINT, + httpClient, + jacksonMapper); // when httpApplicationSettings.getStoredData(null, Set.of("id1", "id2"), Set.of("id1", "id2"), timeout); @@ -328,7 +369,7 @@ public void getStoredDataShouldReturnResultWithErrorIfHttpClientFails() { givenHttpClientProducesException(new RuntimeException("Request exception")); // when - final Future future = + final Future> future = httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then @@ -345,7 +386,7 @@ public void getStoredDataShouldReturnResultWithErrorIfHttpClientRespondsNot200St givenHttpClientReturnsResponse(500, "ignored"); // when - final Future future = + final Future> future = httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then @@ -362,7 +403,7 @@ public void getStoredDataShouldReturnResultWithErrorIfHttpResponseIsMalformed() givenHttpClientReturnsResponse(200, "invalid-response"); // when - final Future future = + final Future> future = httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then @@ -381,7 +422,7 @@ public void getStoredDataShouldReturnResultWithErrorIfStoredRequestObjectIsMalfo givenHttpClientReturnsResponse(200, malformedStoredRequest); // when - final Future future = + final Future> future = httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then @@ -401,7 +442,7 @@ public void getStoredDataShouldReturnResultWithErrorIfStoredImpObjectIsMalformed givenHttpClientReturnsResponse(200, malformedStoredRequest); // when - final Future future = + final Future> future = httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then @@ -421,7 +462,7 @@ public void getStoredDataShouldTolerateMissedId() throws JsonProcessingException givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); // when - final Future future = httpApplicationSettings.getStoredData( + final Future> future = httpApplicationSettings.getStoredData( null, new HashSet<>(asList("id1", "id2")), new HashSet<>(asList("id3", "id4")), timeout); // then @@ -445,7 +486,7 @@ public void getStoredDataShouldReturnExpectedResult() throws JsonProcessingExcep givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); // when - final Future future = + final Future> future = httpApplicationSettings.getStoredData(null, singleton("id1"), singleton("id2"), timeout); // then diff --git a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java index a702d71ab2e..13fda67839e 100644 --- a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java @@ -209,7 +209,7 @@ public void getStoredDataShouldReturnFetchedStoredRequest(VertxTestContext conte "storedRequest".getBytes()))); // when - final Future result = target.getStoredData( + final Future> result = target.getStoredData( "accountId", Set.of("request"), emptySet(), timeout); // then @@ -237,7 +237,7 @@ public void getStoredDataShouldReturnFetchedStoredImpression(VertxTestContext co "storedImp".getBytes()))); // when - final Future result = target.getStoredData( + final Future> result = target.getStoredData( "accountId", emptySet(), Set.of("imp"), timeout); // then @@ -265,7 +265,8 @@ public void getStoredDataShouldReturnFetchedStoredImpressionWithAdUnitPath(Vertx "storedImp".getBytes()))); // when - final Future result = target.getStoredData("accountId", emptySet(), Set.of("/imp"), timeout); + final Future> result = target.getStoredData( + "accountId", emptySet(), Set.of("/imp"), timeout); // then result.onComplete(context.succeeding(storedDataResult -> { @@ -302,7 +303,7 @@ public void getStoredDataShouldReturnFetchedStoredRequestAndStoredImpression(Ver "storedImp".getBytes()))); // when - final Future result = target.getStoredData( + final Future> result = target.getStoredData( "accountId", Set.of("request"), Set.of("imp"), timeout); // then @@ -325,7 +326,7 @@ public void getStoredDataShouldReturnErrorsForNotFoundRequests(VertxTestContext new IllegalStateException("error")))); // when - final Future result = target.getStoredData( + final Future> result = target.getStoredData( "accountId", Set.of("request"), emptySet(), timeout); // then @@ -349,7 +350,7 @@ public void getStoredDataShouldReturnErrorsForNotFoundImpressions(VertxTestConte new IllegalStateException("error")))); // when - final Future result = target.getStoredData( + final Future> result = target.getStoredData( "accountId", emptySet(), Set.of("imp"), timeout); // then diff --git a/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java b/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java index 6f5d3f3e529..820b3717ac7 100644 --- a/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java +++ b/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java @@ -18,39 +18,40 @@ public class StoredItemResolverTest { public void resolveShouldFailWhenNoStoredData() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, null, "id", emptySet())) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), null, "id", emptySet())) .withMessage("No stored imp found for id: id"); } @Test public void resolveShouldFailWhenMultipleStoredDataButNoAccountInRequest() { // given - final Set storedItems = givenMultipleStoredData(); + final Set> storedItems = givenMultipleStoredData(); // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, null, "id", storedItems)) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), null, "id", storedItems)) .withMessage("Multiple stored imps found for id: id but no account was specified"); } @Test public void resolveShouldFailWhenMultipleStoredDataButAccountDiffers() { // given - final Set storedItems = givenMultipleStoredData(); + final Set> storedItems = givenMultipleStoredData(); // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, "1003", "id", storedItems)) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), "1003", "id", storedItems)) .withMessage("No stored imp found among multiple id: id for account: 1003"); } @Test public void resolveShouldReturnResultWhenMultipleStoredDataForAppropriateAccount() { // given - final Set storedItems = givenMultipleStoredData(); + final Set> storedItems = givenMultipleStoredData(); // when - final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1002", "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve( + StoredDataType.imp.name(), "1002", "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of("1002", "data2")); @@ -59,11 +60,12 @@ public void resolveShouldReturnResultWhenMultipleStoredDataForAppropriateAccount @Test public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInRequest() { // given - final Set storedItems = new HashSet<>(); + final Set> storedItems = new HashSet<>(); storedItems.add(StoredItem.of("1001", "data1")); // when - final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1001", "", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve( + StoredDataType.imp.name(), "1001", "", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of("1001", "data1")); @@ -72,11 +74,12 @@ public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInRequest() @Test public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInStoredData() { // given - final Set storedItems = new HashSet<>(); + final Set> storedItems = new HashSet<>(); storedItems.add(StoredItem.of(null, "data1")); // when - final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1001", "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve( + StoredDataType.imp.name(), "1001", "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of(null, "data1")); @@ -85,11 +88,12 @@ public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInStoredDat @Test public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountBothInRequestAndStoredData() { // given - final Set storedItems = new HashSet<>(); + final Set> storedItems = new HashSet<>(); storedItems.add(StoredItem.of(null, "data1")); // when - final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, null, "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve( + StoredDataType.imp.name(), null, "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of(null, "data1")); @@ -98,10 +102,11 @@ public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountBothInReque @Test public void resolveShouldFailWhenSingleStoredDataForAppropriateAccount() { // given - final Set storedItems = givenSingleStoredData(); + final Set> storedItems = givenSingleStoredData(); // when - final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1001", "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve( + StoredDataType.imp.name(), "1001", "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of("1001", "data1")); @@ -110,22 +115,22 @@ public void resolveShouldFailWhenSingleStoredDataForAppropriateAccount() { @Test public void resolveShouldFailWhenSingleStoredDataButAccountDiffers() { // given - final Set storedItems = givenSingleStoredData(); + final Set> storedItems = givenSingleStoredData(); // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, "1002", "id", storedItems)) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), "1002", "id", storedItems)) .withMessage("No stored imp found for id: id for account: 1002"); } - private static Set givenSingleStoredData() { - final Set storedItems = new HashSet<>(); + private static Set> givenSingleStoredData() { + final Set> storedItems = new HashSet<>(); storedItems.add(StoredItem.of("1001", "data1")); return storedItems; } - private static Set givenMultipleStoredData() { - final Set storedItems = new HashSet<>(); + private static Set> givenMultipleStoredData() { + final Set> storedItems = new HashSet<>(); storedItems.add(StoredItem.of("1001", "data1")); storedItems.add(StoredItem.of("1002", "data2")); return storedItems; diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 30a8e1be569..011f81282e6 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -657,6 +657,7 @@ metrics.accounts.default-verbosity=none settings.filesystem.settings-filename=src/test/resources/org/prebid/server/it/test-app-settings.yaml settings.filesystem.stored-requests-dir=src/test/resources/org/prebid/server/it/storedrequests settings.filesystem.stored-imps-dir=src/test/resources/org/prebid/server/it/storedimps +settings.filesystem.profiles-dir=src/test/resources/org/prebid/server/it/profiles settings.filesystem.stored-responses-dir=src/test/resources/org/prebid/server/it/storedresponses settings.filesystem.categories-dir=src/test/resources/org/prebid/server/it/categories settings.in-memory-cache.notification-endpoints-enabled=true From 0f3305f71fd723bec3aa9ceb2f1301f0c1965618 Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 8 Aug 2025 20:41:55 +0200 Subject: [PATCH 13/24] Fix units --- .../org/prebid/server/it/profiles/test-profile.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/test/resources/org/prebid/server/it/profiles/test-profile.json diff --git a/src/test/resources/org/prebid/server/it/profiles/test-profile.json b/src/test/resources/org/prebid/server/it/profiles/test-profile.json new file mode 100644 index 00000000000..76a896ffa55 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/profiles/test-profile.json @@ -0,0 +1,9 @@ +{ + "type": "request", + "mergeprecedence": "profile", + "body": { + "device": { + "devicetype": 1 + } + } +} From 0a9f904d48dba802deb9fe277d323f929c3aa097 Mon Sep 17 00:00:00 2001 From: Danylo Date: Fri, 8 Aug 2025 21:10:54 +0200 Subject: [PATCH 14/24] Refactor --- .../auction/externalortb/ProfilesProcessor.java | 16 +++------------- .../helper/DatabaseProfilesResultMapper.java | 13 +++++++++---- .../prebid/server/settings/model/Profile.java | 16 +++++++++++++--- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index cf017196781..f2bedb6262c 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -12,7 +12,6 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.execution.timeout.TimeoutFactory; -import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.log.ConditionalLogger; @@ -239,22 +238,13 @@ private T applyProfiles(List profilesIds, } private ObjectNode mergeProfile(ObjectNode original, Profile profile, String profileId) { - final ObjectNode profileBody = parseProfile(profile.getBody(), profileId); return switch (profile.getMergePrecedence()) { - case REQUEST -> merge(original, profileBody, profileId); - case PROFILE -> merge(profileBody, original, profileId); + case REQUEST -> merge(original, profile.getBody(), profileId); + case PROFILE -> merge(profile.getBody(), original, profileId); }; } - private ObjectNode parseProfile(String body, String profileId) { - try { - return mapper.decodeValue(body, ObjectNode.class); - } catch (DecodeException e) { - throw new InvalidProfileException("Can't parse profile %s: %s".formatted(profileId, e.getMessage())); - } - } - - private ObjectNode merge(ObjectNode takePrecedence, ObjectNode other, String profileId) { + private ObjectNode merge(JsonNode takePrecedence, JsonNode other, String profileId) { try { return (ObjectNode) jsonMerger.merge(takePrecedence, other); } catch (InvalidRequestException e) { diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java index 79c14f75c8b..abba9c1b1c5 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java @@ -1,9 +1,12 @@ package org.prebid.server.settings.helper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowIterator; import io.vertx.sqlclient.RowSet; import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.settings.model.Profile; @@ -70,16 +73,18 @@ public static StoredDataResult map(RowSet rowSet, final String fetchedAccountId = Objects.toString(row.getValue(0), null); final String id = Objects.toString(row.getValue(1), null); - final String profileBody = Objects.toString(row.getValue(2), null); + final String profileBodyAsString = Objects.toString(row.getValue(2), null); final String mergePrecedenceAsString = Objects.toString(row.getValue(3), null); final String typeAsString = Objects.toString(row.getValue(4), null); - final Profile.Type type; + final JsonNode profileBody; final Profile.MergePrecedence mergePrecedence; + final Profile.Type type; try { - type = Profile.Type.valueOf(typeAsString); + profileBody = ObjectMapperProvider.mapper().readTree(profileBodyAsString); mergePrecedence = Profile.MergePrecedence.valueOf(mergePrecedenceAsString); - } catch (IllegalArgumentException e) { + type = Profile.Type.valueOf(typeAsString); + } catch (IllegalArgumentException | JsonProcessingException e) { logger.error("Profile with id={} has invalid value: ''{}'' and will be ignored.", e, id, typeAsString); continue; diff --git a/src/main/java/org/prebid/server/settings/model/Profile.java b/src/main/java/org/prebid/server/settings/model/Profile.java index ea9dc277d57..96fde5a584f 100644 --- a/src/main/java/org/prebid/server/settings/model/Profile.java +++ b/src/main/java/org/prebid/server/settings/model/Profile.java @@ -1,6 +1,8 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Value; @Value(staticConstructor = "of") @@ -11,15 +13,23 @@ public class Profile { @JsonProperty("mergeprecedence") MergePrecedence mergePrecedence; - String body; + JsonNode body; public enum Type { - REQUEST, IMP + @JsonAlias("request") + REQUEST, + + @JsonAlias("imp") + IMP } public enum MergePrecedence { - REQUEST, PROFILE + @JsonAlias("request") + REQUEST, + + @JsonAlias("profile") + PROFILE } } From 1f1bb4b45cb2f44f3e0286333571584cf40e2068 Mon Sep 17 00:00:00 2001 From: Danylo Date: Sun, 10 Aug 2025 01:14:54 +0200 Subject: [PATCH 15/24] Resolve warnings --- .../auction/VideoStoredRequestProcessorTest.java | 8 ++++---- .../prebid/server/settings/SettingsCacheTest.java | 4 ++-- .../service/DatabasePeriodicRefreshServiceTest.java | 6 +++--- .../service/HttpPeriodicRefreshServiceTest.java | 12 ++++++++---- .../service/S3PeriodicRefreshServiceTest.java | 2 +- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java index 66442d47c93..abe33a80df3 100644 --- a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java @@ -141,7 +141,7 @@ public void shouldReturnMergedStoredAndDefaultRequest() { identity(), builder -> builder.pods(pods)); - final StoredDataResult storedDataResult = StoredDataResult.of( + final StoredDataResult storedDataResult = StoredDataResult.of( singletonMap(STORED_REQUEST_ID, jacksonMapper.encodeToString(storedVideo)), singletonMap(STORED_POD_ID, "{}"), emptyList()); @@ -245,7 +245,7 @@ public void shouldFailWhenThereAreNoStoredImpsFound() { identity(), builder -> builder.pods(asList(pod1, pod2))); - final StoredDataResult storedDataResult = StoredDataResult.of(emptyMap(), emptyMap(), emptyList()); + final StoredDataResult storedDataResult = StoredDataResult.of(emptyMap(), emptyMap(), emptyList()); given(applicationSettings.getVideoStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture(storedDataResult)); @@ -291,7 +291,7 @@ public void shouldReturnCorrectAdPodDurationIfRequireExactDurationIsTrue() { .durationRangeSec(asList(30, 60, 80)) .pods(singletonList(Pod.of(123, 30, STORED_POD_ID)))); - final StoredDataResult storedDataResult = StoredDataResult.of( + final StoredDataResult storedDataResult = StoredDataResult.of( singletonMap(STORED_REQUEST_ID, jacksonMapper.encodeToString(storedVideo)), singletonMap(STORED_POD_ID, "{}"), emptyList()); @@ -364,7 +364,7 @@ public void shouldReturnCorrectPriceGranularityInRequest() { bidRequestVideoBuilder -> bidRequestVideoBuilder.pricegranularity(priceGranularity), builder -> builder.pods(singletonList(Pod.of(123, 20, STORED_POD_ID)))); - final StoredDataResult storedDataResult = StoredDataResult.of( + final StoredDataResult storedDataResult = StoredDataResult.of( singletonMap(STORED_REQUEST_ID, jacksonMapper.encodeToString(storedVideo)), singletonMap(STORED_POD_ID, "{}"), emptyList()); diff --git a/src/test/java/org/prebid/server/settings/SettingsCacheTest.java b/src/test/java/org/prebid/server/settings/SettingsCacheTest.java index 78115f6c870..bcb23d6a949 100644 --- a/src/test/java/org/prebid/server/settings/SettingsCacheTest.java +++ b/src/test/java/org/prebid/server/settings/SettingsCacheTest.java @@ -11,11 +11,11 @@ public class SettingsCacheTest { - private SettingsCache settingsCache; + private SettingsCache settingsCache; @BeforeEach public void setUp() { - settingsCache = new SettingsCache(10, 10, 0); + settingsCache = new SettingsCache<>(10, 10, 0); } @Test diff --git a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java index 1e1ffd37271..bae78c111b4 100644 --- a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java +++ b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java @@ -41,7 +41,7 @@ public class DatabasePeriodicRefreshServiceTest { @Mock - private CacheNotificationListener cacheNotificationListener; + private CacheNotificationListener cacheNotificationListener; @Mock private Vertx vertx; @Mock(strictness = LENIENT) @@ -56,9 +56,9 @@ public class DatabasePeriodicRefreshServiceTest { @BeforeEach public void setUp() { - final StoredDataResult initialResult = StoredDataResult.of(singletonMap("id1", "value1"), + final StoredDataResult initialResult = StoredDataResult.of(singletonMap("id1", "value1"), singletonMap("id2", "value2"), emptyList()); - final StoredDataResult updateResult = StoredDataResult.of(singletonMap("id1", "null"), + final StoredDataResult updateResult = StoredDataResult.of(singletonMap("id1", "null"), singletonMap("id2", "changed_value"), emptyList()); given(databaseClient.executeQuery(eq("init_query"), anyList(), any(), any())) diff --git a/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java index d46b3fb0c29..3cb36048ffb 100644 --- a/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java +++ b/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java @@ -42,7 +42,7 @@ public class HttpPeriodicRefreshServiceTest extends VertxTest { private static final String ENDPOINT_URL = "http://stored-requests.prebid.com"; @Mock - private CacheNotificationListener cacheNotificationListener; + private CacheNotificationListener cacheNotificationListener; @Mock(strictness = LENIENT) private HttpClient httpClient; @Mock @@ -165,9 +165,13 @@ public void shouldModifyEndpointUrlCorrectlyIfUrlHasParameters() { verify(httpClient).get(startsWith("http://stored-requests.prebid.com?amp=true&last-modified="), anyLong()); } - private static void createAndInitService(CacheNotificationListener notificationListener, - String url, long refreshPeriod, long timeout, - Vertx vertx, HttpClient httpClient) { + private static void createAndInitService(CacheNotificationListener notificationListener, + String url, + long refreshPeriod, + long timeout, + Vertx vertx, + HttpClient httpClient) { + final HttpPeriodicRefreshService httpPeriodicRefreshService = new HttpPeriodicRefreshService( url, refreshPeriod, timeout, notificationListener, vertx, httpClient, jacksonMapper); httpPeriodicRefreshService.initialize(Promise.promise()); diff --git a/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java index e9b37a75d94..b46587f01a8 100644 --- a/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java +++ b/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java @@ -51,7 +51,7 @@ public class S3PeriodicRefreshServiceTest extends VertxTest { private S3AsyncClient s3AsyncClient; @Mock - private CacheNotificationListener cacheNotificationListener; + private CacheNotificationListener cacheNotificationListener; @Mock private Clock clock; From 479e3a008e3a860c8567ac5f885e419199ab2939 Mon Sep 17 00:00:00 2001 From: Danylo Date: Sun, 10 Aug 2025 01:37:05 +0200 Subject: [PATCH 16/24] Refactor --- .../externalortb/ProfilesProcessor.java | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index f2bedb6262c..ecbba69ddae 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -76,15 +76,15 @@ public ProfilesProcessor(int maxProfiles, } public Future process(AuctionContext auctionContext, BidRequest bidRequest) { - final AllProfilesIds profilesIds = profilesIds(bidRequest, auctionContext); - if (profilesIds.isEmpty()) { - return Future.succeededFuture(bidRequest); - } - final String accountId = Optional.ofNullable(auctionContext.getAccount()) .map(Account::getId) .orElse(StringUtils.EMPTY); + final AllProfilesIds profilesIds = profilesIds(bidRequest, auctionContext, accountId); + if (profilesIds.isEmpty()) { + return Future.succeededFuture(bidRequest); + } + return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest)) .compose(profiles -> emitMetrics(accountId, profiles, auctionContext)) .map(profiles -> mergeResults( @@ -94,7 +94,7 @@ public Future process(AuctionContext auctionContext, BidRequest bidR new InvalidRequestException("Error during processing profiles: " + e.getMessage()))); } - private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auctionContext) { + private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auctionContext, String accountId) { final AllProfilesIds initialProfilesIds = new AllProfilesIds( requestProfilesIds(bidRequest), bidRequest.getImp().stream().map(this::impProfilesIds).toList()); @@ -109,7 +109,7 @@ private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auction if (auctionContext.getDebugContext().isDebugEnabled() && !profilesIds.equals(initialProfilesIds)) { auctionContext.getDebugWarnings().add("Profiles exceeded the limit."); - metrics.updateProfileMetric(MetricName.limit_exceeded); + metrics.updateAccountProfileMetric(accountId, MetricName.limit_exceeded); } return profilesIds; @@ -217,15 +217,14 @@ private T applyProfiles(List profilesIds, for (String profileId : profilesIds) { try { final Profile profile = idToProfile.get(profileId); - result = profile != null - ? mergeProfile(result, profile, profileId) - : result; - } catch (InvalidProfileException e) { - metrics.updateProfileMetric(MetricName.invalid); - conditionalLogger.error(e.getMessage(), logSamplingRate); + result = profile != null ? mergeProfile(result, profile) : result; + } catch (InvalidRequestException e) { + final String message = "Can't merge with profile %s: %s".formatted(profileId, e.getMessage()); + metrics.updateProfileMetric(MetricName.invalid); + conditionalLogger.error(message, logSamplingRate); if (failOnUnknown) { - throw new InvalidProfileException(e.getMessage()); + throw new InvalidProfileException(message); } } } @@ -237,19 +236,15 @@ private T applyProfiles(List profilesIds, } } - private ObjectNode mergeProfile(ObjectNode original, Profile profile, String profileId) { + private ObjectNode mergeProfile(ObjectNode original, Profile profile) { return switch (profile.getMergePrecedence()) { - case REQUEST -> merge(original, profile.getBody(), profileId); - case PROFILE -> merge(profile.getBody(), original, profileId); + case REQUEST -> merge(original, profile.getBody()); + case PROFILE -> merge(profile.getBody(), original); }; } - private ObjectNode merge(JsonNode takePrecedence, JsonNode other, String profileId) { - try { - return (ObjectNode) jsonMerger.merge(takePrecedence, other); - } catch (InvalidRequestException e) { - throw new InvalidProfileException("Can't merge with profile %s: %s".formatted(profileId, e.getMessage())); - } + private ObjectNode merge(JsonNode takePrecedence, JsonNode other) { + return (ObjectNode) jsonMerger.merge(takePrecedence, other); } private List applyImpsProfiles(List> profilesIds, From 4db1d94afe5a71eec394124b00949a6a17dfd397 Mon Sep 17 00:00:00 2001 From: Danylo Date: Sun, 10 Aug 2025 03:31:27 +0200 Subject: [PATCH 17/24] Add units --- .../settings/FileApplicationSettings.java | 23 +- .../helper/DatabaseProfilesResultMapper.java | 5 +- .../DatabaseStoredDataResultMapper.java | 2 +- .../settings/helper/StoredItemResolver.java | 8 +- .../requestfactory/AmpRequestFactoryTest.java | 21 ++ .../AuctionRequestFactoryTest.java | 21 ++ .../Ortb2RequestFactoryTest.java | 47 ++-- .../org/prebid/server/metric/MetricsTest.java | 20 ++ .../CompositeApplicationSettingsTest.java | 152 ++++++++++- .../DatabaseApplicationSettingsTest.java | 56 +++- .../settings/FileApplicationSettingsTest.java | 243 ++++++++++++++---- .../helper/StoredItemResolverTest.java | 24 +- 12 files changed, 526 insertions(+), 96 deletions(-) diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java index d28497a83a5..abc183c40a7 100644 --- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java @@ -221,7 +221,7 @@ public Future> getProfiles(String accountId, final List errors = new ArrayList<>(); final Map requestProfiles = getProfiles(accountId, requestIds, Profile.Type.REQUEST, errors); - final Map impProfiles = getProfiles(accountId, requestIds, Profile.Type.IMP, errors); + final Map impProfiles = getProfiles(accountId, impIds, Profile.Type.IMP, errors); return Future.succeededFuture(StoredDataResult.of( requestProfiles, @@ -237,14 +237,10 @@ private Map getProfiles(String accountId, final Map result = new HashMap<>(); for (String id : ids) { - final Set> profiles = SetUtils.predicatedSet( - profileIdToProfile.getOrDefault(id, Collections.emptySet()), - storedItem -> storedItem.getData().getType() == type); + final Set> profiles = profilesOfTypeWithId(type, id); try { - final StoredItem profile = StoredItemResolver - .resolve(type.toString(), accountId, id, profiles); - + final StoredItem profile = StoredItemResolver.resolve("profile", accountId, id, profiles); result.put(id, profile.getData()); } catch (PreBidException e) { errors.add(e.getMessage()); @@ -254,6 +250,19 @@ private Map getProfiles(String accountId, return Collections.unmodifiableMap(result); } + private Set> profilesOfTypeWithId(Profile.Type type, String id) { + final Set> allProfiles = profileIdToProfile.get(id); + if (CollectionUtils.isEmpty(allProfiles) + || allProfiles.stream().allMatch(storedItem -> storedItem.getData().getType() == type)) { + + return allProfiles; + } + + return allProfiles.stream() + .filter(storedItem -> storedItem.getData().getType() == type) + .collect(Collectors.toSet()); + } + @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { if (CollectionUtils.isEmpty(responseIds)) { diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java index abba9c1b1c5..68c4ed30b24 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java @@ -101,13 +101,11 @@ public static StoredDataResult map(RowSet rowSet, return StoredDataResult.of( storedItemsOrAddError( - Profile.Type.REQUEST, accountId, requestIds, requestIdToProfiles, errors), storedItemsOrAddError( - Profile.Type.IMP, accountId, impIds, impIdToProfiles, @@ -138,7 +136,6 @@ private static void addStoredItem(String accountId, } private static Map storedItemsOrAddError( - Profile.Type type, String accountId, Set searchIds, Map>> foundIdToStoredItems, @@ -159,7 +156,7 @@ private static Map storedItemsOrAddError( for (String id : searchIds) { try { final StoredItem resolvedStoredItem = StoredItemResolver - .resolve(type.toString(), accountId, id, foundIdToStoredItems.get(id)); + .resolve("profile", accountId, id, foundIdToStoredItems.get(id)); result.put(id, resolvedStoredItem.getData()); } catch (PreBidException e) { diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java index a2c084569de..6e37850a903 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java @@ -151,7 +151,7 @@ private static Map storedItemsOrAddError(StoredDataType type, for (String id : searchIds) { try { final StoredItem resolvedStoredItem = StoredItemResolver - .resolve(type.toString(), accountId, id, foundIdToStoredItems.get(id)); + .resolve("stored " + type.toString(), accountId, id, foundIdToStoredItems.get(id)); result.put(id, resolvedStoredItem.getData()); } catch (PreBidException e) { diff --git a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java index 623dda6747c..d0797a4772f 100644 --- a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java +++ b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java @@ -33,7 +33,7 @@ private StoredItemResolver() { */ public static StoredItem resolve(String type, String accountId, String id, Set> storedItems) { if (CollectionUtils.isEmpty(storedItems)) { - throw new PreBidException("No stored %s found for id: %s".formatted(type, id)); + throw new PreBidException("No %s found for id: %s".formatted(type, id)); } // at least one stored item has account @@ -41,14 +41,14 @@ public static StoredItem resolve(String type, String accountId, String id if (StringUtils.isEmpty(accountId)) { // we cannot choose stored item among multiple without account throw new PreBidException( - "Multiple stored %ss found for id: %s but no account was specified".formatted(type, id)); + "Multiple %ss found for id: %s but no account was specified".formatted(type, id)); } return storedItems.stream() .filter(storedItem -> Objects.equals(storedItem.getAccountId(), accountId)) .findAny() .orElseThrow(() -> new PreBidException( - "No stored %s found among multiple id: %s for account: %s".formatted(type, id, accountId))); + "No %s found among multiple id: %s for account: %s".formatted(type, id, accountId))); } // only one stored item found @@ -60,6 +60,6 @@ public static StoredItem resolve(String type, String accountId, String id return storedItem; } - throw new PreBidException("No stored %s found for id: %s for account: %s".formatted(type, id, accountId)); + throw new PreBidException("No %s found for id: %s for account: %s".formatted(type, id, accountId)); } } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index 90ef1467b04..32de14a2390 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -1743,6 +1743,27 @@ public void shouldUpdateTimeout() { .isEqualTo(10000L); } + @Test + public void shouldUseProfilesResult() { + // given + givenBidRequest(); + + given(profilesProcessor.process(any(), any())).willAnswer( + invocation -> Future.succeededFuture(((BidRequest) invocation.getArgument(1)).toBuilder() + .source(Source.builder().tid("uniqTid").build()) + .build())); + + // when + final Future future = target.fromRequest(routingContext, 0L); + + // then + assertThat(future).isSucceeded(); + assertThat(future.result()) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getSource) + .isEqualTo(Source.builder().tid("uniqTid").build()); + } + private void givenBidRequest(UnaryOperator storedBidRequestBuilderCustomizer, Imp... imps) { diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 2e8579fecec..2e30e8d1bc4 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -812,6 +812,27 @@ public void shouldUpdateTimeout() { .isEqualTo(10000L); } + @Test + public void shouldUseProfilesResult() { + // given + givenValidBidRequest(); + + given(profilesProcessor.process(any(), any())).willAnswer( + invocation -> Future.succeededFuture(((BidRequest) invocation.getArgument(1)).toBuilder() + .source(Source.builder().tid("uniqTid").build()) + .build())); + + // when + target.enrichAuctionContext(defaultActionContext); + + // then + verify(paramsResolver).resolve( + argThat(bidRequest -> bidRequest.getSource().equals(Source.builder().tid("uniqTid").build())), + any(), + any(), + anyBoolean()); + } + private void givenBidRequest(BidRequest bidRequest) { try { given(requestBody.asString()).willReturn(mapper.writeValueAsString(bidRequest)); diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index 649216852f7..37c3964b713 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -547,40 +547,53 @@ public void shouldFetchAccountFromStoredAndReturnFailedFutureIfStoredLookupIsFai } @Test - public void shouldFetchAccountFromStoredAndReturnEmptyAccountIfStoredLookupIsFailed() { - // given - final BidRequest receivedBidRequest = givenBidRequest(identity()); - given(storedRequestProcessor.processAuctionRequest(any(), any())) - .willReturn(Future.failedFuture(new RuntimeException("error"))); - + public void fetchAccountWithoutStoredRequestLookupShouldNeverCallStoredProcessor() { // when - final Future result = target.fetchAccount( + target.fetchAccountWithoutStoredRequestLookup( AuctionContext.builder() .httpRequest(httpRequest) - .bidRequest(receivedBidRequest) + .bidRequest(givenBidRequest(identity())) .timeoutContext(TimeoutContext.of(0, null, 0)) .build()); // then - verify(storedRequestProcessor).processAuctionRequest("", receivedBidRequest); - verifyNoInteractions(applicationSettings); - - assertThat(result.failed()).isTrue(); - assertThat(result.cause()).hasMessage("error"); + verifyNoInteractions(storedRequestProcessor); } @Test - public void fetchAccountWithoutStoredRequestLookupShouldNeverCallStoredProcessor() { + public void shouldFetchAccountFromProfileIfStoredLookupIsTrueAndAccountIsNotFoundPreviously() { + // given + final BidRequest receivedBidRequest = givenBidRequest(identity()); + + final String accountId = "accountId"; + final BidRequest mergedBidRequest = givenBidRequest(builder -> builder + .site(Site.builder() + .publisher(Publisher.builder().id(accountId).build()) + .build())); + + given(storedRequestProcessor.processAuctionRequest(any(), any())) + .willAnswer(invocation -> Future.succeededFuture( + AuctionStoredResult.of(false, invocation.getArgument(1)))); + given(profilesProcessor.process(any(), any())) + .willReturn(Future.succeededFuture(mergedBidRequest)); + + final Account fetchedAccount = Account.builder().id(accountId).status(AccountStatus.active).build(); + given(applicationSettings.getAccountById(eq(accountId), any())) + .willReturn(Future.succeededFuture(fetchedAccount)); + // when - target.fetchAccountWithoutStoredRequestLookup( + final Future result = target.fetchAccount( AuctionContext.builder() .httpRequest(httpRequest) - .bidRequest(givenBidRequest(identity())) + .bidRequest(receivedBidRequest) .timeoutContext(TimeoutContext.of(0, null, 0)) .build()); // then - verifyNoInteractions(storedRequestProcessor); + verify(storedRequestProcessor).processAuctionRequest("", receivedBidRequest); + verify(applicationSettings).getAccountById(eq(accountId), any()); + + assertThat(result.result()).isEqualTo(fetchedAccount); } @Test diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 73e9b902168..fc6510e3529 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -1571,6 +1571,26 @@ public void shouldIncrementPrebidCacheCreativeTtlHistogram() { .isEqualTo(1); } + @Test + public void shouldIncrementUpdateProfileMetric() { + // when + metrics.updateProfileMetric(MetricName.limit_exceeded); + + // then + assertThat(metricRegistry.counter("profiles.limit_exceeded").getCount()) + .isEqualTo(1); + } + + @Test + public void shouldIncrementUpdateAccountProfileMetric() { + // when + metrics.updateAccountProfileMetric("accountId", MetricName.limit_exceeded); + + // then + assertThat(metricRegistry.counter("account.accountId.profiles.limit_exceeded").getCount()) + .isEqualTo(1); + } + private void verifyCreatesConfiguredCounterType(Consumer metricsConsumer) { final EnumMap> counterTypeClasses = new EnumMap<>(CounterType.class); counterTypeClasses.put(CounterType.counter, Counter.class); diff --git a/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java index 890f73e0e21..f140a0428c6 100644 --- a/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java @@ -1,5 +1,6 @@ package org.prebid.server.settings; +import com.fasterxml.jackson.databind.node.TextNode; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,6 +11,7 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; @@ -422,6 +424,147 @@ public void getAmpStoredDataShouldReturnResultConsequentlyFromAllDelegates() { entry("key2", "value2")); } + @Test + public void getProfilesShouldReturnResultFromFirstDelegateIfPresent() { + // given + given(delegate1.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of( + singletonMap("key1", givenProfile("value1")), + singletonMap("key2", givenProfile("value2")), + emptyList()))); + + // when + final Future> future = + compositeApplicationSettings.getProfiles(null, singleton("key1"), singleton("key2"), null); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isNotNull(); + assertThat(future.result().getErrors()).isEmpty(); + assertThat(future.result().getStoredIdToRequest()).hasSize(1) + .containsOnly(entry("key1", givenProfile("value1"))); + assertThat(future.result().getStoredIdToImp()).hasSize(1) + .containsOnly(entry("key2", givenProfile("value2"))); + verifyNoInteractions(delegate2); + } + + @Test + public void getProfilesShouldReturnResultFromFromSecondDelegateIfFirstDelegateFails() { + // given + given(delegate1.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error1")))); + + given(delegate2.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of( + singletonMap("key1", givenProfile("value1")), + singletonMap("key2", givenProfile("value2")), + emptyList()))); + + // when + final Future> future = + compositeApplicationSettings.getProfiles(null, singleton("key1"), singleton("key2"), null); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isNotNull(); + assertThat(future.result().getErrors()).isEmpty(); + assertThat(future.result().getStoredIdToRequest()).hasSize(1) + .containsOnly(entry("key1", givenProfile("value1"))); + assertThat(future.result().getStoredIdToImp()).hasSize(1) + .containsOnly(entry("key2", givenProfile("value2"))); + } + + @Test + public void getProfilesShouldReturnEmptyResultIfAllDelegatesFail() { + // given + given(delegate1.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error1")))); + + given(delegate2.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error2")))); + + // when + final Future> future = + compositeApplicationSettings.getProfiles(null, singleton("key1"), emptySet(), null); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result().getStoredIdToRequest()).isEmpty(); + assertThat(future.result().getErrors()).hasSize(1) + .containsOnly("error2"); + } + + @Test + public void getProfilesShouldPassOnlyMissingIdsToSecondDelegateIfFirstDelegateAlreadyObtainedThey() { + // given + given(delegate1.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of( + singletonMap("key1", givenProfile("value1")), + singletonMap("key3", givenProfile("value3")), + singletonList("error1")))); + + // when + compositeApplicationSettings.getProfiles( + null, + new HashSet<>(asList("key1", "key2")), + new HashSet<>(asList("key3", "key4")), + null); + + // then + final ArgumentCaptor> requestCaptor = ArgumentCaptor.captor(); + final ArgumentCaptor> impCaptor = ArgumentCaptor.captor(); + verify(delegate2).getProfiles(any(), requestCaptor.capture(), impCaptor.capture(), any()); + + assertThat(requestCaptor.getValue()).hasSize(1) + .containsOnly("key2"); + assertThat(impCaptor.getValue()).hasSize(1) + .containsOnly("key4"); + } + + @Test + public void getProfilesShouldReturnResultConsequentlyFromAllDelegates() { + // given + given(delegate1.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of( + singletonMap("key1", givenProfile("value1")), + singletonMap("key3", givenProfile("value3")), + asList("key2 not found", "key4 not found")))); + + given(delegate2.getProfiles(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of( + singletonMap("key2", givenProfile("value2")), + singletonMap("key4", givenProfile("value4")), + emptyList()))); + + // when + final Future> future = + compositeApplicationSettings.getProfiles( + null, + new HashSet<>(asList("key1", "key2")), + new HashSet<>(asList("key3", "key4")), + null); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result().getErrors()).isEmpty(); + assertThat(future.result().getStoredIdToRequest()).hasSize(2) + .containsOnly( + entry("key1", givenProfile("value1")), + entry("key2", givenProfile("value2"))); + assertThat(future.result().getStoredIdToImp()).hasSize(2) + .containsOnly( + entry("key3", givenProfile("value3")), + entry("key4", givenProfile("value4"))); + } + @Test public void getStoredResponsesShouldReturnResultFromFirstDelegateIfPresent() { // given @@ -497,7 +640,7 @@ public void getStoredResponsesShouldPassOnlyMissingIdsToSecondDelegateIfFirstDel compositeApplicationSettings.getStoredResponses(new HashSet<>(asList("key1", "key2")), null); // then - final ArgumentCaptor> responseCaptor = ArgumentCaptor.forClass(Set.class); + final ArgumentCaptor> responseCaptor = ArgumentCaptor.captor(); verify(delegate2).getStoredResponses(responseCaptor.capture(), any()); assertThat(responseCaptor.getValue()).hasSize(1).containsOnly("key2"); @@ -526,4 +669,11 @@ public void getStoredResponsesShouldReturnResultConsequentlyFromAllDelegates() { entry("key1", "value1"), entry("key2", "value2")); } + + private static Profile givenProfile(String value) { + return Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf(value)); + } } diff --git a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java index c0a9730c48f..7cfdb193461 100644 --- a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java @@ -1,5 +1,6 @@ package org.prebid.server.settings; +import com.fasterxml.jackson.databind.node.TextNode; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,6 +13,7 @@ import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.settings.helper.ParametrizedQueryHelper; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; import org.prebid.server.vertx.database.DatabaseClient; @@ -186,6 +188,28 @@ public void getVideoStoredDataShouldReturnExpectedResult() { assertThat(future.result()).isEqualTo(givenStoredDataResult); } + @Test + public void getProfilesShouldReturnExpectedResult() { + // given + given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_PROFILES_QUERY, 2, 2)) + .willReturn("query"); + + final StoredDataResult givenProfilesResult = StoredDataResult.of( + Map.of("1", givenProfile("value1"), "2", givenProfile("value2")), + Map.of("4", givenProfile("value4"), "5", givenProfile("value5")), + emptyList()); + given(databaseClient.executeQuery(eq("query"), eq(List.of("1", "2", "4", "5")), any(), eq(timeout))) + .willReturn(Future.succeededFuture(givenProfilesResult)); + + // when + final Future> future = target.getProfiles( + "1001", new HashSet<>(asList("1", "2")), new HashSet<>(asList("4", "5")), timeout); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(givenProfilesResult); + } + @Test public void getStoredDataShouldReturnResultWithError() { // given @@ -252,6 +276,28 @@ public void getVideoStoredDataShouldReturnResultWithError() { assertThat(future.result()).isEqualTo(givenStoredDataResult); } + @Test + public void getProfilesShouldReturnResultWithError() { + // given + given(parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders(SELECT_PROFILES_QUERY, 2, 0)) + .willReturn("query"); + + final StoredDataResult givenProfilesResult = StoredDataResult.of( + Map.of("1", givenProfile("value1")), + Map.of(), + List.of("No stored request found for id: 3")); + given(databaseClient.executeQuery(eq("query"), eq(List.of("1", "3")), any(), eq(timeout))) + .willReturn(Future.succeededFuture(givenProfilesResult)); + + // when + final Future> future = + target.getProfiles("1001", new HashSet<>(asList("1", "3")), emptySet(), timeout); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(givenProfilesResult); + } + @Test public void getStoredResponseShouldReturnExpectedResult() { // given @@ -276,12 +322,18 @@ public void getStoredResponseShouldReturnExpectedResult() { @Test public void getCategoriesShouldReturnFailedFutureWithUnsupportedPrebidException() { // given and when - final Future> result = target.getCategories("adServer", "publisher", - timeout); + final Future> result = target.getCategories("adServer", "publisher", timeout); // then assertThat(result.failed()).isTrue(); assertThat(result.cause()).isInstanceOf(PreBidException.class) .hasMessage("Not supported"); } + + private static Profile givenProfile(String value) { + return Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf(value)); + } } diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index 506132da036..5c4bd81dff9 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -1,5 +1,7 @@ package org.prebid.server.settings; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.TextNode; import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; import io.vertx.core.file.FileSystem; @@ -23,6 +25,7 @@ import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.EnforcePurpose; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.Purpose; import org.prebid.server.settings.model.PurposeOneTreatmentInterpretation; import org.prebid.server.settings.model.Purposes; @@ -35,6 +38,7 @@ import java.util.Map; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; @@ -98,51 +102,76 @@ public void getAccountByIdShouldReturnEmptyWhenAccountsAreMissing() { public void getAccountByIdShouldReturnPresentAccount() { // given given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer( - "accounts: [" - + "{" - + "id: 123," - + "status: active," - + "auction: {" - + "price-granularity: low," - + "banner-cache-ttl: 100," - + "video-cache-ttl : 100," - + "truncate-target-attr: 20," - + "default-integration: web," - + "bid-validations: {" - + "banner-creative-max-size: enforce" - + "}," - + "events: {" - + "enabled: true" - + "}" - + "}," - + "privacy: {" - + "gdpr: {" - + "enabled: true," - + "channel-enabled: {" - + "amp: true," - + "web: true," - + "video: true," - + "app: true," - + "dooh: true" - + "}," - + "purposes: {" - + "p1: {enforce-purpose: basic,enforce-vendors: false,vendor-exceptions: [rubicon, appnexus]}," - + "p2: {enforce-purpose: full,enforce-vendors: true,vendor-exceptions: [openx]}" - + "}," - + "special-features: {" - + "sf1: {enforce: true,vendor-exceptions: [rubicon, appnexus]}," - + "sf2: {enforce: false,vendor-exceptions: [openx]}" - + "}," - + "purpose-one-treatment-interpretation: access-allowed" - + "}" - + "}," - + "analytics: {" - + "auction-events: {amp: true}," - + "modules: {some-analytics: {supported-endpoints: [auction]}}" - + "}," - + "cookie-sync: {default-limit: 5,max-limit: 8, coop-sync: {default: true}}" - + "}" - + "]")); + """ + accounts: [{ + id: 123, + status: active, + auction: { + price-granularity: low, + banner-cache-ttl: 100, + video-cache-ttl : 100, + truncate-target-attr: 20, + default-integration: web, + bid-validations: { + banner-creative-max-size: enforce + }, + events: { + enabled: true + } + }, + privacy: { + gdpr: { + enabled: true, + channel-enabled: { + amp: true, + web: true, + video: true, + app: true, + dooh: true + }, + purposes: { + p1: { + enforce-purpose: basic, + enforce-vendors: false, + vendor-exceptions: [rubicon, appnexus] + }, + p2: { + enforce-purpose: full, + enforce-vendors: true, + vendor-exceptions: [openx] + } + }, + special-features: { + sf1: { + enforce: true, + vendor-exceptions: [rubicon, appnexus] + }, + sf2: { + enforce: false, + vendor-exceptions: [openx] + } + }, + purpose-one-treatment-interpretation: access-allowed + } + }, + analytics: { + auction-events: { + amp: true + }, + modules: { + some-analytics: { + supported-endpoints: [auction] + } + } + }, + cookie-sync: { + default-limit: 5, + max-limit: 8, + coop-sync: { + default: true + } + } + }]""")); final FileApplicationSettings applicationSettings = new FileApplicationSettings( fileSystem, @@ -474,6 +503,109 @@ public void getStoredDataShouldReturnResultWithNoErrorsIfAllIdsArePresent() { .isEqualTo(singletonMap("2", "value2")); } + @Test + public void getProfilesShouldReturnResultWithNotFoundErrorForNonExistingIds() throws JsonProcessingException { + // given + given(fileSystem.readDirBlocking(eq("ignore"))) + .willReturn(emptyList()); + given(fileSystem.readFileBlocking(eq("settings"))) + .willReturn(Buffer.buffer("accounts:")); + + given(fileSystem.readDirBlocking(eq("profiles"))) + .willReturn(asList( + "/home/user/profiles/1-1.json", + "/home/user/profiles/1-2.json", + "/home/user/profiles/2-1.json")); + + given(fileSystem.readFileBlocking(eq("/home/user/profiles/1-1.json"))) + .willReturn(Buffer.buffer(mapper.writeValueAsString(Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("value1"))))); + given(fileSystem.readFileBlocking(eq("/home/user/profiles/1-2.json"))) + .willReturn(Buffer.buffer(mapper.writeValueAsString(Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("value2"))))); + given(fileSystem.readFileBlocking(eq("/home/user/profiles/2-1.json"))) + .willReturn(Buffer.buffer(mapper.writeValueAsString(Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("value3"))))); + + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "settings", + "ignore", + "ignore", + "profiles", + "ignore", + "ignore", + jacksonMapper); + + // when + final Future> storedRequestResult = + applicationSettings.getProfiles(null, singleton("1"), singleton("3"), null); + + // then + assertThat(storedRequestResult.succeeded()).isTrue(); + assertThat(storedRequestResult.result().getErrors()) + .containsExactly( + "Multiple profiles found for id: 1 but no account was specified", + "No profile found for id: 3"); + } + + @Test + public void getProfilesShouldFilterForAccountAndType() throws JsonProcessingException { + // given + given(fileSystem.readDirBlocking(eq("ignore"))) + .willReturn(emptyList()); + given(fileSystem.readFileBlocking(eq("settings"))) + .willReturn(Buffer.buffer("accounts:")); + + given(fileSystem.readDirBlocking(eq("profiles"))) + .willReturn(asList( + "/home/user/profiles/1-1.json", + "/home/user/profiles/1-2.json", + "/home/user/profiles/2-1.json")); + + given(fileSystem.readFileBlocking(eq("/home/user/profiles/1-1.json"))) + .willReturn(Buffer.buffer(mapper.writeValueAsString(Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("value1"))))); + given(fileSystem.readFileBlocking(eq("/home/user/profiles/1-2.json"))) + .willReturn(Buffer.buffer(mapper.writeValueAsString(Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("value2"))))); + given(fileSystem.readFileBlocking(eq("/home/user/profiles/2-1.json"))) + .willReturn(Buffer.buffer(mapper.writeValueAsString(Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("value3"))))); + + final FileApplicationSettings applicationSettings = new FileApplicationSettings( + fileSystem, + "settings", + "ignore", + "ignore", + "profiles", + "ignore", + "ignore", + jacksonMapper); + + // when + final Future> storedRequestResult = + applicationSettings.getProfiles("1", singleton("1"), singleton("2"), null); + + // then + assertThat(storedRequestResult.succeeded()).isTrue(); + assertThat(storedRequestResult.result().getStoredIdToRequest()).containsOnlyKeys("1"); + assertThat(storedRequestResult.result().getStoredIdToImp()).containsOnlyKeys("2"); + assertThat(storedRequestResult.result().getErrors()).isEmpty(); + } + @Test public void getStoredResponsesShouldReturnEmptyResultAndErrorsWhenResponseIdsAreEmpty() { // given @@ -613,4 +745,25 @@ public void storedDataInitializationShouldNotReadFromNonJsonFiles() { // then verify(fileSystem, never()).readFileBlocking(eq("1.txt")); } + + @Test + public void profilesInitializationShouldNotReadFromNonJsonFiles() { + // given + given(fileSystem.readDirBlocking(anyString())).willReturn(singletonList("/home/user/profiles/1.txt")); + given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("accounts:")); // settings file + + // when + new FileApplicationSettings( + fileSystem, + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + jacksonMapper); + + // then + verify(fileSystem, never()).readFileBlocking(eq("1.txt")); + } } diff --git a/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java b/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java index 820b3717ac7..039deac061b 100644 --- a/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java +++ b/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.Test; import org.prebid.server.exception.PreBidException; -import org.prebid.server.settings.model.StoredDataType; import org.prebid.server.settings.model.StoredItem; import java.util.HashSet; @@ -18,7 +17,7 @@ public class StoredItemResolverTest { public void resolveShouldFailWhenNoStoredData() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), null, "id", emptySet())) + .isThrownBy(() -> StoredItemResolver.resolve("stored imp", null, "id", emptySet())) .withMessage("No stored imp found for id: id"); } @@ -29,7 +28,7 @@ public void resolveShouldFailWhenMultipleStoredDataButNoAccountInRequest() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), null, "id", storedItems)) + .isThrownBy(() -> StoredItemResolver.resolve("stored imp", null, "id", storedItems)) .withMessage("Multiple stored imps found for id: id but no account was specified"); } @@ -40,7 +39,7 @@ public void resolveShouldFailWhenMultipleStoredDataButAccountDiffers() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), "1003", "id", storedItems)) + .isThrownBy(() -> StoredItemResolver.resolve("stored imp", "1003", "id", storedItems)) .withMessage("No stored imp found among multiple id: id for account: 1003"); } @@ -50,8 +49,7 @@ public void resolveShouldReturnResultWhenMultipleStoredDataForAppropriateAccount final Set> storedItems = givenMultipleStoredData(); // when - final StoredItem storedItem = StoredItemResolver.resolve( - StoredDataType.imp.name(), "1002", "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve("stored imp", "1002", "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of("1002", "data2")); @@ -64,8 +62,7 @@ public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInRequest() storedItems.add(StoredItem.of("1001", "data1")); // when - final StoredItem storedItem = StoredItemResolver.resolve( - StoredDataType.imp.name(), "1001", "", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve("stored imp", "1001", "", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of("1001", "data1")); @@ -78,8 +75,7 @@ public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInStoredDat storedItems.add(StoredItem.of(null, "data1")); // when - final StoredItem storedItem = StoredItemResolver.resolve( - StoredDataType.imp.name(), "1001", "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve("stored imp", "1001", "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of(null, "data1")); @@ -92,8 +88,7 @@ public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountBothInReque storedItems.add(StoredItem.of(null, "data1")); // when - final StoredItem storedItem = StoredItemResolver.resolve( - StoredDataType.imp.name(), null, "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve("stored imp", null, "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of(null, "data1")); @@ -105,8 +100,7 @@ public void resolveShouldFailWhenSingleStoredDataForAppropriateAccount() { final Set> storedItems = givenSingleStoredData(); // when - final StoredItem storedItem = StoredItemResolver.resolve( - StoredDataType.imp.name(), "1001", "id", storedItems); + final StoredItem storedItem = StoredItemResolver.resolve("stored imp", "1001", "id", storedItems); // then assertThat(storedItem).isEqualTo(StoredItem.of("1001", "data1")); @@ -119,7 +113,7 @@ public void resolveShouldFailWhenSingleStoredDataButAccountDiffers() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp.name(), "1002", "id", storedItems)) + .isThrownBy(() -> StoredItemResolver.resolve("stored imp", "1002", "id", storedItems)) .withMessage("No stored imp found for id: id for account: 1002"); } From c9263e2a32eb4338d45d4c5ac084472d5d0f99e3 Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 12 Aug 2025 00:38:59 +0200 Subject: [PATCH 18/24] Add units --- .../helper/DatabaseProfilesResultMapper.java | 11 +- .../DatabaseProfilesResultMapperTest.java | 365 ++++++++++++++++++ .../DatabaseStoredDataResultMapperTest.java | 31 +- 3 files changed, 387 insertions(+), 20 deletions(-) create mode 100644 src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java index 68c4ed30b24..4078418c33f 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java @@ -5,6 +5,7 @@ import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowIterator; import io.vertx.sqlclient.RowSet; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.log.Logger; @@ -73,17 +74,17 @@ public static StoredDataResult map(RowSet rowSet, final String fetchedAccountId = Objects.toString(row.getValue(0), null); final String id = Objects.toString(row.getValue(1), null); - final String profileBodyAsString = Objects.toString(row.getValue(2), null); - final String mergePrecedenceAsString = Objects.toString(row.getValue(3), null); - final String typeAsString = Objects.toString(row.getValue(4), null); + final String profileBodyAsString = Objects.toString(row.getValue(2), StringUtils.EMPTY); + final String mergePrecedenceAsString = Objects.toString(row.getValue(3), StringUtils.EMPTY); + final String typeAsString = Objects.toString(row.getValue(4), StringUtils.EMPTY); final JsonNode profileBody; final Profile.MergePrecedence mergePrecedence; final Profile.Type type; try { profileBody = ObjectMapperProvider.mapper().readTree(profileBodyAsString); - mergePrecedence = Profile.MergePrecedence.valueOf(mergePrecedenceAsString); - type = Profile.Type.valueOf(typeAsString); + mergePrecedence = Profile.MergePrecedence.valueOf(mergePrecedenceAsString.toUpperCase()); + type = Profile.Type.valueOf(typeAsString.toUpperCase()); } catch (IllegalArgumentException | JsonProcessingException e) { logger.error("Profile with id={} has invalid value: ''{}'' and will be ignored.", e, id, typeAsString); diff --git a/src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java b/src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java new file mode 100644 index 00000000000..c3b8d2532eb --- /dev/null +++ b/src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java @@ -0,0 +1,365 @@ +package org.prebid.server.settings.helper; + +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import lombok.Value; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.settings.model.Profile; +import org.prebid.server.settings.model.StoredDataResult; + +import java.util.Arrays; +import java.util.Iterator; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; +import static org.mockito.quality.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class DatabaseProfilesResultMapperTest extends VertxTest { + + @Mock + private RowSet rowSet; + + @Test + public void mapShouldReturnEmptyProfilesWithErrorWhenResultSetHasEmptyResult() { + // given + givenRowSet(); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map(rowSet, null, emptySet(), emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactly("No profiles were found"); + } + + @Test + public void mapShouldReturnEmptyProfilesWithErrorWhenResultSetHasEmptyResultForGivenIds() { + // given + givenRowSet(); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + null, + singleton("reqId"), + singleton("impId")); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactly("No request profiles for ids [reqId] and imp profiles for ids [impId] were found"); + } + + @Test + public void mapShouldReturnEmptyProfilesWithErrorWhenResultSetHasLessColumns() { + // given + givenRowSet(givenRow("accountId", "id1", "data", "request")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("reqId"), + emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactly("Error occurred while mapping profiles: some columns are missing"); + } + + @Test + public void mapShouldReturnEmptyProfilesWithErrorWhenResultSetHasUnexpectedColumnType() { + // given + givenRowSet(givenRow("accountId", "id1", "{}", 123, "request")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("reqId"), + emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactly("No profile found for id: reqId"); + } + + @Test + public void mapShouldSkipProfileWithInvalidBody() { + // given + givenRowSet( + givenRow("accountId", "id1", "{}", "request", "request"), + givenRow("accountId", "id1", "invalid", "request", "request")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("id1"), + emptySet()); + + // then + assertThat(result.getStoredIdToRequest()) + .containsExactly(entry("id1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode()))); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void mapShouldSkipProfileWithInvalidMergePrecedence() { + // given + givenRowSet( + givenRow("accountId", "id1", "{}", "request", "request"), + givenRow("accountId", "id1", "{}", "invalid", "request")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("id1"), + emptySet()); + + // then + assertThat(result.getStoredIdToRequest()) + .containsExactly(entry("id1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode()))); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void mapShouldSkipProfileWithInvalidType() { + // given + givenRowSet( + givenRow("accountId", "id1", "{}", "request", "request"), + givenRow("accountId", "id1", "{}", "request", "invalid")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("id1"), + emptySet()); + + // then + assertThat(result.getStoredIdToRequest()) + .containsExactly(entry("id1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode()))); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void mapShouldReturnProfileWithErrorForMissingId() { + // given + givenRowSet(givenRow("accountId", "id1", "{}", "request", "request")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("id1"), + singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()) + .containsExactly(entry("id1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode()))); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactly("No profile found for id: id2"); + } + + @Test + public void mapShouldReturnEmptyProfileWithErrorsForMissingIdsIfAccountDiffers() { + // given + givenRowSet( + givenRow("accountId", "id1", "{}", "request", "request"), + givenRow("accountId", "id2", "{}", "request", "imp")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "otherAccountId", + singleton("id1"), + singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactlyInAnyOrder( + "No profile found for id: id1 for account: otherAccountId", + "No profile found for id: id2 for account: otherAccountId"); + } + + @Test + public void mapShouldReturnEmptyProfileWithErrorIfMultipleStoredItemsFoundButNoAccountIdIsDefined() { + // given + givenRowSet( + givenRow("accountId1", "id1", "{}", "request", "request"), + givenRow("accountId2", "id1", "{}", "request", "request")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + null, + singleton("id1"), + emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactly("Multiple profiles found for id: id1 but no account was specified"); + } + + @Test + public void mapShouldReturnEmptyProfileWithErrorIfMultipleStoredItemsFoundButNoAccountIdIsDiffers() { + // given + givenRowSet( + givenRow("accountId1", "id1", "{}", "request", "request"), + givenRow("accountId2", "id1", "{}", "request", "request"), + givenRow("accountId1", "id2", "{}", "request", "imp"), + givenRow("accountId2", "id2", "{}", "request", "imp")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "otherAccountId", + singleton("id1"), + singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactlyInAnyOrder( + "No profile found among multiple id: id1 for account: otherAccountId", + "No profile found among multiple id: id2 for account: otherAccountId"); + } + + @Test + public void mapShouldReturnExpectedProfileForGivenAccount() { + // given + givenRowSet( + givenRow("accountId", "id1", "{}", "request", "request"), + givenRow("otherAccountId", "id1", "{}", "request", "request"), + givenRow("accountId", "id2", "{}", "profile", "imp"), + givenRow("otherAccountId", "id2", "{}", "request", "imp")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("id1"), + singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()) + .containsExactly(entry("id1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode()))); + assertThat(result.getStoredIdToImp()) + .containsExactly(entry("id2", Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.PROFILE, + mapper.createObjectNode()))); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void mapWithoutParamsShouldReturnEmptyProfileWithErrorWhenResultSetHasEmptyResult() { + // given + givenRowSet(); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map(rowSet); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()) + .containsExactly("No profiles were found"); + } + + @Test + public void mapWithoutParamsShouldReturnExpectedProfile() { + // given + givenRowSet( + givenRow("accountId", "id1", "{}", "request", "request"), + givenRow("accountId", "id2", "{}", "profile", "imp")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map(rowSet); + + // then + assertThat(result.getStoredIdToRequest()) + .containsExactly(entry("id1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode()))); + assertThat(result.getStoredIdToImp()) + .containsExactly(entry("id2", Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.PROFILE, + mapper.createObjectNode()))); + assertThat(result.getErrors()).isEmpty(); + } + + private void givenRowSet(Row... rows) { + given(rowSet.iterator()).willReturn(CustomRowIterator.of(Arrays.asList(rows).iterator())); + } + + private Row givenRow(Object... values) { + final Row row = mock(Row.class, withSettings().strictness(LENIENT)); + given(row.getValue(anyInt())).willAnswer(invocation -> values[(Integer) invocation.getArgument(0)]); + given(row.size()).willReturn(values.length); + return row; + } + + @Value(staticConstructor = "of") + private static class CustomRowIterator implements RowIterator { + + Iterator delegate; + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Row next() { + return delegate.next(); + } + } +} diff --git a/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java b/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java index 3b323ce86da..622e5d549bd 100644 --- a/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java +++ b/src/test/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapperTest.java @@ -35,7 +35,8 @@ public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasEmptyResult givenRowSet(); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet, null, emptySet(), emptySet()); + final StoredDataResult result = DatabaseStoredDataResultMapper.map( + rowSet, null, emptySet(), emptySet()); // then assertThat(result.getStoredIdToRequest()).isEmpty(); @@ -50,7 +51,7 @@ public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasEmptyResult givenRowSet(); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, null, singleton("reqId"), @@ -69,7 +70,7 @@ public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasLessColumns givenRowSet(givenRow("accountId", "id1", "data")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, "accountId", singleton("reqId"), @@ -88,7 +89,7 @@ public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasUnexpectedC givenRowSet(givenRow("accountId", "id1", "data", 123)); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, "accountId", singleton("reqId"), @@ -109,7 +110,7 @@ public void mapShouldSkipStoredResultWithInvalidType() { givenRow("accountId", "id1", "data2", "invalid")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, "accountId", singleton("id1"), @@ -128,7 +129,7 @@ public void mapShouldReturnStoredResultWithErrorForMissingId() { givenRowSet(givenRow("accountId", "id1", "data1", "request")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, "accountId", singleton("id1"), @@ -150,7 +151,7 @@ public void mapShouldReturnEmptyStoredResultWithErrorsForMissingIdsIfAccountDiff givenRow("accountId", "id2", "data2", "imp")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, "otherAccountId", singleton("id1"), @@ -173,7 +174,7 @@ public void mapShouldReturnEmptyStoredResultWithErrorIfMultipleStoredItemsFoundB givenRow("accountId2", "id1", "data2", "request")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, null, singleton("id1"), @@ -196,7 +197,7 @@ public void mapShouldReturnEmptyStoredResultWithErrorIfMultipleStoredItemsFoundB givenRow("accountId2", "id2", "data-otherAccountId", "imp")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, "otherAccountId", singleton("id1"), @@ -221,7 +222,7 @@ public void mapShouldReturnExpectedStoredResultForGivenAccount() { givenRow("otherAccountId", "id2", "data-otherAccountId", "imp")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map( + final StoredDataResult result = DatabaseStoredDataResultMapper.map( rowSet, "accountId", singleton("id1"), @@ -241,7 +242,7 @@ public void mapWithoutParamsShouldReturnEmptyStoredResultWithErrorWhenResultSetH givenRowSet(); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); + final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); // then assertThat(result.getStoredIdToRequest()).isEmpty(); @@ -258,7 +259,7 @@ public void mapWithoutParamsShouldSkipStoredResultWithInvalidType() { givenRow("accountId", "id2", "data2", "invalid")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); + final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); // then assertThat(result.getStoredIdToRequest()).hasSize(1) @@ -273,7 +274,7 @@ public void mapWithoutParamsShouldReturnEmptyStoredResultWithErrorWhenResultSetH givenRowSet(givenRow("accountId", "id1", "data")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); + final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); // then assertThat(result.getStoredIdToRequest()).isEmpty(); @@ -288,7 +289,7 @@ public void mapWithoutParamsShouldReturnEmptyStoredResultWhenResultSetHasInvalid givenRowSet(givenRow("accountId", "id1", "data", 123)); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); + final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); // then assertThat(result.getStoredIdToRequest()).isEmpty(); @@ -304,7 +305,7 @@ public void mapWithoutParamsShouldReturnExpectedStoredResult() { givenRow("accountId", "id2", "data2", "imp")); // when - final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); + final StoredDataResult result = DatabaseStoredDataResultMapper.map(rowSet); // then assertThat(result.getStoredIdToRequest()).hasSize(1) From 334ee98b9ee58fb68cd0b11257b7d56bf0211a41 Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 12 Aug 2025 02:23:02 +0200 Subject: [PATCH 19/24] Add units --- .../externalortb/ProfilesProcessor.java | 49 +- .../externalortb/ProfilesProcessorTest.java | 470 ++++++++++++++++++ 2 files changed, 510 insertions(+), 9 deletions(-) create mode 100644 src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index ecbba69ddae..d0ffb69e180 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -85,11 +85,21 @@ public Future process(AuctionContext auctionContext, BidRequest bidR return Future.succeededFuture(bidRequest); } + final boolean failOnUnknown = isFailOnUnknown(auctionContext.getAccount()); + return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest)) - .compose(profiles -> emitMetrics(accountId, profiles, auctionContext)) + .compose(profiles -> emitMetrics(accountId, profiles, auctionContext, failOnUnknown)) .map(profiles -> mergeResults( - applyRequestProfiles(profilesIds.request(), profiles.getStoredIdToRequest(), bidRequest), - applyImpsProfiles(profilesIds.imps(), profiles.getStoredIdToImp(), bidRequest.getImp()))) + applyRequestProfiles( + profilesIds.request(), + profiles.getStoredIdToRequest(), + bidRequest, + failOnUnknown), + applyImpsProfiles( + profilesIds.imps(), + profiles.getStoredIdToImp(), + bidRequest.getImp(), + failOnUnknown))) .recover(e -> Future.failedFuture( new InvalidRequestException("Error during processing profiles: " + e.getMessage()))); } @@ -161,6 +171,14 @@ private long timeoutMillis(BidRequest bidRequest) { return tmax != null && tmax > 0 ? tmax : defaultTimeoutMillis; } + private boolean isFailOnUnknown(Account account) { + return Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getProfiles) + .map(AccountProfilesConfig::getFailOnUnknown) + .orElse(failOnUnknown); + } + private Future> fetchProfiles(String accountId, AllProfilesIds allProfilesIds, long timeoutMillis) { @@ -176,7 +194,8 @@ private Future> fetchProfiles(String accountId, private Future> emitMetrics(String accountId, StoredDataResult fetchResult, - AuctionContext auctionContext) { + AuctionContext auctionContext, + boolean failOnUnknown) { final List errors = fetchResult.getErrors(); if (!errors.isEmpty()) { @@ -197,17 +216,19 @@ private Future> emitMetrics(String accountId, private BidRequest applyRequestProfiles(List profilesIds, Map idToRequestProfile, - BidRequest bidRequest) { + BidRequest bidRequest, + boolean failOnUnknown) { return !idToRequestProfile.isEmpty() - ? applyProfiles(profilesIds, idToRequestProfile, bidRequest, BidRequest.class) + ? applyProfiles(profilesIds, idToRequestProfile, bidRequest, BidRequest.class, failOnUnknown) : bidRequest; } private T applyProfiles(List profilesIds, Map idToProfile, T original, - Class tClass) { + Class tClass, + boolean failOnUnknown) { if (profilesIds.isEmpty()) { return original; @@ -244,12 +265,17 @@ private ObjectNode mergeProfile(ObjectNode original, Profile profile) { } private ObjectNode merge(JsonNode takePrecedence, JsonNode other) { + if (!takePrecedence.isObject() || !other.isObject()) { + throw new InvalidRequestException("One of the merge arguments is not an object."); + } + return (ObjectNode) jsonMerger.merge(takePrecedence, other); } private List applyImpsProfiles(List> profilesIds, Map idToImpProfile, - List imps) { + List imps, + boolean failOnUnknown) { if (idToImpProfile.isEmpty()) { return imps; @@ -257,7 +283,12 @@ private List applyImpsProfiles(List> profilesIds, final List updatedImps = new ArrayList<>(imps); for (int i = 0; i < profilesIds.size(); i++) { - updatedImps.set(i, applyProfiles(profilesIds.get(i), idToImpProfile, imps.get(i), Imp.class)); + updatedImps.set(i, applyProfiles( + profilesIds.get(i), + idToImpProfile, + imps.get(i), + Imp.class, + failOnUnknown)); } return Collections.unmodifiableList(updatedImps); diff --git a/src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java b/src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java new file mode 100644 index 00000000000..2cb128cc1ba --- /dev/null +++ b/src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java @@ -0,0 +1,470 @@ +package org.prebid.server.auction.externalortb; + +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.proto.openrtb.ext.request.ExtImp; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountProfilesConfig; +import org.prebid.server.settings.model.Profile; +import org.prebid.server.settings.model.StoredDataResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class ProfilesProcessorTest extends VertxTest { + + @Mock(strictness = LENIENT) + private ApplicationSettings applicationSettings; + + @Mock(strictness = LENIENT) + private TimeoutFactory timeoutFactory; + + @Mock(strictness = LENIENT) + private Metrics metrics; + + private ProfilesProcessor target; + + @BeforeEach + public void setUp() { + given(applicationSettings.getProfiles(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of(emptyMap(), emptyMap(), emptyList()))); + + target = givenTarget(100, true); + } + + @Test + public void processShouldReturnSameBidRequestIfNoProfilesFound() { + // given + final AuctionContext auctionContext = givenAuctionContext(); + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Future result = target.process(auctionContext, bidRequest); + + // then + assertThat(result.result()).isSameAs(bidRequest); + verifyNoInteractions(applicationSettings, timeoutFactory, metrics); + } + + @Test + public void processShouldFetchExpectedProfiles() { + // given + final AuctionContext auctionContext = givenAuctionContext(true); + final BidRequest bidRequest = withProfiles( + givenBidRequest( + identity(), + withProfiles(givenImp(identity()), "ip11", "ip12"), + withProfiles(givenImp(identity()), "ip21", "ip22"), + withProfiles(givenImp(identity()), "ip31", "ip32")), + "rp1", "rp2", "rp3"); + + // when + target.process(auctionContext, bidRequest); + + // then + assertThat(auctionContext.getDebugWarnings()).isEmpty(); + + verify(applicationSettings).getProfiles( + eq("accountId"), + eq(Set.of("rp1", "rp2", "rp3")), + eq(Set.of("ip11", "ip12", "ip21", "ip22", "ip31", "ip32")), + any()); + verifyNoInteractions(metrics); + } + + @Test + public void processShouldLimitImpProfiles() { + // given + target = givenTarget(4, true); + + final AuctionContext auctionContext = givenAuctionContext(true); + final BidRequest bidRequest = withProfiles( + givenBidRequest( + identity(), + withProfiles(givenImp(identity()), "ip11", "ip12"), + withProfiles(givenImp(identity())), + withProfiles(givenImp(identity()), "ip31")), + "rp1", "rp2", "rp3"); + + // when + target.process(auctionContext, bidRequest); + + // then + assertThat(auctionContext.getDebugWarnings()).containsExactly("Profiles exceeded the limit."); + + verify(applicationSettings).getProfiles( + eq("accountId"), + eq(Set.of("rp1", "rp2", "rp3")), + eq(Set.of("ip11", "ip31")), + any()); + verify(metrics).updateAccountProfileMetric(eq("accountId"), eq(MetricName.limit_exceeded)); + } + + @Test + public void processShouldLimitRequestProfiles() { + // given + target = givenTarget(2, true); + + final AuctionContext auctionContext = givenAuctionContext(true); + final BidRequest bidRequest = withProfiles( + givenBidRequest( + identity(), + withProfiles(givenImp(identity()), "ip11", "ip12"), + withProfiles(givenImp(identity()), "ip21", "ip22"), + withProfiles(givenImp(identity()), "ip31", "ip32")), + "rp1", "rp2", "rp3"); + + // when + target.process(auctionContext, bidRequest); + + // then + assertThat(auctionContext.getDebugWarnings()).containsExactly("Profiles exceeded the limit."); + + verify(applicationSettings).getProfiles( + eq("accountId"), + eq(Set.of("rp1", "rp2")), + eq(emptySet()), + any()); + verify(metrics).updateAccountProfileMetric(eq("accountId"), eq(MetricName.limit_exceeded)); + } + + @Test + public void processShouldLimitRequestProfilesUsingAccountConfig() { + // given + final AuctionContext auctionContext = givenAuctionContext(AccountProfilesConfig.of(2, null), true); + final BidRequest bidRequest = withProfiles( + givenBidRequest( + identity(), + withProfiles(givenImp(identity()), "ip11", "ip12"), + withProfiles(givenImp(identity()), "ip21", "ip22"), + withProfiles(givenImp(identity()), "ip31", "ip32")), + "rp1", "rp2", "rp3"); + + // when + target.process(auctionContext, bidRequest); + + // then + assertThat(auctionContext.getDebugWarnings()).containsExactly("Profiles exceeded the limit."); + + verify(applicationSettings).getProfiles( + eq("accountId"), + eq(Set.of("rp1", "rp2")), + eq(emptySet()), + any()); + verify(metrics).updateAccountProfileMetric(eq("accountId"), eq(MetricName.limit_exceeded)); + } + + @Test + public void processShouldFailAndEmitMetricsWhenDebugDisabledAndFailOnUnknownIsTrue() { + // given + given(applicationSettings.getProfiles(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of( + Map.of("rp1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode())), + emptyMap(), + singletonList("rp2 missed")))); + + final AuctionContext auctionContext = givenAuctionContext(false); + final BidRequest bidRequest = withProfiles(givenBidRequest(identity()), "rp1", "rp2"); + + // when + final Future result = target.process(auctionContext, bidRequest); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo("Error during processing profiles: rp2 missed"); + + verify(metrics).updateProfileMetric(eq(MetricName.missing)); + } + + @Test + public void processShouldFailAndEmitMetricsWhenDebugEnabledAndFailOnUnknownIsTrue() { + // given + given(applicationSettings.getProfiles(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of( + Map.of("rp1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode())), + emptyMap(), + singletonList("rp2 missed")))); + + final AuctionContext auctionContext = givenAuctionContext(true); + final BidRequest bidRequest = withProfiles(givenBidRequest(identity()), "rp1", "rp2"); + + // when + final Future result = target.process(auctionContext, bidRequest); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo("Error during processing profiles: rp2 missed"); + assertThat(auctionContext.getDebugWarnings()).containsExactly("rp2 missed"); + + verify(metrics).updateProfileMetric(eq(MetricName.missing)); + verify(metrics).updateAccountProfileMetric(eq("accountId"), eq(MetricName.missing)); + } + + @Test + public void processShouldIgnoreErrorsAndEmitMetricsWhenDebugEnabledAndFailOnUnknownIsFalse() { + // given + target = givenTarget(100, false); + + given(applicationSettings.getProfiles(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of( + Map.of("rp1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode())), + emptyMap(), + singletonList("rp2 missed")))); + + final AuctionContext auctionContext = givenAuctionContext(true); + final BidRequest bidRequest = withProfiles(givenBidRequest(identity()), "rp1", "rp2"); + + // when + final Future result = target.process(auctionContext, bidRequest); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(auctionContext.getDebugWarnings()).containsExactly("rp2 missed"); + + verify(metrics).updateProfileMetric(eq(MetricName.missing)); + verify(metrics).updateAccountProfileMetric(eq("accountId"), eq(MetricName.missing)); + } + + @Test + public void processShouldIgnoreErrorsUsingAccountConfig() { + // given + given(applicationSettings.getProfiles(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of( + Map.of("rp1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode())), + emptyMap(), + singletonList("rp2 missed")))); + + final AuctionContext auctionContext = givenAuctionContext(AccountProfilesConfig.of(100, false), true); + final BidRequest bidRequest = withProfiles(givenBidRequest(identity()), "rp1", "rp2"); + + // when + final Future result = target.process(auctionContext, bidRequest); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(auctionContext.getDebugWarnings()).containsExactly("rp2 missed"); + + verify(metrics).updateProfileMetric(eq(MetricName.missing)); + verify(metrics).updateAccountProfileMetric(eq("accountId"), eq(MetricName.missing)); + } + + @Test + public void processShouldFailOnInvalidProfileIfFailOnUnknown() { + // given + given(applicationSettings.getProfiles(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of( + Map.of( + "rp1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.valueToTree(givenBidRequest(request -> request.test(1).at(1)))), + "rp2", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("invalid")), + "rp3", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + mapper.valueToTree(givenBidRequest(request -> request.id("newRequestId"))))), + emptyMap(), + emptyList()))); + + final AuctionContext auctionContext = givenAuctionContext(true); + final BidRequest bidRequest = withProfiles( + givenBidRequest( + request -> request.id("requestId").test(0), + withProfiles(givenImp(identity()), "ip11", "ip12"), + withProfiles(givenImp(identity()), "ip21", "ip22"), + withProfiles(givenImp(identity()), "ip31", "ip32")), + "rp1", "rp2", "rp3"); + + // when + final Future result = target.process(auctionContext, bidRequest); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo(""" + Error during processing profiles: \ + Can't merge with profile rp2: \ + One of the merge arguments is not an object."""); + + verify(metrics).updateProfileMetric(eq(MetricName.invalid)); + } + + @Test + public void processShouldReturnExpectedResult() { + // given + given(applicationSettings.getProfiles(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of( + Map.of( + "rp1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.valueToTree(givenBidRequest(request -> request.test(1).at(1)))), + "rp2", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + TextNode.valueOf("invalid")), + "rp3", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.PROFILE, + mapper.valueToTree(givenBidRequest(request -> request.id("newRequestId"))))), + Map.of( + "ip11", Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.REQUEST, + mapper.valueToTree(givenImp(imp -> imp.id("id11")))), + "ip12", Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.REQUEST, + TextNode.valueOf("invalid")), + "ip13", Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.REQUEST, + mapper.valueToTree(givenImp(imp -> imp.id("id13")))), + "ip21", Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.PROFILE, + mapper.valueToTree(givenImp(imp -> imp.id("id21")))), + "ip22", Profile.of( + Profile.Type.IMP, + Profile.MergePrecedence.PROFILE, + mapper.valueToTree(givenImp(imp -> imp.id("id22"))))), + emptyList()))); + + final AuctionContext auctionContext = givenAuctionContext(AccountProfilesConfig.of(100, false), true); + final BidRequest bidRequest = withProfiles( + givenBidRequest( + request -> request.id("requestId").test(0), + withProfiles(givenImp(identity()), "ip11", "ip12", "ip13"), + withProfiles(givenImp(identity()), "ip21", "ip22")), + "rp1", "rp2", "rp3"); + + // when + final Future result = target.process(auctionContext, bidRequest); + + // then + assertThat(result.result()).satisfies(request -> { + assertThat(request.getId()).isEqualTo("newRequestId"); + assertThat(request.getTest()).isEqualTo(0); + assertThat(request.getAt()).isEqualTo(1); + + assertThat(request.getImp()) + .extracting(Imp::getId) + .containsExactly("id11", "id22"); + }); + + verify(metrics, times(2)).updateProfileMetric(eq(MetricName.invalid)); + } + + private ProfilesProcessor givenTarget(int maxProfiles, boolean failOnUnknown) { + return new ProfilesProcessor( + maxProfiles, + 100L, + failOnUnknown, + 0.0, + applicationSettings, + timeoutFactory, + metrics, + jacksonMapper, + new JsonMerger(jacksonMapper)); + } + + private static AuctionContext givenAuctionContext() { + return givenAuctionContext(false); + } + + private static AuctionContext givenAuctionContext(boolean debugEnabled) { + return givenAuctionContext(null, debugEnabled); + } + + private static AuctionContext givenAuctionContext(AccountProfilesConfig profilesConfig, boolean debugEnabled) { + return AuctionContext.builder() + .account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .profiles(profilesConfig) + .build()) + .build()) + .debugContext(DebugContext.of(debugEnabled, false, null)) + .debugWarnings(new ArrayList<>()) + .build(); + } + + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { + + return bidRequestCustomizer.apply(BidRequest.builder().imp(List.of(imps))).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder()).build(); + } + + private static BidRequest withProfiles(BidRequest bidRequest, String... profiles) { + return bidRequest.toBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .profiles(List.of(profiles)) + .build())) + .build(); + } + + private static Imp withProfiles(Imp imp, String... profiles) { + return imp.toBuilder() + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .profiles(List.of(profiles)) + .build(), + null))) + .build(); + } +} From f4f9496fbdb1fd9dca191c18ed6fe464ca6cac65 Mon Sep 17 00:00:00 2001 From: Danylo Date: Wed, 27 Aug 2025 16:55:48 +0200 Subject: [PATCH 20/24] Fix message --- .../settings/helper/DatabaseProfilesResultMapper.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java index 4078418c33f..ad92b274228 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java @@ -85,9 +85,13 @@ public static StoredDataResult map(RowSet rowSet, profileBody = ObjectMapperProvider.mapper().readTree(profileBodyAsString); mergePrecedence = Profile.MergePrecedence.valueOf(mergePrecedenceAsString.toUpperCase()); type = Profile.Type.valueOf(typeAsString.toUpperCase()); - } catch (IllegalArgumentException | JsonProcessingException e) { - logger.error("Profile with id={} has invalid value: ''{}'' and will be ignored.", - e, id, typeAsString); + } catch (IllegalArgumentException e) { + logger.error("Profile with id={} has invalid value: type={}, mergePrecedence={} and will be ignored.", + e, id, typeAsString, mergePrecedenceAsString); + continue; + } catch (JsonProcessingException e) { + logger.error("Profile with id={} has invalid body: ''{}'' and will be ignored.", + e, id, profileBodyAsString); continue; } From 44cf214be67e54eadf76cecce647f24a5654dba6 Mon Sep 17 00:00:00 2001 From: Danylo Date: Thu, 28 Aug 2025 19:11:19 +0200 Subject: [PATCH 21/24] Fix equality bug --- .../externalortb/ProfilesProcessor.java | 10 +++---- .../externalortb/ProfilesProcessorTest.java | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index d0ffb69e180..c7bb281a0b3 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -151,15 +151,13 @@ private ExtImpPrebid parseImpExt(JsonNode jsonNode) { private static AllProfilesIds truncate(AllProfilesIds profilesIds, int maxProfiles) { final List requestProfiles = profilesIds.request(); - final int impProfilesLimit = maxProfiles - requestProfiles.size(); + final int impProfilesLimit = Math.max(0, maxProfiles - requestProfiles.size()); - return impProfilesLimit > 0 - ? new AllProfilesIds( - requestProfiles, + return new AllProfilesIds( + truncate(requestProfiles, maxProfiles), profilesIds.imps().stream() .map(impProfiles -> truncate(impProfiles, impProfilesLimit)) - .toList()) - : new AllProfilesIds(truncate(requestProfiles, maxProfiles), Collections.emptyList()); + .toList()); } private static List truncate(List list, int maxSize) { diff --git a/src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java b/src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java index 2cb128cc1ba..f639bcce611 100644 --- a/src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/externalortb/ProfilesProcessorTest.java @@ -191,6 +191,32 @@ public void processShouldLimitRequestProfilesUsingAccountConfig() { verify(metrics).updateAccountProfileMetric(eq("accountId"), eq(MetricName.limit_exceeded)); } + @Test + public void processShouldHandleCorrectlyWhenLimitEqualsRequestProfilesCount() { + // given + final AuctionContext auctionContext = givenAuctionContext(AccountProfilesConfig.of(2, null), true); + final BidRequest bidRequest = withProfiles( + givenBidRequest( + identity(), + withProfiles(givenImp(identity())), + withProfiles(givenImp(identity())), + withProfiles(givenImp(identity()))), + "rp1", "rp2"); + + // when + target.process(auctionContext, bidRequest); + + // then + assertThat(auctionContext.getDebugWarnings()).isEmpty(); + + verify(applicationSettings).getProfiles( + eq("accountId"), + eq(Set.of("rp1", "rp2")), + eq(emptySet()), + any()); + verifyNoInteractions(metrics); + } + @Test public void processShouldFailAndEmitMetricsWhenDebugDisabledAndFailOnUnknownIsTrue() { // given From 56c705d7de62f6d5031c0b3967eaf7671179e107 Mon Sep 17 00:00:00 2001 From: Danylo Date: Tue, 2 Sep 2025 19:49:11 +0200 Subject: [PATCH 22/24] Add default value for `MergePrecedence` --- .../helper/DatabaseProfilesResultMapper.java | 6 +++-- .../prebid/server/settings/model/Profile.java | 17 ++++++++++++-- .../DatabaseProfilesResultMapperTest.java | 22 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java index ad92b274228..65471ef30ab 100644 --- a/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java @@ -75,7 +75,7 @@ public static StoredDataResult map(RowSet rowSet, final String fetchedAccountId = Objects.toString(row.getValue(0), null); final String id = Objects.toString(row.getValue(1), null); final String profileBodyAsString = Objects.toString(row.getValue(2), StringUtils.EMPTY); - final String mergePrecedenceAsString = Objects.toString(row.getValue(3), StringUtils.EMPTY); + final String mergePrecedenceAsString = Objects.toString(row.getValue(3), null); final String typeAsString = Objects.toString(row.getValue(4), StringUtils.EMPTY); final JsonNode profileBody; @@ -83,7 +83,9 @@ public static StoredDataResult map(RowSet rowSet, final Profile.Type type; try { profileBody = ObjectMapperProvider.mapper().readTree(profileBodyAsString); - mergePrecedence = Profile.MergePrecedence.valueOf(mergePrecedenceAsString.toUpperCase()); + mergePrecedence = mergePrecedenceAsString != null + ? Profile.MergePrecedence.valueOf(mergePrecedenceAsString.toUpperCase()) + : Profile.MergePrecedence.REQUEST; type = Profile.Type.valueOf(typeAsString.toUpperCase()); } catch (IllegalArgumentException e) { logger.error("Profile with id={} has invalid value: type={}, mergePrecedence={} and will be ignored.", diff --git a/src/main/java/org/prebid/server/settings/model/Profile.java b/src/main/java/org/prebid/server/settings/model/Profile.java index 96fde5a584f..11b46fc43d1 100644 --- a/src/main/java/org/prebid/server/settings/model/Profile.java +++ b/src/main/java/org/prebid/server/settings/model/Profile.java @@ -3,18 +3,31 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; import lombok.Value; +import lombok.extern.jackson.Jacksonized; -@Value(staticConstructor = "of") +@Value +@Builder +@Jacksonized public class Profile { Type type; @JsonProperty("mergeprecedence") - MergePrecedence mergePrecedence; + @Builder.Default + MergePrecedence mergePrecedence = MergePrecedence.REQUEST; JsonNode body; + public static Profile of(Type type, MergePrecedence mergePrecedence, JsonNode body) { + return Profile.builder() + .type(type) + .mergePrecedence(mergePrecedence) + .body(body) + .build(); + } + public enum Type { @JsonAlias("request") diff --git a/src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java b/src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java index c3b8d2532eb..52012d8c8dc 100644 --- a/src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java +++ b/src/test/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapperTest.java @@ -151,6 +151,28 @@ public void mapShouldSkipProfileWithInvalidMergePrecedence() { assertThat(result.getErrors()).isEmpty(); } + @Test + public void mapShouldUseDefaultMergePrecedence() { + // given + givenRowSet(givenRow("accountId", "id1", "{}", null, "request")); + + // when + final StoredDataResult result = DatabaseProfilesResultMapper.map( + rowSet, + "accountId", + singleton("id1"), + emptySet()); + + // then + assertThat(result.getStoredIdToRequest()) + .containsExactly(entry("id1", Profile.of( + Profile.Type.REQUEST, + Profile.MergePrecedence.REQUEST, + mapper.createObjectNode()))); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + @Test public void mapShouldSkipProfileWithInvalidType() { // given From 1cbf5df6bf7e2b9d76a653db189d179950e81208 Mon Sep 17 00:00:00 2001 From: Danylo Date: Wed, 3 Sep 2025 15:18:02 +0200 Subject: [PATCH 23/24] Refactor --- .../server/auction/externalortb/ProfilesProcessor.java | 10 ++++------ .../auction/requestfactory/Ortb2RequestFactory.java | 4 ++-- .../server/settings/DatabaseApplicationSettings.java | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java index c7bb281a0b3..4f8603a8287 100644 --- a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -100,8 +100,8 @@ public Future process(AuctionContext auctionContext, BidRequest bidR profiles.getStoredIdToImp(), bidRequest.getImp(), failOnUnknown))) - .recover(e -> Future.failedFuture( - new InvalidRequestException("Error during processing profiles: " + e.getMessage()))); + .recover(error -> Future.failedFuture( + new InvalidRequestException("Error during processing profiles: " + error.getMessage()))); } private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auctionContext, String accountId) { @@ -218,14 +218,13 @@ private BidRequest applyRequestProfiles(List profilesIds, boolean failOnUnknown) { return !idToRequestProfile.isEmpty() - ? applyProfiles(profilesIds, idToRequestProfile, bidRequest, BidRequest.class, failOnUnknown) + ? applyProfiles(profilesIds, idToRequestProfile, bidRequest, failOnUnknown) : bidRequest; } private T applyProfiles(List profilesIds, Map idToProfile, T original, - Class tClass, boolean failOnUnknown) { if (profilesIds.isEmpty()) { @@ -249,7 +248,7 @@ private T applyProfiles(List profilesIds, } try { - return mapper.mapper().treeToValue(result, tClass); + return mapper.mapper().treeToValue(result, (Class) original.getClass()); } catch (JsonProcessingException e) { throw new InvalidProfileException(e.getMessage()); } @@ -285,7 +284,6 @@ private List applyImpsProfiles(List> profilesIds, profilesIds.get(i), idToImpProfile, imps.get(i), - Imp.class, failOnUnknown)); } diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index d66aabcc8e3..ac9619f6b83 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -482,7 +482,7 @@ private Future findAccountIdFrom(AuctionContext auctionContext, return Future.succeededFuture(accountId); } - return accountIdFromStored(bidRequest) + return accountIdFromStoredRequest(bidRequest) .compose(id -> StringUtils.isBlank(id) ? accountIdFromProfiles(auctionContext, bidRequest) : Future.succeededFuture(id)); @@ -511,7 +511,7 @@ private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; } - private Future accountIdFromStored(BidRequest bidRequest) { + private Future accountIdFromStoredRequest(BidRequest bidRequest) { return storedRequestProcessor.processAuctionRequest(StringUtils.EMPTY, bidRequest) .map(AuctionStoredResult::bidRequest) .map(this::accountIdFromBidRequest); diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java index 371edfcb0e3..ae15aeb1561 100644 --- a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -18,6 +18,7 @@ import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.util.ObjectUtil; import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient; import org.prebid.server.vertx.database.DatabaseClient; @@ -129,7 +130,7 @@ private T mapToModelOrError(RowSet rowSet, Function mapper) { } private Account toAccount(Row row) { - final String source = Objects.toString(row.getValue(0), null); + final String source = ObjectUtil.getIfNotNull(row.getValue(0), Object::toString); try { return source != null ? mapper.decodeValue(source, Account.class) : null; } catch (DecodeException e) { From 855f129fcf822197d27bee120b24dec65d31facd Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:10:10 +0300 Subject: [PATCH 24/24] Tests: Add profiles (#4056) --- .../model/config/AccountAuctionConfig.groovy | 1 + .../config/AccountProfilesConfigs.groovy | 17 + .../model/db/StoredProfileImp.groovy | 46 + .../model/db/StoredProfileRequest.groovy | 46 + .../functional/model/db/StoredRequest.groovy | 4 +- .../functional/model/db/StoredResponse.groovy | 4 +- ...y => BidRequestConfigTypeConverter.groovy} | 2 +- ... => BidResponseConfigTypeConverter.groovy} | 2 +- .../ProfileMergePrecedenceConvert.groovy | 17 + .../typeconverter/ProfileTypeConvert.groovy | 17 + .../FileSystemAccountsConfig.groovy | 10 + .../model/request/auction/Banner.groovy | 2 + .../request/auction/BidRequestExt.groovy | 2 + .../model/request/auction/Device.groovy | 13 + .../model/request/auction/Format.groovy | 8 + .../model/request/auction/ImpExtPrebid.groovy | 3 +- .../model/request/auction/Prebid.groovy | 2 + .../model/request/auction/Site.groovy | 2 + .../model/request/auction/SiteExtData.groovy | 2 + .../model/request/profile/ImpProfile.groovy | 26 + .../model/request/profile/Profile.groovy | 24 + .../profile/ProfileMergePrecedence.groovy | 28 + .../model/request/profile/ProfileType.groovy | 28 + .../request/profile/RequestProfile.groovy | 40 + .../model/response/auction/BidResponse.groovy | 4 +- .../HibernateRepositoryService.groovy | 12 + .../repository/dao/ProfileImpDao.groovy | 11 + .../repository/dao/ProfileRequestDao.groovy | 11 + .../testcontainers/PbsConfig.groovy | 3 +- .../container/PrebidServerContainer.groovy | 9 + .../functional/tests/ProfileSpec.groovy | 1426 +++++++++++++++++ .../util/ObjectMapperWrapper.groovy | 6 + .../server/functional/util/PBSUtils.groovy | 8 + .../server/functional/db_mysql_schema.sql | 9 + 34 files changed, 1836 insertions(+), 9 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy rename src/test/groovy/org/prebid/server/functional/model/db/typeconverter/{StoredRequestConfigTypeConverter.groovy => BidRequestConfigTypeConverter.groovy} (81%) rename src/test/groovy/org/prebid/server/functional/model/db/typeconverter/{StoredBidResponseConfigTypeConverter.groovy => BidResponseConfigTypeConverter.groovy} (81%) create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index 21a60bef192..2dc5ff7c77b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -26,6 +26,7 @@ class AccountAuctionConfig { AccountCacheConfig cache AccountRankingConfig ranking AccountPriceFloorsConfig priceFloors + AccountProfilesConfigs profiles Targeting targeting PaaFormat paaformat @JsonProperty("preferredmediatype") diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy new file mode 100644 index 00000000000..71852279fb8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AccountProfilesConfigs { + + Integer limit + Boolean failOnUnknown + + @JsonProperty("fail_on_unknown") + Boolean failOnUnknownSnakeCase +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy new file mode 100644 index 00000000000..19f87ac0d9c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy @@ -0,0 +1,46 @@ +package org.prebid.server.functional.model.db + +import groovy.transform.ToString +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.prebid.server.functional.model.db.typeconverter.ImpConfigTypeConverter +import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert +import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.profile.ImpProfile +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.ProfileType + +@Entity +@Table(name = "profiles") +@ToString(includeNames = true) +class StoredProfileImp { + + @Id + @Column(name = "profileId") + String profileName + @Column(name = "accountId") + String accountId + @Column(name = "mergePrecedence") + @Convert(converter = ProfileMergePrecedenceConvert) + ProfileMergePrecedence mergePrecedence + @Column(name = "type") + @Convert(converter = ProfileTypeConvert) + ProfileType type + @Column(name = "profile") + @Convert(converter = ImpConfigTypeConverter) + Imp impBody + + static StoredProfileImp getProfile(ImpProfile profile) { + new StoredProfileImp().tap { + it.profileName = profile.id + it.accountId = profile.accountId + it.mergePrecedence = profile.mergePrecedence + it.type = profile.type + it.impBody = profile.body + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy new file mode 100644 index 00000000000..8b04143d368 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy @@ -0,0 +1,46 @@ +package org.prebid.server.functional.model.db + +import groovy.transform.ToString +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert +import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert +import org.prebid.server.functional.model.db.typeconverter.BidRequestConfigTypeConverter +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.RequestProfile +import org.prebid.server.functional.model.request.profile.ProfileType + +@Entity +@Table(name = "profiles") +@ToString(includeNames = true) +class StoredProfileRequest { + + @Id + @Column(name = "profileId") + String profileName + @Column(name = "accountId") + String accountId + @Column(name = "mergePrecedence") + @Convert(converter = ProfileMergePrecedenceConvert) + ProfileMergePrecedence mergePrecedence + @Column(name = "type") + @Convert(converter = ProfileTypeConvert) + ProfileType type + @Column(name = "profile") + @Convert(converter = BidRequestConfigTypeConverter) + BidRequest requestBody + + static StoredProfileRequest getProfile(RequestProfile profile) { + new StoredProfileRequest().tap { + it.profileName = profile.id + it.accountId = profile.accountId + it.mergePrecedence = profile.mergePrecedence + it.type = profile.type + it.requestBody = profile.body + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy index 69264aa04eb..5bf47b830fc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy @@ -7,7 +7,7 @@ import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.Table -import org.prebid.server.functional.model.db.typeconverter.StoredRequestConfigTypeConverter +import org.prebid.server.functional.model.db.typeconverter.BidRequestConfigTypeConverter import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest @@ -27,7 +27,7 @@ class StoredRequest { @Column(name = "reqId") String requestId @Column(name = "requestData") - @Convert(converter = StoredRequestConfigTypeConverter) + @Convert(converter = BidRequestConfigTypeConverter) BidRequest requestData static StoredRequest getStoredRequest(AmpRequest ampRequest, BidRequest storedRequest) { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy index b57e793d22e..ebfc31f3c6d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy @@ -8,7 +8,7 @@ import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.Table import org.prebid.server.functional.model.db.typeconverter.StoredAuctionResponseConfigTypeConverter -import org.prebid.server.functional.model.db.typeconverter.StoredBidResponseConfigTypeConverter +import org.prebid.server.functional.model.db.typeconverter.BidResponseConfigTypeConverter import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.SeatBid @@ -29,6 +29,6 @@ class StoredResponse { @Convert(converter = StoredAuctionResponseConfigTypeConverter) SeatBid storedAuctionResponse @Column(name = "storedBidResponse") - @Convert(converter = StoredBidResponseConfigTypeConverter) + @Convert(converter = BidResponseConfigTypeConverter) BidResponse storedBidResponse } diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy similarity index 81% rename from src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy rename to src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy index 3e968d39565..b3761640226 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy @@ -4,7 +4,7 @@ import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.util.ObjectMapperWrapper -class StoredRequestConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { +class BidRequestConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { @Override String convertToDatabaseColumn(BidRequest bidRequest) { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy similarity index 81% rename from src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy rename to src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy index 43120fcad65..789d57e045e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy @@ -4,7 +4,7 @@ import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.ObjectMapperWrapper -class StoredBidResponseConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { +class BidResponseConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { @Override String convertToDatabaseColumn(BidResponse bidResponse) { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy new file mode 100644 index 00000000000..2b347cd9521 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence + +class ProfileMergePrecedenceConvert implements AttributeConverter { + + @Override + String convertToDatabaseColumn(ProfileMergePrecedence profileMergePrecedence) { + profileMergePrecedence?.value + } + + @Override + ProfileMergePrecedence convertToEntityAttribute(String value) { + value ? ProfileMergePrecedence.forValue(value) : null + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy new file mode 100644 index 00000000000..5c5565385f1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.request.profile.ProfileType + +class ProfileTypeConvert implements AttributeConverter { + + @Override + String convertToDatabaseColumn(ProfileType profileMergePrecedence) { + profileMergePrecedence?.value + } + + @Override + ProfileType convertToEntityAttribute(String value) { + value ? ProfileType.forValue(value) : null + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy new file mode 100644 index 00000000000..6851ece5527 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.filesystem + +import groovy.transform.ToString +import org.prebid.server.functional.model.config.AccountConfig + +@ToString(includeNames = true, ignoreNulls = true) +class FileSystemAccountsConfig { + + List accounts +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy index b4d6c23f4f5..32bd83365f9 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class Banner { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy index c253291dbf8..3c39de5781e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.bidder.AppNexus +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class BidRequestExt { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy index fbce486ac47..91a7e54dc37 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.model.request.auction import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) class Device { @@ -38,4 +39,16 @@ class Device { String macsha1 String macmd5 DeviceExt ext + + static Device getDefault() { + new Device().tap { + didsha1 = PBSUtils.randomString + didmd5 = PBSUtils.randomString + dpidsha1 = PBSUtils.randomString + ifa = PBSUtils.randomString + macsha1 = PBSUtils.randomString + macmd5 = PBSUtils.randomString + dpidmd5 = PBSUtils.randomString + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy index f6d1798ca57..326ba14ed5d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy @@ -3,6 +3,7 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils @EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @@ -25,4 +26,11 @@ class Format { height = 250 } } + + static Format getRandomFormat() { + new Format().tap { + weight = PBSUtils.randomNumber + height = PBSUtils.randomNumber + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy index 6ff83b53b9c..a20e3ea894d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy @@ -21,7 +21,8 @@ class ImpExtPrebid { Map imp String adUnitCode PrebidOptions options - + @JsonProperty("profiles") + List profileNames static ImpExtPrebid getDefaultImpExtPrebid() { new ImpExtPrebid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index 499dccea5a3..3ab6e7a6dbf 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -45,6 +45,8 @@ class Prebid { PaaFormat paaFormat @JsonProperty("alternatebiddercodes") AlternateBidderCodes alternateBidderCodes + @JsonProperty("profiles") + List profileNames static class Channel { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy index b74d83ff8fb..c8dfcbdbe79 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy @@ -2,9 +2,11 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class Site { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy index 87b5e19c21c..7e8ecd556fa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class SiteExtData { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy new file mode 100644 index 00000000000..aa63358fb24 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy @@ -0,0 +1,26 @@ +package org.prebid.server.functional.model.request.profile + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.util.PBSUtils + +import static ProfileMergePrecedence.PROFILE + +@ToString(includeNames = true, ignoreNulls = true) +class ImpProfile extends Profile { + + static ImpProfile getProfile(String accountId = PBSUtils.randomNumber.toString(), + Imp imp = Imp.defaultImpression, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + + new ImpProfile().tap { + it.accountId = accountId + it.id = name + it.type = ProfileType.IMP + it.mergePrecedence = mergePrecedence + it.body = imp + it.accountId = accountId + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy new file mode 100644 index 00000000000..2bb6387da77 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty + +abstract class Profile { + + @JsonIgnore + String accountId + @JsonIgnore + String id + ProfileType type + @JsonProperty("mergeprecedence") + ProfileMergePrecedence mergePrecedence + T body + + String getRecordName() { + "${accountId}-${id}" + } + + String getFileName() { + "${recordName}.json" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy new file mode 100644 index 00000000000..80227015989 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum ProfileMergePrecedence { + + EMPTY(""), + REQUEST("request"), + PROFILE("profile"), + UNKNOWN("unknown") + + private final String value + + ProfileMergePrecedence(String value) { + this.value = value + } + + @JsonValue + String getValue() { + name().toLowerCase() + } + + static ProfileMergePrecedence forValue(String value) { + values().find { it.value == value } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy new file mode 100644 index 00000000000..6254976300f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum ProfileType { + + EMPTY(""), + REQUEST("request"), + IMP("imp"), + UNKNOWN("unknown") + + private final String value + + ProfileType(String value) { + this.value = value + } + + @JsonValue + String getValue() { + name().toLowerCase() + } + + static ProfileType forValue(String value) { + values().find { it.value == value } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy new file mode 100644 index 00000000000..62d36bddc38 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy @@ -0,0 +1,40 @@ +package org.prebid.server.functional.model.request.profile + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.util.PBSUtils + +import static ProfileMergePrecedence.PROFILE + +@ToString(includeNames = true, ignoreNulls = true) +class RequestProfile extends Profile { + + static RequestProfile getProfile(String accountId = PBSUtils.randomNumber.toString(), + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + BidRequest request = BidRequest.defaultBidRequest.tap { + it.id = null + it.imp = null + it.site = Site.configFPDSite + it.device = Device.default + } + getProfile(accountId, request, name, mergePrecedence) + } + + static RequestProfile getProfile(String accountId, + BidRequest request, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + + new RequestProfile().tap { + it.accountId = accountId + it.id = name + it.type = ProfileType.REQUEST + it.mergePrecedence = mergePrecedence + it.body = request + it.accountId = accountId + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy index fab76cfc309..353f89c454e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.response.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency @@ -18,7 +19,8 @@ class BidResponse implements ResponseModel { String bidid Currency cur String customdata - NoBidResponse nbr + @JsonProperty("nbr") + NoBidResponse noBidResponse BidResponseExt ext static BidResponse getDefaultBidResponse(BidRequest bidRequest, BidderName bidderName = GENERIC) { diff --git a/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy b/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy index d6ee8d65c10..cd1b9706f79 100644 --- a/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy +++ b/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy @@ -3,10 +3,14 @@ package org.prebid.server.functional.repository import org.hibernate.SessionFactory import org.hibernate.cfg.Configuration import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.model.db.StoredProfileRequest import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.repository.dao.AccountDao +import org.prebid.server.functional.repository.dao.ProfileImpDao +import org.prebid.server.functional.repository.dao.ProfileRequestDao import org.prebid.server.functional.repository.dao.StoredImpDao import org.prebid.server.functional.repository.dao.StoredRequestDao import org.prebid.server.functional.repository.dao.StoredResponseDao @@ -23,6 +27,8 @@ class HibernateRepositoryService { StoredImpDao storedImpDao StoredRequestDao storedRequestDao StoredResponseDao storedResponseDao + ProfileImpDao profileImpDao + ProfileRequestDao profileRequestDao HibernateRepositoryService(JdbcDatabaseContainer container) { def jdbcUrl = container.jdbcUrl @@ -38,6 +44,8 @@ class HibernateRepositoryService { storedImpDao = new StoredImpDao(entityManagerUtil) storedRequestDao = new StoredRequestDao(entityManagerUtil) storedResponseDao = new StoredResponseDao(entityManagerUtil) + profileImpDao = new ProfileImpDao(entityManagerUtil) + profileRequestDao = new ProfileRequestDao(entityManagerUtil) } private static SessionFactory configureHibernate(String jdbcUrl, @@ -59,6 +67,8 @@ class HibernateRepositoryService { configuration.addAnnotatedClass(StoredImp) configuration.addAnnotatedClass(StoredRequest) configuration.addAnnotatedClass(StoredResponse) + configuration.addAnnotatedClass(StoredProfileImp) + configuration.addAnnotatedClass(StoredProfileRequest) SessionFactory sessionFactory = configuration.addProperties(properties).buildSessionFactory() sessionFactory @@ -69,5 +79,7 @@ class HibernateRepositoryService { storedImpDao.removeAll() storedRequestDao.removeAll() storedResponseDao.removeAll() + profileImpDao.removeAll() + profileRequestDao.removeAll() } } diff --git a/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy new file mode 100644 index 00000000000..00531ff9a7b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.repository.dao + +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.repository.EntityManagerUtil + +class ProfileImpDao extends EntityDao { + + ProfileImpDao(EntityManagerUtil entityManagerUtil) { + super(entityManagerUtil, StoredProfileImp) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy new file mode 100644 index 00000000000..455c64a3fd8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.repository.dao + +import org.prebid.server.functional.model.db.StoredProfileRequest +import org.prebid.server.functional.repository.EntityManagerUtil + +class ProfileRequestDao extends EntityDao { + + ProfileRequestDao(EntityManagerUtil entityManagerUtil) { + super(entityManagerUtil, StoredProfileRequest) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 052bcf2f69f..6a06a9f2e8a 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -38,7 +38,8 @@ LIMIT 1 "settings.database.account-query" : DB_ACCOUNT_QUERY, "settings.database.stored-requests-query" : "SELECT accountId, reqId, requestData, 'request' as dataType FROM stored_requests WHERE reqId IN (%REQUEST_ID_LIST%) UNION ALL SELECT accountId, impId, impData, 'imp' as dataType FROM stored_imps WHERE impId IN (%IMP_ID_LIST%)", "settings.database.amp-stored-requests-query": "SELECT accountId, reqId, requestData, 'request' as dataType FROM stored_requests WHERE reqId IN (%REQUEST_ID_LIST%)", - "settings.database.stored-responses-query" : "SELECT resId, COALESCE(storedAuctionResponse, storedBidResponse) as responseData FROM stored_responses WHERE resId IN (%RESPONSE_ID_LIST%)" + "settings.database.stored-responses-query" : "SELECT resId, COALESCE(storedAuctionResponse, storedBidResponse) as responseData FROM stored_responses WHERE resId IN (%RESPONSE_ID_LIST%)", + 'settings.database.profiles-query' : "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)" ].asImmutable() static Map getPubstackAnalyticsConfig(String scopeId) { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index b3f938a7ca0..e13fcae3764 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -5,6 +5,7 @@ import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.util.SystemProperties import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.images.builder.Transferable import static org.prebid.server.functional.testcontainers.PbsConfig.DEFAULT_ENV @@ -95,6 +96,14 @@ class PrebidServerContainer extends GenericContainer { .replace("]", "_") } + PrebidServerContainer withFolder(String containerPath) { + this.withCopyToContainer( + Transferable.of(new byte[0], 010755), + containerPath + "/.keep" + ) + return this + } + // This is a workaround for cases when container is killed mid-test due to OOM void refresh() { if (!running) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy new file mode 100644 index 00000000000..f0c39cf4d73 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -0,0 +1,1426 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountProfilesConfigs +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.model.db.StoredProfileRequest +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.filesystem.FileSystemAccountsConfig +import org.prebid.server.functional.model.request.auction.App +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Format +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.ImpExt +import org.prebid.server.functional.model.request.auction.ImpExtPrebid +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.request.profile.Profile +import org.prebid.server.functional.model.request.profile.ImpProfile +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.RequestProfile +import org.prebid.server.functional.model.request.profile.ProfileType +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.repository.dao.ProfileImpDao +import org.prebid.server.functional.repository.dao.ProfileRequestDao +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.container.PrebidServerContainer +import org.prebid.server.functional.util.PBSUtils +import org.testcontainers.images.builder.Transferable +import spock.lang.PendingFeature + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.PROFILE +import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.REQUEST +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO + +class ProfileSpec extends BaseSpec { + + private static final String PROFILES_PATH = '/app/prebid-server/profiles' + private static final String REQUESTS_PATH = '/app/prebid-server/requests' + private static final String IMPS_PATH = '/app/prebid-server/imps' + private static final String RESPONSES_PATH = '/app/prebid-server/responses' + private static final String CATEGORIES_PATH = '/app/prebid-server/categories' + private static final String SETTINGS_FILENAME = '/app/prebid-server/settings.yaml' + private static final Integer LIMIT_HOST_PROFILE = 2 + private static final Integer ACCOUNT_ID_FILE_STORAGE = PBSUtils.randomNumber + + private static final Map FILESYSTEM_CONFIG = [ + 'settings.filesystem.settings-filename' : SETTINGS_FILENAME, + 'settings.filesystem.profiles-dir' : PROFILES_PATH, + 'settings.filesystem.stored-requests-dir' : REQUESTS_PATH, + 'settings.filesystem.stored-imps-dir' : IMPS_PATH, + 'settings.filesystem.stored-responses-dir': RESPONSES_PATH, + 'settings.filesystem.categories-dir' : CATEGORIES_PATH + ] + + private static final Map PROFILES_CONFIG = [ + 'auction.profiles.fail-on-unknown': "false", + 'auction.profiles.limit' : LIMIT_HOST_PROFILE.toString(), + 'settings.database.profiles-query': "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles " + + "WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)".toString()] + + private static final String LIMIT_ERROR_MESSAGE = 'Profiles exceeded the limit.' + private static final String INVALID_REQUEST_PREFIX = 'Invalid request format: Error during processing profiles: ' + private static final String NO_IMP_PROFILE_MESSAGE = "No imp profiles for ids [%s] were found" + private static final String NO_REQUEST_PROFILE_MESSAGE = "No request profiles for ids [%s] were found" + private static final String NO_PROFILE_MESSAGE = "No profile found for id: %s" + + private static final String LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.limit_exceeded" + private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.missing" + + private static final ProfileImpDao profileImpDao = repository.profileImpDao + private static final ProfileRequestDao profileRequestDao = repository.profileRequestDao + + private static PrebidServerContainer pbsContainer + private static PrebidServerService pbsWithStoredProfiles + private static RequestProfile fileRequestProfile + private static RequestProfile fileRequestProfileWithEmptyMerge + private static ImpProfile fileImpProfile + private static ImpProfile fileImpProfileWithEmptyMerge + + def setupSpec() { + pbsContainer = new PrebidServerContainer(FILESYSTEM_CONFIG + PROFILES_CONFIG) + fileRequestProfile = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + fileImpProfile = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + pbsContainer.withCopyToContainer(Transferable.of(encode(fileRequestProfile)), "$PROFILES_PATH/${fileRequestProfile.fileName}") + pbsContainer.withCopyToContainer(Transferable.of(encode(fileImpProfile)), "$PROFILES_PATH/${fileImpProfile.fileName}") + fileRequestProfileWithEmptyMerge = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()).tap { + mergePrecedence = null + } + fileImpProfileWithEmptyMerge = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()).tap { + body.banner.tap { + btype = [PBSUtils.randomNumber] + format = [Format.randomFormat] + } + mergePrecedence = null + } + pbsContainer.withCopyToContainer(Transferable.of(encode(fileRequestProfileWithEmptyMerge)), "$PROFILES_PATH/${fileRequestProfileWithEmptyMerge.fileName}") + pbsContainer.withCopyToContainer(Transferable.of(encode(fileImpProfileWithEmptyMerge)), "$PROFILES_PATH/${fileImpProfileWithEmptyMerge.fileName}") + pbsContainer.withFolder(REQUESTS_PATH) + pbsContainer.withFolder(IMPS_PATH) + pbsContainer.withFolder(RESPONSES_PATH) + pbsContainer.withFolder(CATEGORIES_PATH) + def accountsConfig = new FileSystemAccountsConfig(accounts: [new AccountConfig(id: ACCOUNT_ID_FILE_STORAGE, status: ACTIVE)]) + pbsContainer.withCopyToContainer(Transferable.of(encodeYaml(accountsConfig)), + SETTINGS_FILENAME) + pbsContainer.start() + pbsWithStoredProfiles = new PrebidServerService(pbsContainer) + } + + def cleanupSpec() { + pbsContainer.stop() + } + + def "PBS should use profile for request when it exist in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = RequestProfile.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + } + } + + def "PBS should use imp profile for request when it exist in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ImpProfile.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { + it.imp.first.banner = null + } as BidRequest + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id).imp) { + it.id == [impProfile.body.id] + it.banner == [impProfile.body.banner] + } + } + + def "PBS should use profile for request when it exist in filesystem"() { + given: "Default bidRequest with request profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileRequestProfile]) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == fileRequestProfile.body.site.id + it.site.name == fileRequestProfile.body.site.name + it.site.domain == fileRequestProfile.body.site.domain + it.site.cat == fileRequestProfile.body.site.cat + it.site.sectionCat == fileRequestProfile.body.site.sectionCat + it.site.pageCat == fileRequestProfile.body.site.pageCat + it.site.page == fileRequestProfile.body.site.page + it.site.ref == fileRequestProfile.body.site.ref + it.site.search == fileRequestProfile.body.site.search + it.site.keywords == fileRequestProfile.body.site.keywords + it.site.ext.data == fileRequestProfile.body.site.ext.data + + it.device.didsha1 == fileRequestProfile.body.device.didsha1 + it.device.didmd5 == fileRequestProfile.body.device.didmd5 + it.device.dpidsha1 == fileRequestProfile.body.device.dpidsha1 + it.device.ifa == fileRequestProfile.body.device.ifa + it.device.macsha1 == fileRequestProfile.body.device.macsha1 + it.device.macmd5 == fileRequestProfile.body.device.macmd5 + it.device.dpidmd5 == fileRequestProfile.body.device.dpidmd5 + } + } + + def "PBS should use imp profile for request when it exist in filesystem"() { + given: "Default bidRequest with request profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileImpProfile]).tap { + it.imp.first.banner = null + } as BidRequest + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id).imp) { + it.id == [fileImpProfile.body.id] + it.banner == [fileImpProfile.body.banner] + } + } + + def "PBS should set merge strategy to default profile without error for request profile when merge strategy is empty in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = RequestProfile.getProfile(accountId).tap { + it.mergePrecedence = null + } + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { + it.site = Site.configFPDSite + } as BidRequest + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from original request when data is present" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + } + + and: "Bidder request should contain data from profile when data is empty" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + } + } + + def "PBS should set merge strategy to default profile without error for request profile when merge strategy is empty in filesystem"() { + given: "Default bidRequest with request profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileRequestProfileWithEmptyMerge]).tap { + it.site = Site.configFPDSite + } as BidRequest + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from original request when data is present" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + } + + and: "Bidder request should contain data from original request when data is empty" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.device.didsha1 == fileRequestProfileWithEmptyMerge.body.device.didsha1 + it.device.didmd5 == fileRequestProfileWithEmptyMerge.body.device.didmd5 + it.device.dpidsha1 == fileRequestProfileWithEmptyMerge.body.device.dpidsha1 + it.device.ifa == fileRequestProfileWithEmptyMerge.body.device.ifa + it.device.macsha1 == fileRequestProfileWithEmptyMerge.body.device.macsha1 + it.device.macmd5 == fileRequestProfileWithEmptyMerge.body.device.macmd5 + it.device.dpidmd5 == fileRequestProfileWithEmptyMerge.body.device.dpidmd5 + } + } + + def "PBS should set merge strategy to default profile without error for imp profile when merge strategy is empty in database"() { + given: "Default bidRequest with imp profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ImpProfile.getProfile(accountId).tap { + it.mergePrecedence = null + body.banner.tap { + btype = [PBSUtils.randomNumber] + format = [Format.randomFormat] + } + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile when data is present" + def bidderImpBanner = bidder.getBidderRequest(bidRequest.id).imp.banner.first + assert bidderImpBanner.format == bidRequest.imp.first.banner.format + + and: "Bidder request should contain data from profile when data is empty" + assert bidderImpBanner.btype == impProfile.body.banner.btype + } + + def "PBS should set merge strategy to default profile without error for imp profile when merge strategy is empty in filesystem"() { + given: "Default bidRequest with imp profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileImpProfileWithEmptyMerge]) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile when data is present" + def bidderImpBanner = bidder.getBidderRequest(bidRequest.id).imp.banner.first + assert bidderImpBanner.format == bidRequest.imp.first.banner.format + + and: "Bidder request should contain data from profile when data is empty" + assert bidderImpBanner.btype == fileImpProfileWithEmptyMerge.body.banner.btype + } + + def "PBS should merge latest-specified profile when there merge conflict and different merge precedence present"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + firstProfile.accountId = accountId + secondProfile.accountId = accountId + def bidRequest = getRequestWithProfiles(accountId, [firstProfile, secondProfile]).tap { + it.site = Site.configFPDSite + it.device = Device.default + } as BidRequest + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profiles" + def mergedRequest = [firstProfile, secondProfile].find { it.mergePrecedence == PROFILE }.body + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == mergedRequest.site.id + it.site.name == mergedRequest.site.name + it.site.domain == mergedRequest.site.domain + it.site.cat == mergedRequest.site.cat + it.site.sectionCat == mergedRequest.site.sectionCat + it.site.pageCat == mergedRequest.site.pageCat + it.site.page == mergedRequest.site.page + it.site.ref == mergedRequest.site.ref + it.site.search == mergedRequest.site.search + it.site.keywords == mergedRequest.site.keywords + it.site.ext.data == mergedRequest.site.ext.data + + it.device.didsha1 == mergedRequest.device.didsha1 + it.device.didmd5 == mergedRequest.device.didmd5 + it.device.dpidsha1 == mergedRequest.device.dpidsha1 + it.device.ifa == mergedRequest.device.ifa + it.device.macsha1 == mergedRequest.device.macsha1 + it.device.macmd5 == mergedRequest.device.macmd5 + it.device.dpidmd5 == mergedRequest.device.dpidmd5 + } + + where: + firstProfile | secondProfile + RequestProfile.getProfile().tap { mergePrecedence = REQUEST } | RequestProfile.getProfile() + RequestProfile.getProfile() | RequestProfile.getProfile().tap { mergePrecedence = REQUEST } + } + + def "PBS should merge first-specified profile with request merge precedence when there merge conflict"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + it.mergePrecedence = REQUEST + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + it.mergePrecedence = REQUEST + } + def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == firstRequestProfile.body.site.id + it.site.name == firstRequestProfile.body.site.name + it.site.domain == firstRequestProfile.body.site.domain + it.site.cat == firstRequestProfile.body.site.cat + it.site.sectionCat == firstRequestProfile.body.site.sectionCat + it.site.pageCat == firstRequestProfile.body.site.pageCat + it.site.ref == firstRequestProfile.body.site.ref + it.site.search == firstRequestProfile.body.site.search + it.site.keywords == firstRequestProfile.body.site.keywords + it.site.ext.data == firstRequestProfile.body.site.ext.data + + it.device.didsha1 == firstRequestProfile.body.device.didsha1 + it.device.didmd5 == firstRequestProfile.body.device.didmd5 + it.device.dpidsha1 == firstRequestProfile.body.device.dpidsha1 + it.device.ifa == firstRequestProfile.body.device.ifa + it.device.macsha1 == firstRequestProfile.body.device.macsha1 + it.device.macmd5 == firstRequestProfile.body.device.macmd5 + it.device.dpidmd5 == firstRequestProfile.body.device.dpidmd5 + } + } + + def "PBS should merge latest-specified profile with profile merge precedence when there merge conflict"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + } + def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == secondRequestProfile.body.site.id + it.site.name == secondRequestProfile.body.site.name + it.site.domain == secondRequestProfile.body.site.domain + it.site.cat == secondRequestProfile.body.site.cat + it.site.sectionCat == secondRequestProfile.body.site.sectionCat + it.site.pageCat == secondRequestProfile.body.site.pageCat + it.site.page == secondRequestProfile.body.site.page + it.site.ref == secondRequestProfile.body.site.ref + it.site.search == secondRequestProfile.body.site.search + it.site.keywords == secondRequestProfile.body.site.keywords + it.site.ext.data == secondRequestProfile.body.site.ext.data + + it.device.didsha1 == secondRequestProfile.body.device.didsha1 + it.device.didmd5 == secondRequestProfile.body.device.didmd5 + it.device.dpidsha1 == secondRequestProfile.body.device.dpidsha1 + it.device.ifa == secondRequestProfile.body.device.ifa + it.device.macsha1 == secondRequestProfile.body.device.macsha1 + it.device.macmd5 == secondRequestProfile.body.device.macmd5 + it.device.dpidmd5 == secondRequestProfile.body.device.dpidmd5 + } + } + + def "PBS should prioritise profile for request and emit warning when request is overloaded by profiles"() { + given: "Default bidRequest with profiles" + def accountId = PBSUtils.randomNumber as String + def profileSite = Site.rootFPDSite + def profileDevice = Device.default + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.site = profileSite + it.body.device = null + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.site = null + it.body.device = profileDevice + } + def impProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def bidRequest = getRequestWithProfiles(accountId, [impProfile, firstRequestProfile, secondRequestProfile]) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [LIMIT_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == profileSite.id + it.site.name == profileSite.name + it.site.domain == profileSite.domain + it.site.cat == profileSite.cat + it.site.sectionCat == profileSite.sectionCat + it.site.pageCat == profileSite.pageCat + it.site.page == profileSite.page + it.site.ref == profileSite.ref + it.site.search == profileSite.search + it.site.keywords == profileSite.keywords + it.site.ext.data == profileSite.ext.data + + it.device.didsha1 == profileDevice.didsha1 + it.device.didmd5 == profileDevice.didmd5 + it.device.dpidsha1 == profileDevice.dpidsha1 + it.device.ifa == profileDevice.ifa + it.device.macsha1 == profileDevice.macsha1 + it.device.macmd5 == profileDevice.macmd5 + it.device.dpidmd5 == profileDevice.dpidmd5 + } + + and: "Bidder imp should contain original data from request" + assert verifyAll(bidderRequest.imp) { + it.banner == bidRequest.imp.banner + it.video == [null] + } + } + + def "PBS should be able override profile limit by account config and use remaining limits for each imp separately"() { + given: "BidRequest with profiles" + def accountId = PBSUtils.randomNumber as String + def profileSite = Site.defaultSite + def profileDevice = Device.default + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = null + it.body.site = profileSite + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.site = null + it.body.device = profileDevice + } + def firstImp = Imp.defaultImpression.tap { + it.banner.btype = [PBSUtils.randomNumber] + } + def secondImp = Imp.defaultImpression.tap { + it.banner.battr = [PBSUtils.randomNumber] + } + def thirdImp = Imp.defaultImpression.tap { + it.banner.mimes = [PBSUtils.randomString] + } + def firstImpProfile = ImpProfile.getProfile(accountId, firstImp) + def secondImpProfile = ImpProfile.getProfile(accountId, secondImp) + def thirdImpProfile = ImpProfile.getProfile(accountId, thirdImp) + def bidRequest = getRequestWithProfiles(accountId, [firstImpProfile, secondImpProfile, firstRequestProfile, secondRequestProfile]).tap { + imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profileNames: [secondImpProfile, thirdImpProfile].id))) + } as BidRequest + + and: "Default account" + def profilesConfigs = new AccountProfilesConfigs(limit: LIMIT_HOST_PROFILE + 2) + def accountAuctionConfig = new AccountAuctionConfig(profiles: profilesConfigs) + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE, config: accountConfig) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + profileImpDao.save(StoredProfileImp.getProfile(firstImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(secondImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(thirdImpProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Missing metric shouldn't increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert !metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] + + and: "Bidder request should contain data from profiles" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == profileSite.id + it.site.name == profileSite.name + it.site.domain == profileSite.domain + it.site.cat == profileSite.cat + it.site.sectionCat == profileSite.sectionCat + it.site.pageCat == profileSite.pageCat + it.site.page == profileSite.page + it.site.ref == profileSite.ref + it.site.search == profileSite.search + it.site.keywords == profileSite.keywords + + it.device.didsha1 == profileDevice.didsha1 + it.device.didmd5 == profileDevice.didmd5 + it.device.dpidsha1 == profileDevice.dpidsha1 + it.device.ifa == profileDevice.ifa + it.device.macsha1 == profileDevice.macsha1 + it.device.macmd5 == profileDevice.macmd5 + it.device.dpidmd5 == profileDevice.dpidmd5 + } + + and: "Bidder imp should contain data from specified profiles" + def firstBidderImpBanner = bidderRequest.imp.first.banner + verifyAll(firstBidderImpBanner) { + it.btype == firstImpProfile.body.banner.btype + it.battr == secondImpProfile.body.banner.battr + } + + and: "Ignore data from unspecified profiles" + assert !firstBidderImpBanner.mimes + + and: "Bidder imp should contain data from specified profiles" + def secondBidderImpBanner = bidderRequest.imp.last.banner + verifyAll(secondBidderImpBanner) { + it.battr == secondImpProfile.body.banner.battr + it.mimes == thirdImpProfile.body.banner.mimes + } + + and: "Ignore data from unspecified profiles" + assert !secondBidderImpBanner.btype + } + + def "PBS should count invalid or missing profiles towards the limit"() { + given: "Default bidRequest with request profiles" + def accountId = PBSUtils.randomNumber as String + def invalidProfileRequest = RequestProfile.getProfile(accountId).tap { + it.body = null + } + def impProfile = ImpProfile.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.tap { + it.banner.format = [Format.randomFormat] + it.ext.prebid.profileNames = [impProfile.id] + } + it.ext.prebid.profileNames = [invalidProfileRequest.id, PBSUtils.randomString] + it.site = Site.configFPDSite + it.device = Device.default + setAccountId(accountId) + } + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(invalidProfileRequest)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.message.contains(LIMIT_ERROR_MESSAGE) + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + + and: "Bidder request imp should contain data from request" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + } + + def "PBS should include data from storedBidResponses when it specified in profiles"() { + given: "Default BidRequest with profile" + def accountId = PBSUtils.randomNumber as String + def storedResponseId = PBSUtils.randomNumber + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.id = null + it.body.ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored bid response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedBidResponse.seatbid[0].seat + assert response.seatbid[0]?.bid?.size() == storedBidResponse.seatbid[0].bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedBidResponse.seatbid[0].bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedBidResponse.seatbid[0].bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedBidResponse.seatbid[0].bid[0].id + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should include data from storedAuctionResponse when it specified in profiles"() { + given: "Default basic BidRequest with profile" + def accountId = PBSUtils.randomNumber as String + def storedAuctionId = PBSUtils.randomNumber + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.id = null + it.body.ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedAuctionId) + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Stored response in DB" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedAuctionId, + storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored bid response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedAuctionResponse.seat + assert response.seatbid[0]?.bid?.size() == storedAuctionResponse.bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedAuctionResponse.bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedAuctionResponse.bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedAuctionResponse.bid[0].id + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should fail auction when fail-on-unknown-profile enabled and profile is missing"() { + given: "PBS with profiles.fail-on-unknown config" + def prebidServerService = pbsServiceFactory.getService(PROFILES_CONFIG + + ['auction.profiles.fail-on-unknown': 'true']) + + and: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': 'true']) + } + + def "PBS should fail auction when fail-on-unknown-profile default and profile is missing"() { + given: "PBS without profiles.fail-on-unknown config" + def prebidServerService = pbsServiceFactory.getService(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': null]) + + and: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': null]) + } + + def "PBS should prioritise fail-on-unknown-profile from account over host config"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def accountAuctionConfig = new AccountAuctionConfig(profiles: profilesConfigs) + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + where: + profilesConfigs << [ + new AccountProfilesConfigs(failOnUnknown: true), + new AccountProfilesConfigs(failOnUnknownSnakeCase: true), + ] + } + + def "PBS should ignore inner request profiles when stored request profile contain link for another profile"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def innerRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.app = App.defaultApp + } + + def requestProfile = RequestProfile.getProfile(accountId).tap { + it.body.ext.prebid.profileNames = [innerRequestProfile.id] + } + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { + it.site = Site.configFPDSite + it.device = Device.default + } as BidRequest + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(innerRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + } + + and: "Bidder request shouldn't contain data from inner profile" + assert !bidderRequest.app + } + + def "PBS should ignore inner imp profiles when stored imp profile contain link for another profile"() { + given: "Default bidRequest with imp profile" + def accountId = PBSUtils.randomNumber as String + def innerImpProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.ext.prebid.profileNames = [innerImpProfile.id] + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { + it.imp.first.banner = null + } as BidRequest + + and: "Default profiles in database" + profileImpDao.save(StoredProfileImp.getProfile(innerImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.banner == impProfile.body.banner + + and: "Bidder request imp shouldn't contain data from inner profile" + assert bidderImp.video == impProfile.body.video + } + + def "PBS shouldn't validate profiles and imp before margining"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def height = PBSUtils.randomNumber + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.banner.format.first.weight = null + it.body.banner.format.first.height = height + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) as BidRequest + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid request" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: request.imp[0].banner.format[0] must define a valid "h" and "w" properties' + } + + def "PBS shouldn't emit error or warnings when bidRequest contains multiple imps with same profile"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def imp = Imp.defaultImpression.tap { + it.banner.format = [Format.randomFormat] + } + def impProfile = ImpProfile.getProfile(accountId, imp) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + addImp(Imp.getDefaultImpression()) + setAccountId(accountId) + } as BidRequest + bidRequest.imp.each { + it.ext.prebid.profileNames = [impProfile.id] + } + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imps should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.first.banner == impProfile.body.banner + assert bidder.getBidderRequest(bidRequest.id).imp.last.banner == impProfile.body.banner + } + + def "PBS should ignore imp data from request profile when imp for profile not null"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequestProfile = BidRequest.defaultBidRequest.tap { + it.id = null + it.imp.first.banner.format = [Format.randomFormat] + } + def requestProfile = RequestProfile.getProfile(accountId, + bidRequestProfile, + PBSUtils.randomString, + mergePrecedence) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + + where: + mergePrecedence << [REQUEST, PROFILE] + } + + @PendingFeature + def "PBS should add error and metrics when imp name is invalid"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ImpProfile.getProfile(accountId, Imp.defaultImpression, invalidProfileName) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [impProfile.id] + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [LIMIT_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(LIMIT_ERROR_MESSAGE) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + + where: + invalidProfileName << [PBSUtils.randomSpecialChars, PBSUtils.randomStringWithSpecials] + } + + def "PBS should emit error and metrics when request profile called from imp level"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = RequestProfile.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [requestProfile.id] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + } + + def "PBS should emit error and metrics when imp profile called from request level"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = ImpProfile.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [requestProfile.id] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(requestProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + } + + def "PBS should emit error and metrics when imp profile missing"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request imp should contain data from original imp" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + } + + def "PBS should emit error and metrics when request profile missing"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [invalidProfileId] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_REQUEST_PROFILE_MESSAGE.formatted(invalidProfileId)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + } + + def "PBS should emit error and metrics when imp profile have invalid data"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfile.id] + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_IMP_PROFILE_MESSAGE.formatted(invalidProfile.id)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request imp should contain data from original imp" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + + where: + invalidProfile << [ + ImpProfile.getProfile().tap { it.type = ProfileType.EMPTY}, + ImpProfile.getProfile().tap { it.type = ProfileType.UNKNOWN}, + ImpProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.EMPTY}, + ImpProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.UNKNOWN}, + ] + } + + def "PBS should emit error and metrics when request profile have invalid data"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [invalidProfileId] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_REQUEST_PROFILE_MESSAGE.formatted(invalidProfileId)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + + where: + invalidProfile << [ + RequestProfile.getProfile().tap { it.type = ProfileType.EMPTY}, + RequestProfile.getProfile().tap { it.type = ProfileType.UNKNOWN}, + RequestProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.EMPTY}, + RequestProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.UNKNOWN}, + ] + } + + private static BidRequest getRequestWithProfiles(String accountId, List profiles) { + BidRequest.getDefaultBidRequest().tap { + if (profiles.type.contains(ProfileType.IMP)) { + it.imp.first.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.IMP }*.id + } + it.imp.first.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.IMP }*.id + it.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.REQUEST }*.id + setAccountId(accountId) + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy index 3ab9e349ac9..15106b6a55f 100644 --- a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.xml.XmlMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL @@ -11,6 +12,7 @@ trait ObjectMapperWrapper { private static final ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(NON_NULL) .registerModule(new ZonedDateTimeModule()) + private static final YAMLMapper yamlMapper = new YAMLMapper().setSerializationInclusion(NON_NULL) as YAMLMapper private static final XmlMapper xmlMapper = new XmlMapper() final static String encode(Object object) { @@ -44,4 +46,8 @@ trait ObjectMapperWrapper { final static String encodeXml(Object object) { xmlMapper.writeValueAsString(object) } + + final static String encodeYaml(Object object) { + yamlMapper.writeValueAsString(object) + } } diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy index e1e7750ea05..de518ea4209 100644 --- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy @@ -50,6 +50,14 @@ class PBSUtils implements ObjectMapperWrapper { RandomStringUtils.randomAlphanumeric(stringLength) } + static String getRandomSpecialChars(int stringLength = 20) { + RandomStringUtils.random(stringLength, "!@#\$%^&*()-_=+[]{}|;:'\",.<>/?") + } + + static String getRandomStringWithSpecials(int stringLength = 20) { + RandomStringUtils.randomAscii(stringLength) + } + static Boolean getRandomBoolean() { new Random().nextBoolean() } diff --git a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql index 9d1732c97b0..0f4d026337f 100644 --- a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql +++ b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql @@ -42,5 +42,14 @@ CREATE TABLE stored_responses storedBidResponse varchar(1024) ); +CREATE TABLE profiles +( + accountId varchar(40) NOT NULL, + profileId varchar(128) NOT NULL, + profile json, + mergePrecedence enum ('request', 'profile', '', 'unknown'), + type enum ('request', 'imp', '', 'unknown') +); + -- set session wait timeout to 1 minute SET SESSION wait_timeout = 60000;