Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

GVL: Add backoff for vendor list fetching #2919

Merged
merged 6 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public class ExponentialBackoffRetryPolicy implements Retryable {
@Override
public RetryPolicy next() {
final long nextDelay = (long) Math.min(delay * factor, maxDelayMillis);
final long variedDelay = nextDelay + (long) ThreadLocalRandom.current().nextDouble(nextDelay * jitter);
final long variedDelay = Math.min(
nextDelay + (long) ThreadLocalRandom.current().nextDouble(nextDelay * jitter), maxDelayMillis);
return of(variedDelay, maxDelayMillis, factor, jitter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.prebid.server.privacy.gdpr.vendorlist;

import lombok.Value;
import org.prebid.server.execution.retry.RetryPolicy;
import org.prebid.server.execution.retry.Retryable;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/*
* This class is a dirty hack, that will be removed with Vertx update to 4.0
* TODO: Replace with Vertx 4.0 Circuit breaker's backoff.
*/
public class VendorListFetchThrottler {

private final Map<Integer, FetchAttempt> versionToFetchAttempt;
private final RetryPolicy retryPolicy;
private final Clock clock;

public VendorListFetchThrottler(RetryPolicy retryPolicy, Clock clock) {
this.retryPolicy = Objects.requireNonNull(retryPolicy);
this.clock = Objects.requireNonNull(clock);

versionToFetchAttempt = new ConcurrentHashMap<>();
}

public boolean registerFetchAttempt(int version) {
final Instant now = clock.instant();
final FetchAttempt computedAttempt = versionToFetchAttempt.compute(
version, (ignored, previousAttempt) -> resolveAttempt(previousAttempt, now));

// Memory address of object returned by Instant.now() is used as unique identifier of attempt.
// If memory address of computed `computedAttempt.attemptedAt` is equal to the `now` that we provided for
// resolving, then it is our attempt, and we can fetch vendor list.
return computedAttempt.attemptedAt == now;
}

private FetchAttempt resolveAttempt(FetchAttempt previousAttempt, Instant currentAttemptStart) {
if (previousAttempt == null) {
return FetchAttempt.of(retryPolicy, currentAttemptStart);
}

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

return previouslyDecidedToRetryAfter.isBefore(currentAttemptStart)
? FetchAttempt.of(previousAttemptRetryPolicy.next(), currentAttemptStart)
: previousAttempt;
}

return previousAttempt;
}

public void succeedFetchAttempt(int version) {
versionToFetchAttempt.remove(version);
}

@Value(staticConstructor = "of")
private static class FetchAttempt {

RetryPolicy retryPolicy;

Instant attemptedAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public class VendorListService {

private final Map<Integer, Vendor> fallbackVendorList;
private final Set<Integer> versionsToFallback;
private final VendorListFetchThrottler fetchThrottler;

public VendorListService(double logSamplingRate,
String cacheDir,
Expand All @@ -92,7 +93,8 @@ public VendorListService(double logSamplingRate,
HttpClient httpClient,
Metrics metrics,
String generationVersion,
JacksonMapper mapper) {
JacksonMapper mapper,
VendorListFetchThrottler fetchThrottler) {

this.logSamplingRate = logSamplingRate;
this.cacheDir = Objects.requireNonNull(cacheDir);
Expand All @@ -106,6 +108,7 @@ public VendorListService(double logSamplingRate,
this.httpClient = Objects.requireNonNull(httpClient);
this.metrics = Objects.requireNonNull(metrics);
this.mapper = Objects.requireNonNull(mapper);
this.fetchThrottler = Objects.requireNonNull(fetchThrottler);

createAndCheckWritePermissionsFor(fileSystem, cacheDir);
cache = Objects.requireNonNull(createCache(fileSystem, cacheDir));
Expand Down Expand Up @@ -149,9 +152,11 @@ public Future<Map<Integer, Vendor>> forVersion(int version) {

metrics.updatePrivacyTcfVendorListMissingMetric(tcf);

logger.info("TCF {0} vendor list for version {1}.{2} not found, started downloading.",
tcf, generationVersion, version);
fetchNewVendorListFor(version);
if (fetchThrottler.registerFetchAttempt(version)) {
logger.info("TCF {0} vendor list for version {1}.{2} not found, started downloading.",
tcf, generationVersion, version);
fetchNewVendorListFor(version);
}

return Future.failedFuture("TCF %d vendor list for version %s.%d not fetched yet, try again later."
.formatted(tcf, generationVersion, version));
Expand Down Expand Up @@ -305,6 +310,7 @@ private VendorListResult<VendorList> processResponse(HttpClientResponse response
throw new PreBidException("Fetched vendor list parsed but has invalid data: " + body);
}

fetchThrottler.succeedFetchAttempt(version);
return VendorListResult.of(version, body, vendorList);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.vertx.core.Vertx;
import io.vertx.core.file.FileSystem;
import lombok.Data;
import org.prebid.server.auction.IpAddressHelper;
import org.prebid.server.bidder.BidderCatalog;
import org.prebid.server.geolocation.GeoLocationService;
Expand All @@ -27,20 +28,27 @@
import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.PurposeTwoBasicEnforcePurposeStrategy;
import org.prebid.server.privacy.gdpr.tcfstrategies.specialfeature.SpecialFeaturesOneStrategy;
import org.prebid.server.privacy.gdpr.tcfstrategies.specialfeature.SpecialFeaturesStrategy;
import org.prebid.server.privacy.gdpr.vendorlist.VendorListFetchThrottler;
import org.prebid.server.privacy.gdpr.vendorlist.VendorListService;
import org.prebid.server.privacy.gdpr.vendorlist.VersionedVendorListService;
import org.prebid.server.settings.model.GdprConfig;
import org.prebid.server.settings.model.Purpose;
import org.prebid.server.settings.model.Purposes;
import org.prebid.server.settings.model.SpecialFeature;
import org.prebid.server.settings.model.SpecialFeatures;
import org.prebid.server.spring.config.retry.RetryPolicyConfigurationProperties;
import org.prebid.server.vertx.http.HttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.Clock;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
Expand All @@ -52,63 +60,71 @@ public class PrivacyServiceConfiguration {
@Bean
VendorListService vendorListServiceV2(
@Value("${logging.sampling-rate:0.01}") double logSamplingRate,
@Value("${gdpr.vendorlist.v2.cache-dir}") String cacheDir,
@Value("${gdpr.vendorlist.v2.http-endpoint-template}") String endpointTemplate,
@Value("${gdpr.vendorlist.default-timeout-ms}") int defaultTimeoutMs,
@Value("${gdpr.vendorlist.v2.refresh-missing-list-period-ms}") int refreshMissingListPeriodMs,
@Value("${gdpr.vendorlist.v2.fallback-vendor-list-path:#{null}}") String fallbackVendorListPath,
@Value("${gdpr.vendorlist.v2.deprecated}") boolean deprecated,
VendorListServiceConfigurationProperties vendorListServiceV2Properties,
Vertx vertx,
Clock clock,
FileSystem fileSystem,
HttpClient httpClient,
Metrics metrics,
JacksonMapper mapper) {

return new VendorListService(
logSamplingRate,
cacheDir,
endpointTemplate,
vendorListServiceV2Properties.getCacheDir(),
vendorListServiceV2Properties.getHttpEndpointTemplate(),
defaultTimeoutMs,
refreshMissingListPeriodMs,
deprecated,
fallbackVendorListPath,
vendorListServiceV2Properties.getRefreshMissingListPeriodMs(),
vendorListServiceV2Properties.getDeprecated(),
vendorListServiceV2Properties.getFallbackVendorListPath(),
vertx,
fileSystem,
httpClient,
metrics,
"v2",
mapper);
mapper,
new VendorListFetchThrottler(vendorListServiceV2Properties.getRetryPolicy().toPolicy(), clock));
}

@Bean
@ConfigurationProperties(prefix = "gdpr.vendorlist.v2")
VendorListServiceConfigurationProperties vendorListServiceV2Properties() {
return new VendorListServiceConfigurationProperties();
}

@Bean
VendorListService vendorListServiceV3(
@Value("${logging.sampling-rate:0.01}") double logSamplingRate,
@Value("${gdpr.vendorlist.v3.cache-dir}") String cacheDir,
@Value("${gdpr.vendorlist.v3.http-endpoint-template}") String endpointTemplate,
@Value("${gdpr.vendorlist.default-timeout-ms}") int defaultTimeoutMs,
@Value("${gdpr.vendorlist.v3.refresh-missing-list-period-ms}") int refreshMissingListPeriodMs,
@Value("${gdpr.vendorlist.v3.fallback-vendor-list-path:#{null}}") String fallbackVendorListPath,
@Value("${gdpr.vendorlist.v3.deprecated}") boolean deprecated,
VendorListServiceConfigurationProperties vendorListServiceV3Properties,
Vertx vertx,
Clock clock,
FileSystem fileSystem,
HttpClient httpClient,
Metrics metrics,
JacksonMapper mapper) {

return new VendorListService(
logSamplingRate,
cacheDir,
endpointTemplate,
vendorListServiceV3Properties.getCacheDir(),
vendorListServiceV3Properties.getHttpEndpointTemplate(),
defaultTimeoutMs,
refreshMissingListPeriodMs,
deprecated,
fallbackVendorListPath,
vendorListServiceV3Properties.getRefreshMissingListPeriodMs(),
vendorListServiceV3Properties.getDeprecated(),
vendorListServiceV3Properties.getFallbackVendorListPath(),
vertx,
fileSystem,
httpClient,
metrics,
"v3",
mapper);
mapper,
new VendorListFetchThrottler(vendorListServiceV3Properties.getRetryPolicy().toPolicy(), clock));
}

@Bean
@ConfigurationProperties(prefix = "gdpr.vendorlist.v3")
VendorListServiceConfigurationProperties vendorListServiceV3Properties() {
return new VendorListServiceConfigurationProperties();
}

@Bean
Expand Down Expand Up @@ -295,4 +311,25 @@ SpecialFeatures specialFeatures() {
SpecialFeature specialFeature() {
return new SpecialFeature();
}

@Data
@Validated
public static class VendorListServiceConfigurationProperties {

@NotEmpty
String cacheDir;

@NotEmpty
String httpEndpointTemplate;

@Min(1)
int refreshMissingListPeriodMs;

String fallbackVendorListPath;

@NotNull
Boolean deprecated;

RetryPolicyConfigurationProperties retryPolicy;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.prebid.server.spring.config.retry;

import lombok.Data;
import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Min;
import javax.validation.constraints.Positive;

@Data
@Validated
public final class ExponentialBackoffRetryPolicyConfigurationProperties {

@Min(1)
long delayMillis;

@Min(1)
long maxDelayMillis;

@Positive
double factor;

@Positive
double jitter;

public ExponentialBackoffRetryPolicy toPolicy() {
return ExponentialBackoffRetryPolicy.of(delayMillis, maxDelayMillis, factor, jitter);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.prebid.server.spring.config.retry;

import lombok.Data;
import org.prebid.server.execution.retry.FixedIntervalRetryPolicy;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Min;

@Data
@Validated
public final class FixedIntervalRetryPolicyConfigurationProperties {

@Min(1)
long delay;

@Min(0)
Integer retriesLeft;

FixedIntervalRetryPolicy toPolicy() {
return retriesLeft == null
? FixedIntervalRetryPolicy.of(delay)
: FixedIntervalRetryPolicy.limited(delay, retriesLeft);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.prebid.server.spring.config.retry;

import lombok.Data;
import org.apache.commons.lang3.ObjectUtils;
import org.prebid.server.execution.retry.RetryPolicy;
import org.springframework.validation.annotation.Validated;

@Data
@Validated
public class RetryPolicyConfigurationProperties {

private ExponentialBackoffRetryPolicyConfigurationProperties exponentialBackoff;

private FixedIntervalRetryPolicyConfigurationProperties fixedInterval;

public RetryPolicy toPolicy() {
if (ObjectUtils.allNull(exponentialBackoff, fixedInterval)) {
throw new IllegalArgumentException("Invalid configuration of retry policy. No retry policy specified.");
}
if (ObjectUtils.allNotNull(exponentialBackoff, fixedInterval)) {
throw new IllegalArgumentException("Invalid configuration of retry policy."
+ " Should be either exponential backoff or fixed interval, but not both.");
}

return exponentialBackoff != null ? exponentialBackoff.toPolicy() : fixedInterval.toPolicy();
}
}
12 changes: 12 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,22 @@ gdpr:
http-endpoint-template: https://vendor-list.consensu.org/v2/archives/vendor-list-v{VERSION}.json
refresh-missing-list-period-ms: 3600000
deprecated: false
retry-policy:
exponential-backoff:
delay-millis: 60000
max-delay-millis: 120000
factor: 1.1
jitter: 0.2
v3:
http-endpoint-template: https://vendor-list.consensu.org/v3/archives/vendor-list-v{VERSION}.json
refresh-missing-list-period-ms: 3600000
deprecated: false
retry-policy:
exponential-backoff:
delay-millis: 60000
max-delay-millis: 120000
factor: 1.1
jitter: 0.2
purposes:
p1:
enforce-purpose: full
Expand Down
Loading
Loading