Skip to content

optable-targeting: Enhance Optable Targeting: Non-blocking network calls and granular traffic controls#2

Draft
softcoder594 wants to merge 25 commits into
masterfrom
optable-targeting-non-blocking-early-network-call
Draft

optable-targeting: Enhance Optable Targeting: Non-blocking network calls and granular traffic controls#2
softcoder594 wants to merge 25 commits into
masterfrom
optable-targeting-non-blocking-early-network-call

Conversation

@softcoder594
Copy link
Copy Markdown
Collaborator

@softcoder594 softcoder594 commented May 5, 2026

Type of changes

  • module update

What's the context?

This update reduces auction latency by initiating the Optable API call at the earliest hook stage (raw-auction-request) and deferring the wait to the per-bidder stage (bidder-request), where the enrichment data is actually injected. It also adds granular controls over which traffic and which bidders receive enrichment.

New Features

  1. Non-Blocking Early Network Call: The API call starts in raw-auction-request and runs in parallel with other auction processing. The result is consumed per-bidder in bidder-request, avoiding pipeline blocking.
  2. Per-Bidder Enrichment Percentage: Configurable sampling rate per bidder (0-100%) to control what fraction of bid requests receive enrichment.
  3. Web vs. App Traffic Toggle: enrich-web / enrich-app flags to select which traffic sources are enriched.

Backwards Compatibility

The processed-auction-request hook is retained as a legacy fallback. If operators have not migrated to the new execution plan, the module behaves as before — enrichment happens synchronously in the processed hook.

New Configuration (recommended)

hooks:
  modules:
    optable-targeting:
      enrichment-percentage: 100
      bidder-enrichment-percentages:
        appnexus: 75
        rubicon: 75
        pubmatic: 100
        criteo: 0
      enrich-web: true
      enrich-app: true

  execution-plan:
    endpoints:
      "/openrtb2/auction":
        stages:
          raw-auction-request:
            groups:
              - timeout: 50
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-raw-auction-request-hook"
          bidder-request:
            groups:
              - timeout: 50
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-bidder-request-hook"
          auction-response:
            groups:
              - timeout: 10
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-auction-response-hook"

Legacy Configuration (to be migrated)

The following execution plan fragment should be removed once migrated to the new configuration above:

          processed-auction-request:
            groups:
              - timeout: 600
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-processed-auction-request-hook"

The processed-auction-request hook detects whether the new hooks (raw-auction-request and bidder-request) are present in the execution plan. If both are active, it passes through immediately without blocking the pipeline. If the new hooks are absent, it falls back to the legacy synchronous behavior. This means the legacy fragment can be kept during migration without negating the latency benefit of the new configuration.

Test plan

  • Unit Tests
  • Manual Integration Tests

Quality check

  • Are your changes following our code style guidelines?
  • Are there any breaking changes in your code? No
  • Does your test coverage exceed 90%?
  • Are there any erroneous console logs, debuggers or leftover code in your changes? No

@softcoder594 softcoder594 changed the title optable-targeting: implement Non-Blocking Early Network Call optable-targeting: Enhance Optable Targeting: Non-blocking network calls and granular traffic controls May 25, 2026
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you avoid keeping "legacy" files. Those 2 files in this PR seem to not be referenced anywhere. It feels like you are just duplicating effort relative to git history. This causes bloat and will drift. Also, what does legacy mean when you'll have a third version ?

The README already describes the relevant content that was there before (the migration section). I think this is sufficient and those legacy files could be removed to keep it more tidy.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

return bidRequest;
}

final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer importing com.iab.openrtb.request.User

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment applies everywhere in this file, other modified files and for anything else that can be imported.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, there was another FQN that was colliding final org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User optableUser - replaced with var instead of type

Comment on lines +56 to +95
public boolean hasRawAuctionRequestHook(Account account) {
final String accountId = account != null ? account.getId() : null;

return StringUtils.isNotEmpty(accountId)
? rawAuctionRequestHookCache.computeIfAbsent(accountId, id -> {
final ExecutionPlan accountSpecificHooksExecutionPlan = resolveExecutionPlan(account);
return hasHook(
accountSpecificHooksExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION)
|| hasGlobalRawAuctionRequestHook;
})
: false;
}

public boolean hasBidderRequestHook(Account account) {
final String accountId = account != null ? account.getId() : null;

return StringUtils.isNotEmpty(accountId)
? bidderRequestHookCache.computeIfAbsent(accountId, id -> {
final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account);
return hasHook(
accountSpecificHoksExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)
|| hasGlobalBidderRequestHook;
})
: false;
}

public long getOptableTargetingBidderRequestTimeout(Account account) {
final String accountId = account != null ? account.getId() : null;

return StringUtils.isNotEmpty(accountId)
? bidderRequestHookTimeoutCache.computeIfAbsent(accountId, id -> {
final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account);
final long hookTimeOut = getHookTimeout(
accountSpecificHoksExecutionPlan,
STAGE_BIDDER_REQUEST,
HOOK_CODE_OPTABLE_BIDDER_REQUEST);
return hookTimeOut != 0 ? hookTimeOut : globalBidderRequestHookTimeout;
})
: globalBidderRequestHookTimeout;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is quite some code duplication in these 3 methods. Consider a refactor. Here is a Claude code suggestion where the logic of getting the accountId, then resolving the execution plan, then use the plan to do whatever evaluation, is written only once. The three initial methods then become one-liners.

    private static <T> T computeFromAccount(Account account,
            ConcurrentHashMap<String, T> cache,
            T defaultValue,
            Function<ExecutionPlan, T> compute) {
        final String accountId = account != null ? account.getId() : null;
        return StringUtils.isNotEmpty(accountId)
                ? cache.computeIfAbsent(accountId, id -> compute.apply(resolveExecutionPlan(account)))
                : defaultValue;
    }

    public boolean hasRawAuctionRequestHook(Account account) {
        return computeFromAccount(account, rawAuctionRequestHookCache, false,
                plan -> hasHook(plan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION)
                        || hasGlobalRawAuctionRequestHook);
    }

    public boolean hasBidderRequestHook(Account account) {
        return computeFromAccount(account, bidderRequestHookCache, false,
                plan -> hasHook(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)
                        || hasGlobalBidderRequestHook);
    }

    public long getOptableTargetingBidderRequestTimeout(Account account) {
        return computeFromAccount(account, bidderRequestHookTimeoutCache, globalBidderRequestHookTimeout,
                plan -> {
                    final long timeout = getHookTimeout(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST);
                    return timeout != 0 ? timeout : globalBidderRequestHookTimeout;
                });
    }

This change requires also resolveExecutionPlan to be static, which it can be.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@softcoder594 please take a look, as it is beyond cosmetical refactor for me, or we can just accept the suggestion of claude code, just verify we don't break anything


import java.util.Objects;

public class NetworkCall {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is vague, generic. Consider finding a more suitable name that properly describes the purpose of this class. Something like OptableTargetingHelper ? Or something related to applying privacy and other properties to the targeting request.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to TargetingRequestExecutor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest those validation functions are placed in the same package as OptableTargetingProperties. It will encourage reusability and reduce imports.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

@Test
public void shouldLimitGppSidToTwoValues() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this limit of 2 ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well because Targeting API processes only 2 gpp_sids... according to optable-sandbox code. maybe we were wrong

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I confirm, just found that in the sandbox code, didn't know about this limit 👍


// then
assertThat(query).contains("&gpp=DBABzw~1YNY~BVQqAAAAAgA");
assertThat(query).containsPattern("&gpp_sid=\\d+,\\d+");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should assert that the 2 values are indeed 7 and 22. I think order does not matter here ? But the actual values do.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return update(BidRequestCleaner.instance(), moduleContext);
}

public static <AuctionRequestPayload> Future<InvocationResult<AuctionRequestPayload>> update(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can remove the first <AuctionRequestPayload> type parameter


import java.util.Objects;

public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment here if this is the now deprecated hook

Copy link
Copy Markdown

@ericst-amand-optable ericst-amand-optable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Submitted some small comments/refactor. I did not try to run the server, nor the tests, so this is pure code review for what it is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants