Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
ab8d6d4
Initial rules.
And1sS Jan 27, 2025
b230ab8
POC.
And1sS Jan 28, 2025
4d34955
POC refactoring.
And1sS Jan 28, 2025
af8c163
POC refactoring.
And1sS Jan 29, 2025
e64d6fd
POC refactoring, added wildcard matching.
And1sS Jan 29, 2025
21aa144
Moved code to module
And1sS Feb 11, 2025
3b06905
Merge branch 'master' into ruleengine
And1sS Feb 11, 2025
e2cbe61
WIP
And1sS Feb 26, 2025
899f9da
WIP.
And1sS Apr 3, 2025
9057581
WIP.
And1sS Apr 16, 2025
7230f79
Added weighted rule and utilities for it.
And1sS Apr 22, 2025
d87cf58
WIP.
And1sS Apr 29, 2025
95cab53
WIP.
And1sS Apr 30, 2025
66bcf44
WIP.
And1sS Apr 30, 2025
45782a5
WIP.
And1sS Apr 30, 2025
9f2af72
WIP.
And1sS Apr 30, 2025
8f0aed7
WIP.
And1sS Apr 30, 2025
5ef2d23
WIP.
And1sS Apr 30, 2025
e5c5a8c
Added removal of invalid configuration cache entries.
And1sS May 1, 2025
c7d421b
Added default actions.
And1sS May 1, 2025
1325d5b
Refactored default action.
And1sS May 1, 2025
3afa3f3
Added checks for schema and tree dimensions, added ability to make we…
And1sS May 1, 2025
b71803f
Fixed bug when unmatched rule gets translated to NoOp.
And1sS May 1, 2025
c4d34e2
Added functions config arguments validations.
And1sS May 1, 2025
5ea75da
Styling fixes.
And1sS May 4, 2025
966ee57
Merge branch 'refs/heads/master' into ruleengine
And1sS May 4, 2025
b9baf8e
WIP.
And1sS May 21, 2025
4c73213
WIP.
And1sS May 21, 2025
570d097
Started writing schema functions.
And1sS May 21, 2025
5c3599a
Added adUnitCode function.
And1sS May 29, 2025
f4bf5c9
WIP.
And1sS May 29, 2025
be50826
Cleanup.
And1sS May 29, 2025
f2904d5
Added a bunch of schema functions.
And1sS Jun 2, 2025
b91ceb8
Made compiler happy.
And1sS Jun 2, 2025
aa1cc89
Small refactorings.
And1sS Jun 2, 2025
6913355
Random weighted rule refinement.
And1sS Jun 5, 2025
13cfd6a
Added ability to check rule matched values, almost finished exclude/i…
And1sS Jun 13, 2025
3e4ddba
Added seatNonBid, finished include/excludeBidders functions.
And1sS Jun 18, 2025
fa29aed
Moved impId to result function context to generify result function ar…
And1sS Jun 18, 2025
d0bdc2a
Changed result function config field from config to args, fixed filte…
And1sS Jul 11, 2025
75f4b8d
Refactorings and analytics tags additions.
And1sS Jul 11, 2025
b1eaf68
Enhanced registry cache with versioning and exponential backoff throt…
And1sS Jul 13, 2025
f0d1b2b
Fixed error in exclude/include result functions related to ifSyncedId…
And1sS Jul 14, 2025
3b837dc
Added toggles on different levels, secured code against NPEs.
And1sS Jul 16, 2025
19f71c5
Added LogATag result function.
And1sS Jul 16, 2025
8fb7db0
Changed parser cache backend.
And1sS Jul 16, 2025
c1f2b91
Refactorings.
And1sS Jul 16, 2025
67cc1a5
Finished all schema functions.
And1sS Jul 17, 2025
ab13030
Styling fixes.
And1sS Jul 17, 2025
20ef8bd
Fixed misplaced modelVersion and analyticsKey fields.
And1sS Jul 23, 2025
812ef28
Fixed minor remarks.
And1sS Jul 23, 2025
7afa1dd
Fixed minor remarks.
And1sS Jul 24, 2025
a105ab7
More unit tests.
And1sS Jul 24, 2025
a421417
Merge branch 'master' into ruleengine
And1sS Jul 24, 2025
9d5a7c9
Renamed rule-engine to pb-rule-engine, bumped version to latest pbs v…
And1sS Jul 24, 2025
56901d7
Added unit tests for some schema functions.
And1sS Aug 5, 2025
673de00
Added unit tests for more schema functions.
And1sS Aug 5, 2025
b38c797
Added unit tests for more schema functions.
And1sS Aug 5, 2025
4ca8182
Added unit tests for more schema functions.
And1sS Aug 6, 2025
f365d40
Major refactoring.
And1sS Aug 8, 2025
78bfdef
Generified condition matching rule.
And1sS Aug 8, 2025
f93ec2a
Minor refactorings.
And1sS Aug 8, 2025
d38b9ef
Fixes.
And1sS Aug 8, 2025
62d7ddb
Added a bunch of unit tests for schema functions, some fixes.
And1sS Aug 8, 2025
9be5c95
Finished schema functions unit tests.
And1sS Aug 8, 2025
9262c06
Added unit test for condition matching rule.
And1sS Aug 8, 2025
6536ad0
Added tests for stage config parser.
And1sS Aug 19, 2025
cbfd49b
Added unit test for per imp matching rule.
And1sS Aug 19, 2025
8304e23
Added tests for GppSidInFunction.
And1sS Aug 19, 2025
90393de
Started writing tests for filtering result functions.
And1sS Aug 19, 2025
766298a
Added ability for result functions to reject payload.
And1sS Aug 19, 2025
8084bb9
Finished tests for ExcludeBiddersFunction.
And1sS Aug 19, 2025
e446f36
Finished tests for filering functions.
And1sS Aug 19, 2025
7a6e3d2
Added tests for logAtag function.
And1sS Aug 19, 2025
f93b7c3
Added tests for AccountConfigParser.
And1sS Aug 19, 2025
d4900f3
Added tests for DefaultActionRule.
And1sS Aug 19, 2025
2788a0a
Added unit tests for RequestConditionalRuleFactory.
And1sS Aug 19, 2025
6d2b4e5
Added unit tests for PbRuleEngineProcessedAuctionRequestHook.
And1sS Aug 19, 2025
a42788f
Merge branch 'refs/heads/master' into ruleengine
And1sS Aug 19, 2025
aa8fb63
Fixed styling.
And1sS Aug 19, 2025
28c472f
Made conditional rule throw NoMatchingException when no condition mat…
And1sS Sep 1, 2025
06c27ae
Fixes after review.
And1sS Sep 3, 2025
8d7c203
Merge branch 'master' into ruleengine
And1sS Sep 3, 2025
ef511de
Bumped module version.
And1sS Sep 3, 2025
724eadc
Fixed styling.
And1sS Sep 3, 2025
31ae549
Added checks for ambiguity.
And1sS Sep 19, 2025
996c887
Merge branch 'master' into ruleengine
And1sS Sep 22, 2025
7e07e46
Updates after merging master.
And1sS Sep 22, 2025
00dabfe
Added support for rejection tracking.
And1sS Sep 22, 2025
2cbeb01
Removed unnecessary @Accessors annotation.
And1sS Sep 22, 2025
bbd7a9b
Minor fixes.
And1sS Sep 29, 2025
cb7aeaf
Minor fixes.
And1sS Sep 30, 2025
b1f47a7
Test: Rule Engine (#4103)
marki1an Oct 5, 2025
471a317
fix tests
osulzhenko Oct 5, 2025
c30f624
Added jackson parsing module for instant.
And1sS Oct 5, 2025
39fd337
Added debug log for successfull parsing.
And1sS Oct 6, 2025
90a202f
Update caching mechanism for function tests
marki1an Oct 6, 2025
b2c25bb
Add @Retry annotation for rule engine base spec
marki1an Oct 6, 2025
6ed12b2
Add retry for rule engine tests
osulzhenko Oct 6, 2025
301023f
prevents timeout issues on slow pipelines
osulzhenko Oct 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions extra/bundle/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
<artifactId>live-intent-omni-channel-identity</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.prebid.server.hooks.modules</groupId>
<artifactId>pb-rule-engine</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<build>
Expand Down
15 changes: 15 additions & 0 deletions extra/modules/pb-rule-engine/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.prebid.server.hooks.modules</groupId>
<artifactId>all-modules</artifactId>
<version>3.33.0-SNAPSHOT</version>
</parent>

<artifactId>pb-rule-engine</artifactId>

<name>pb-rule-engine</name>
<description>Rule engine module</description>
</project>
1 change: 1 addition & 0 deletions extra/modules/pb-rule-engine/src/lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lombok.anyConstructor.addConstructorProperties = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.prebid.server.hooks.modules.rule.engine.config;

import com.iab.openrtb.request.BidRequest;
import io.vertx.core.Vertx;
import org.prebid.server.bidder.BidderCatalog;
import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy;
import org.prebid.server.hooks.execution.model.Stage;
import org.prebid.server.hooks.modules.rule.engine.core.config.AccountConfigParser;
import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser;
import org.prebid.server.hooks.modules.rule.engine.core.config.StageConfigParser;
import org.prebid.server.hooks.modules.rule.engine.core.request.RequestConditionalRuleFactory;
import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
import org.prebid.server.hooks.modules.rule.engine.core.request.RequestStageSpecification;
import org.prebid.server.hooks.modules.rule.engine.v1.PbRuleEngineModule;
import org.prebid.server.json.ObjectMapperProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Clock;
import java.util.concurrent.ThreadLocalRandom;
import java.util.random.RandomGenerator;

@Configuration
@ConditionalOnProperty(prefix = "hooks." + PbRuleEngineModule.CODE, name = "enabled", havingValue = "true")
public class PbRuleEngineModuleConfiguration {

@Bean
PbRuleEngineModule ruleEngineModule(RuleParser ruleParser,
@Value("${datacenter-region:#{null}}") String datacenter) {

return new PbRuleEngineModule(ruleParser, datacenter);
}

@Bean
StageConfigParser<BidRequest, RequestRuleContext> processedAuctionRequestStageParser(
BidderCatalog bidderCatalog) {

final RandomGenerator randomGenerator = () -> ThreadLocalRandom.current().nextLong();

return new StageConfigParser<>(
randomGenerator,
Stage.processed_auction_request,
new RequestStageSpecification(ObjectMapperProvider.mapper(), bidderCatalog, randomGenerator),
new RequestConditionalRuleFactory());
}

@Bean
AccountConfigParser accountConfigParser(
StageConfigParser<BidRequest, RequestRuleContext> processedAuctionRequestStageParser) {

return new AccountConfigParser(ObjectMapperProvider.mapper(), processedAuctionRequestStageParser);
}

@Bean
RuleParser ruleParser(
@Value("${hooks.pb-rule-engine.rule-cache.expire-after-minutes}") long cacheExpireAfterMinutes,
@Value("${hooks.pb-rule-engine.rule-cache.max-size}") long cacheMaxSize,
@Value("${hooks.pb-rule-engine.rule-parsing.retry-initial-delay-millis}") long delay,
@Value("${hooks.pb-rule-engine.rule-parsing.retry-max-delay-millis}") long maxDelay,
@Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-factor}") double factor,
@Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-jitter}") double jitter,
AccountConfigParser accountConfigParser,
Vertx vertx,
Clock clock) {

return new RuleParser(
cacheExpireAfterMinutes,
cacheMaxSize,
ExponentialBackoffRetryPolicy.of(delay, maxDelay, factor, jitter),
accountConfigParser,
vertx,
clock);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.prebid.server.hooks.modules.rule.engine.core.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig;
import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule;
import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule;

import java.util.Objects;

public class AccountConfigParser {

private final ObjectMapper mapper;
private final StageConfigParser<BidRequest, RequestRuleContext> processedAuctionRequestStageParser;

public AccountConfigParser(
ObjectMapper mapper,
StageConfigParser<BidRequest, RequestRuleContext> processedAuctionRequestStageParser) {

this.mapper = Objects.requireNonNull(mapper);
this.processedAuctionRequestStageParser = Objects.requireNonNull(processedAuctionRequestStageParser);
}

public PerStageRule parse(ObjectNode accountConfig) {
final AccountConfig parsedConfig;
try {
parsedConfig = mapper.treeToValue(accountConfig, AccountConfig.class);
} catch (JsonProcessingException e) {
throw new PreBidException(e.getMessage());
}

if (!parsedConfig.isEnabled()) {
return PerStageRule.builder()
.timestamp(parsedConfig.getTimestamp())
.processedAuctionRequestRule(NoOpRule.create())
.build();
}

return PerStageRule.builder()
.timestamp(parsedConfig.getTimestamp())
.processedAuctionRequestRule(processedAuctionRequestStageParser.parse(parsedConfig))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package org.prebid.server.hooks.modules.rule.engine.core.config;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import org.apache.commons.lang3.ObjectUtils;
import org.prebid.server.execution.retry.RetryPolicy;
import org.prebid.server.execution.retry.Retryable;
import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule;
import org.prebid.server.log.Logger;
import org.prebid.server.log.LoggerFactory;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

public class RuleParser {

private static final Logger logger = LoggerFactory.getLogger(RuleParser.class);

private final AccountConfigParser parser;
private final Vertx vertx;
private final Clock clock;

private final RetryPolicy retryPolicy;

private final Map<String, ParsingAttempt> accountIdToParsingAttempt;
private final Map<String, PerStageRule> accountIdToRules;

public RuleParser(long cacheExpireAfterMinutes,
long cacheMaxSize,
RetryPolicy retryPolicy,
AccountConfigParser parser,
Vertx vertx,
Clock clock) {

this.parser = Objects.requireNonNull(parser);
this.vertx = Objects.requireNonNull(vertx);
this.clock = Objects.requireNonNull(clock);
this.retryPolicy = Objects.requireNonNull(retryPolicy);

this.accountIdToParsingAttempt = Caffeine.newBuilder()
.expireAfterAccess(cacheExpireAfterMinutes, TimeUnit.MINUTES)
.maximumSize(cacheMaxSize)
.<String, ParsingAttempt>build()
.asMap();

this.accountIdToRules = Caffeine.newBuilder()
.expireAfterAccess(cacheExpireAfterMinutes, TimeUnit.MINUTES)
.maximumSize(cacheMaxSize)
.<String, PerStageRule>build()
.asMap();
}

public Future<PerStageRule> parseForAccount(String accountId, ObjectNode config) {
final PerStageRule cachedRule = accountIdToRules.get(accountId);

if (cachedRule != null && cachedRule.timestamp().compareTo(getConfigTimestamp(config)) >= 0) {
return Future.succeededFuture(cachedRule);
}

parseConfig(accountId, config);
return Future.succeededFuture(ObjectUtils.defaultIfNull(cachedRule, PerStageRule.noOp()));
}

private Instant getConfigTimestamp(ObjectNode config) {
try {
return Optional.of(config)
.map(node -> node.get("timestamp"))
.filter(JsonNode::isTextual)
.map(JsonNode::asText)
.map(Instant::parse)
.orElse(Instant.EPOCH);
} catch (DateTimeParseException exception) {
return Instant.EPOCH;
}
}

private void parseConfig(String accountId, ObjectNode config) {
final Instant now = clock.instant();
final ParsingAttempt attempt = accountIdToParsingAttempt.compute(
accountId, (ignored, previousAttempt) -> tryRegisteringNewAttempt(previousAttempt, now));

// reference equality used on purpose - if references are equal - then we should parse
if (attempt.timestamp() == now) {
logger.info("Parsing rule for account {}", accountId);
vertx.executeBlocking(() -> parser.parse(config))
.onSuccess(result -> succeedParsingAttempt(accountId, result))
.onFailure(error -> failParsingAttempt(accountId, attempt, error));
}
}

private ParsingAttempt tryRegisteringNewAttempt(ParsingAttempt previousAttempt, Instant currentAttemptStart) {
if (previousAttempt == null) {
return new ParsingAttempt.InProgress(currentAttemptStart, retryPolicy);
}

if (previousAttempt instanceof ParsingAttempt.InProgress) {
return previousAttempt;
}

if (previousAttempt.retryPolicy() instanceof Retryable previousAttemptRetryPolicy) {
final Instant previouslyDecidedToRetryAfter = previousAttempt.timestamp().plus(
Duration.ofMillis(previousAttemptRetryPolicy.delay()));

return previouslyDecidedToRetryAfter.isBefore(currentAttemptStart)
? new ParsingAttempt.InProgress(currentAttemptStart, previousAttemptRetryPolicy.next())
: previousAttempt;
}

return previousAttempt;
}

private void succeedParsingAttempt(String accountId, PerStageRule result) {
accountIdToRules.put(accountId, result);
accountIdToParsingAttempt.remove(accountId);

logger.debug("Successfully parsed rule-engine config for account {}", accountId);
}

private void failParsingAttempt(String accountId, ParsingAttempt attempt, Throwable cause) {
accountIdToParsingAttempt.put(accountId, ((ParsingAttempt.InProgress) attempt).failed());

logger.error(
"Failed to parse rule-engine config for account %s: %s".formatted(accountId, cause.getMessage()),
cause);
}

private sealed interface ParsingAttempt {

Instant timestamp();

RetryPolicy retryPolicy();

record Failed(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt {
}

record InProgress(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt {

public Failed failed() {
return new Failed(timestamp, retryPolicy);
}
}
}
}
Loading
Loading