|
| 1 | +package org.prebid.server.auction.externalortb; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.core.JsonProcessingException; |
| 4 | +import com.fasterxml.jackson.databind.JsonNode; |
| 5 | +import com.fasterxml.jackson.databind.node.ObjectNode; |
| 6 | +import com.iab.openrtb.request.BidRequest; |
| 7 | +import com.iab.openrtb.request.Imp; |
| 8 | +import io.vertx.core.Future; |
| 9 | +import org.apache.commons.lang3.StringUtils; |
| 10 | +import org.prebid.server.auction.model.AuctionContext; |
| 11 | +import org.prebid.server.exception.InvalidProfileException; |
| 12 | +import org.prebid.server.exception.InvalidRequestException; |
| 13 | +import org.prebid.server.execution.timeout.Timeout; |
| 14 | +import org.prebid.server.execution.timeout.TimeoutFactory; |
| 15 | +import org.prebid.server.json.JacksonMapper; |
| 16 | +import org.prebid.server.json.JsonMerger; |
| 17 | +import org.prebid.server.log.ConditionalLogger; |
| 18 | +import org.prebid.server.log.LoggerFactory; |
| 19 | +import org.prebid.server.metric.MetricName; |
| 20 | +import org.prebid.server.metric.Metrics; |
| 21 | +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; |
| 22 | +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; |
| 23 | +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; |
| 24 | +import org.prebid.server.settings.ApplicationSettings; |
| 25 | +import org.prebid.server.settings.model.Account; |
| 26 | +import org.prebid.server.settings.model.AccountAuctionConfig; |
| 27 | +import org.prebid.server.settings.model.AccountProfilesConfig; |
| 28 | +import org.prebid.server.settings.model.Profile; |
| 29 | +import org.prebid.server.settings.model.StoredDataResult; |
| 30 | + |
| 31 | +import java.util.ArrayList; |
| 32 | +import java.util.Collection; |
| 33 | +import java.util.Collections; |
| 34 | +import java.util.HashSet; |
| 35 | +import java.util.List; |
| 36 | +import java.util.Map; |
| 37 | +import java.util.Objects; |
| 38 | +import java.util.Optional; |
| 39 | +import java.util.Set; |
| 40 | +import java.util.stream.Collectors; |
| 41 | + |
| 42 | +public class ProfilesProcessor { |
| 43 | + |
| 44 | + private static final ConditionalLogger conditionalLogger = |
| 45 | + new ConditionalLogger(LoggerFactory.getLogger(ProfilesProcessor.class)); |
| 46 | + |
| 47 | + private final int maxProfiles; |
| 48 | + private final long defaultTimeoutMillis; |
| 49 | + private final boolean failOnUnknown; |
| 50 | + private final double logSamplingRate; |
| 51 | + private final ApplicationSettings applicationSettings; |
| 52 | + private final TimeoutFactory timeoutFactory; |
| 53 | + private final Metrics metrics; |
| 54 | + private final JacksonMapper mapper; |
| 55 | + private final JsonMerger jsonMerger; |
| 56 | + |
| 57 | + public ProfilesProcessor(int maxProfiles, |
| 58 | + long defaultTimeoutMillis, |
| 59 | + boolean failOnUnknown, |
| 60 | + double logSamplingRate, |
| 61 | + ApplicationSettings applicationSettings, |
| 62 | + TimeoutFactory timeoutFactory, |
| 63 | + Metrics metrics, |
| 64 | + JacksonMapper mapper, |
| 65 | + JsonMerger jsonMerger) { |
| 66 | + |
| 67 | + this.maxProfiles = maxProfiles; |
| 68 | + this.defaultTimeoutMillis = defaultTimeoutMillis; |
| 69 | + this.failOnUnknown = failOnUnknown; |
| 70 | + this.logSamplingRate = logSamplingRate; |
| 71 | + this.applicationSettings = Objects.requireNonNull(applicationSettings); |
| 72 | + this.timeoutFactory = Objects.requireNonNull(timeoutFactory); |
| 73 | + this.metrics = Objects.requireNonNull(metrics); |
| 74 | + this.mapper = Objects.requireNonNull(mapper); |
| 75 | + this.jsonMerger = Objects.requireNonNull(jsonMerger); |
| 76 | + } |
| 77 | + |
| 78 | + public Future<BidRequest> process(AuctionContext auctionContext, BidRequest bidRequest) { |
| 79 | + final String accountId = Optional.ofNullable(auctionContext.getAccount()) |
| 80 | + .map(Account::getId) |
| 81 | + .orElse(StringUtils.EMPTY); |
| 82 | + |
| 83 | + final AllProfilesIds profilesIds = profilesIds(bidRequest, auctionContext, accountId); |
| 84 | + if (profilesIds.isEmpty()) { |
| 85 | + return Future.succeededFuture(bidRequest); |
| 86 | + } |
| 87 | + |
| 88 | + final boolean failOnUnknown = isFailOnUnknown(auctionContext.getAccount()); |
| 89 | + |
| 90 | + return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest)) |
| 91 | + .compose(profiles -> emitMetrics(accountId, profiles, auctionContext, failOnUnknown)) |
| 92 | + .map(profiles -> mergeResults( |
| 93 | + applyRequestProfiles( |
| 94 | + profilesIds.request(), |
| 95 | + profiles.getStoredIdToRequest(), |
| 96 | + bidRequest, |
| 97 | + failOnUnknown), |
| 98 | + applyImpsProfiles( |
| 99 | + profilesIds.imps(), |
| 100 | + profiles.getStoredIdToImp(), |
| 101 | + bidRequest.getImp(), |
| 102 | + failOnUnknown))) |
| 103 | + .recover(error -> Future.failedFuture( |
| 104 | + new InvalidRequestException("Error during processing profiles: " + error.getMessage()))); |
| 105 | + } |
| 106 | + |
| 107 | + private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auctionContext, String accountId) { |
| 108 | + final AllProfilesIds initialProfilesIds = new AllProfilesIds( |
| 109 | + requestProfilesIds(bidRequest), |
| 110 | + bidRequest.getImp().stream().map(this::impProfilesIds).toList()); |
| 111 | + |
| 112 | + final AllProfilesIds profilesIds = truncate( |
| 113 | + initialProfilesIds, |
| 114 | + Optional.ofNullable(auctionContext.getAccount()) |
| 115 | + .map(Account::getAuction) |
| 116 | + .map(AccountAuctionConfig::getProfiles) |
| 117 | + .map(AccountProfilesConfig::getLimit) |
| 118 | + .orElse(maxProfiles)); |
| 119 | + |
| 120 | + if (auctionContext.getDebugContext().isDebugEnabled() && !profilesIds.equals(initialProfilesIds)) { |
| 121 | + auctionContext.getDebugWarnings().add("Profiles exceeded the limit."); |
| 122 | + metrics.updateAccountProfileMetric(accountId, MetricName.limit_exceeded); |
| 123 | + } |
| 124 | + |
| 125 | + return profilesIds; |
| 126 | + } |
| 127 | + |
| 128 | + private static List<String> requestProfilesIds(BidRequest bidRequest) { |
| 129 | + return Optional.ofNullable(bidRequest) |
| 130 | + .map(BidRequest::getExt) |
| 131 | + .map(ExtRequest::getPrebid) |
| 132 | + .map(ExtRequestPrebid::getProfiles) |
| 133 | + .orElse(Collections.emptyList()); |
| 134 | + } |
| 135 | + |
| 136 | + private List<String> impProfilesIds(Imp imp) { |
| 137 | + return Optional.ofNullable(imp.getExt()) |
| 138 | + .map(ext -> ext.get("prebid")) |
| 139 | + .map(this::parseImpExt) |
| 140 | + .map(ExtImpPrebid::getProfiles) |
| 141 | + .orElse(Collections.emptyList()); |
| 142 | + } |
| 143 | + |
| 144 | + private ExtImpPrebid parseImpExt(JsonNode jsonNode) { |
| 145 | + try { |
| 146 | + return mapper.mapper().treeToValue(jsonNode, ExtImpPrebid.class); |
| 147 | + } catch (JsonProcessingException e) { |
| 148 | + throw new InvalidRequestException(e.getMessage()); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + private static AllProfilesIds truncate(AllProfilesIds profilesIds, int maxProfiles) { |
| 153 | + final List<String> requestProfiles = profilesIds.request(); |
| 154 | + final int impProfilesLimit = Math.max(0, maxProfiles - requestProfiles.size()); |
| 155 | + |
| 156 | + return new AllProfilesIds( |
| 157 | + truncate(requestProfiles, maxProfiles), |
| 158 | + profilesIds.imps().stream() |
| 159 | + .map(impProfiles -> truncate(impProfiles, impProfilesLimit)) |
| 160 | + .toList()); |
| 161 | + } |
| 162 | + |
| 163 | + private static <T> List<T> truncate(List<T> list, int maxSize) { |
| 164 | + return list.size() > maxSize ? list.subList(0, maxSize) : list; |
| 165 | + } |
| 166 | + |
| 167 | + private long timeoutMillis(BidRequest bidRequest) { |
| 168 | + final Long tmax = bidRequest.getTmax(); |
| 169 | + return tmax != null && tmax > 0 ? tmax : defaultTimeoutMillis; |
| 170 | + } |
| 171 | + |
| 172 | + private boolean isFailOnUnknown(Account account) { |
| 173 | + return Optional.ofNullable(account) |
| 174 | + .map(Account::getAuction) |
| 175 | + .map(AccountAuctionConfig::getProfiles) |
| 176 | + .map(AccountProfilesConfig::getFailOnUnknown) |
| 177 | + .orElse(failOnUnknown); |
| 178 | + } |
| 179 | + |
| 180 | + private Future<StoredDataResult<Profile>> fetchProfiles(String accountId, |
| 181 | + AllProfilesIds allProfilesIds, |
| 182 | + long timeoutMillis) { |
| 183 | + |
| 184 | + final Set<String> requestProfilesIds = new HashSet<>(allProfilesIds.request()); |
| 185 | + final Set<String> impProfilesIds = allProfilesIds.imps().stream() |
| 186 | + .flatMap(Collection::stream) |
| 187 | + .collect(Collectors.toSet()); |
| 188 | + final Timeout timeout = timeoutFactory.create(timeoutMillis); |
| 189 | + |
| 190 | + return applicationSettings.getProfiles(accountId, requestProfilesIds, impProfilesIds, timeout); |
| 191 | + } |
| 192 | + |
| 193 | + private Future<StoredDataResult<Profile>> emitMetrics(String accountId, |
| 194 | + StoredDataResult<Profile> fetchResult, |
| 195 | + AuctionContext auctionContext, |
| 196 | + boolean failOnUnknown) { |
| 197 | + |
| 198 | + final List<String> errors = fetchResult.getErrors(); |
| 199 | + if (!errors.isEmpty()) { |
| 200 | + metrics.updateProfileMetric(MetricName.missing); |
| 201 | + |
| 202 | + if (auctionContext.getDebugContext().isDebugEnabled()) { |
| 203 | + metrics.updateAccountProfileMetric(accountId, MetricName.missing); |
| 204 | + auctionContext.getDebugWarnings().addAll(errors); |
| 205 | + } |
| 206 | + |
| 207 | + if (failOnUnknown) { |
| 208 | + return Future.failedFuture(new InvalidProfileException(errors)); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + return Future.succeededFuture(fetchResult); |
| 213 | + } |
| 214 | + |
| 215 | + private BidRequest applyRequestProfiles(List<String> profilesIds, |
| 216 | + Map<String, Profile> idToRequestProfile, |
| 217 | + BidRequest bidRequest, |
| 218 | + boolean failOnUnknown) { |
| 219 | + |
| 220 | + return !idToRequestProfile.isEmpty() |
| 221 | + ? applyProfiles(profilesIds, idToRequestProfile, bidRequest, failOnUnknown) |
| 222 | + : bidRequest; |
| 223 | + } |
| 224 | + |
| 225 | + private <T> T applyProfiles(List<String> profilesIds, |
| 226 | + Map<String, Profile> idToProfile, |
| 227 | + T original, |
| 228 | + boolean failOnUnknown) { |
| 229 | + |
| 230 | + if (profilesIds.isEmpty()) { |
| 231 | + return original; |
| 232 | + } |
| 233 | + |
| 234 | + ObjectNode result = mapper.mapper().valueToTree(original); |
| 235 | + for (String profileId : profilesIds) { |
| 236 | + try { |
| 237 | + final Profile profile = idToProfile.get(profileId); |
| 238 | + result = profile != null ? mergeProfile(result, profile) : result; |
| 239 | + } catch (InvalidRequestException e) { |
| 240 | + final String message = "Can't merge with profile %s: %s".formatted(profileId, e.getMessage()); |
| 241 | + |
| 242 | + metrics.updateProfileMetric(MetricName.invalid); |
| 243 | + conditionalLogger.error(message, logSamplingRate); |
| 244 | + if (failOnUnknown) { |
| 245 | + throw new InvalidProfileException(message); |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + try { |
| 251 | + return mapper.mapper().treeToValue(result, (Class<T>) original.getClass()); |
| 252 | + } catch (JsonProcessingException e) { |
| 253 | + throw new InvalidProfileException(e.getMessage()); |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + private ObjectNode mergeProfile(ObjectNode original, Profile profile) { |
| 258 | + return switch (profile.getMergePrecedence()) { |
| 259 | + case REQUEST -> merge(original, profile.getBody()); |
| 260 | + case PROFILE -> merge(profile.getBody(), original); |
| 261 | + }; |
| 262 | + } |
| 263 | + |
| 264 | + private ObjectNode merge(JsonNode takePrecedence, JsonNode other) { |
| 265 | + if (!takePrecedence.isObject() || !other.isObject()) { |
| 266 | + throw new InvalidRequestException("One of the merge arguments is not an object."); |
| 267 | + } |
| 268 | + |
| 269 | + return (ObjectNode) jsonMerger.merge(takePrecedence, other); |
| 270 | + } |
| 271 | + |
| 272 | + private List<Imp> applyImpsProfiles(List<List<String>> profilesIds, |
| 273 | + Map<String, Profile> idToImpProfile, |
| 274 | + List<Imp> imps, |
| 275 | + boolean failOnUnknown) { |
| 276 | + |
| 277 | + if (idToImpProfile.isEmpty()) { |
| 278 | + return imps; |
| 279 | + } |
| 280 | + |
| 281 | + final List<Imp> updatedImps = new ArrayList<>(imps); |
| 282 | + for (int i = 0; i < profilesIds.size(); i++) { |
| 283 | + updatedImps.set(i, applyProfiles( |
| 284 | + profilesIds.get(i), |
| 285 | + idToImpProfile, |
| 286 | + imps.get(i), |
| 287 | + failOnUnknown)); |
| 288 | + } |
| 289 | + |
| 290 | + return Collections.unmodifiableList(updatedImps); |
| 291 | + } |
| 292 | + |
| 293 | + private static BidRequest mergeResults(BidRequest bidRequest, List<Imp> imps) { |
| 294 | + return bidRequest.toBuilder().imp(imps).build(); |
| 295 | + } |
| 296 | + |
| 297 | + private record AllProfilesIds(List<String> request, List<List<String>> imps) { |
| 298 | + |
| 299 | + public boolean isEmpty() { |
| 300 | + return request.isEmpty() && imps.stream().allMatch(List::isEmpty); |
| 301 | + } |
| 302 | + } |
| 303 | +} |
0 commit comments