-
Notifications
You must be signed in to change notification settings - Fork 249
Handle GVL deletions #4519
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
Open
Lightwood13
wants to merge
11
commits into
master
Choose a base branch
from
handle-gvl-deletions
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Handle GVL deletions #4519
Changes from 6 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
206c088
Implemented handling of gvl deletions
Lightwood13 e2f8349
Merge branch 'refs/heads/master' into handle-gvl-deletions
osulzhenko 8aa7fdc
Refactor: separated disk related functionality of VendorListService
Lightwood13 844a3cb
Added validation of live vendor list and removed exception on empty d…
Lightwood13 6c750b4
Added validation that vendor list is not deleted in requested GVL
Lightwood13 22555dd
Added loading of live vendor list from disk cache on startup
Lightwood13 0dbfefd
Tests: Handle GVL deletions (#4470)
osulzhenko f9186e4
Refactor
Lightwood13 6427d7c
Merge branch 'master' into handle-gvl-deletions
Lightwood13 67e27ab
Fix comments
Lightwood13 0a1e601
update functional tests
osulzhenko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,6 +131,7 @@ Following metrics are collected and submitted if account is configured with `det | |
| - `privacy.tcf.(v1,v2).in-geo` - number of requests received from TCF-concerned geo region with consent string of particular version | ||
| - `privacy.tcf.(v1,v2).out-geo` - number of requests received outside of TCF-concerned geo region with consent string of particular version | ||
| - `privacy.tcf.(v1,v2).vendorlist.(missing|ok|err|fallback)` - number of processed vendor lists of particular version | ||
| - `privacy.tcf.vendorlist.latest.(ok|err)` - number of successful or failed refreshes of the live GVL used for deleted-vendor detection | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - `privacy.usp.specified` - number of requests with a valid US Privacy string (CCPA) | ||
| - `privacy.usp.opt-out` - number of requests that required privacy enforcement according to CCPA rules | ||
| - `privacy.lmt` - number of requests that required privacy enforcement according to LMT flag | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
src/main/java/org/prebid/server/privacy/gdpr/vendorlist/LiveVendorListService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| package org.prebid.server.privacy.gdpr.vendorlist; | ||
|
|
||
| import io.vertx.core.Promise; | ||
| import io.vertx.core.Vertx; | ||
| import org.prebid.server.exception.PreBidException; | ||
| import org.prebid.server.json.JacksonMapper; | ||
| import org.prebid.server.log.Logger; | ||
| import org.prebid.server.log.LoggerFactory; | ||
| import org.prebid.server.metric.Metrics; | ||
| import org.prebid.server.privacy.gdpr.vendorlist.proto.Vendor; | ||
| import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorList; | ||
| import org.prebid.server.util.HttpUtil; | ||
| import org.prebid.server.vertx.Initializable; | ||
| import org.prebid.server.vertx.httpclient.HttpClient; | ||
| import org.prebid.server.vertx.httpclient.model.HttpClientResponse; | ||
|
|
||
| import java.time.Clock; | ||
| import java.time.Instant; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class LiveVendorListService implements Initializable { | ||
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(LiveVendorListService.class); | ||
|
|
||
| private final String cacheDir; | ||
| private final String liveGvlUrl; | ||
| private final long refreshPeriodMs; | ||
| private final int defaultTimeoutMs; | ||
| private final Vertx vertx; | ||
| private final HttpClient httpClient; | ||
| private final VendorListFileStore vendorListFileStore; | ||
| private final Metrics metrics; | ||
| private final JacksonMapper mapper; | ||
| private final Clock clock; | ||
|
|
||
| private volatile Set<Integer> deletedVendorIds = Set.of(); | ||
|
|
||
| public LiveVendorListService(String cacheDir, | ||
| String liveGvlUrl, | ||
| long refreshPeriodMs, | ||
| int defaultTimeoutMs, | ||
| Vertx vertx, | ||
| HttpClient httpClient, | ||
| VendorListFileStore vendorListFileStore, | ||
| Metrics metrics, | ||
| JacksonMapper mapper, | ||
| Clock clock) { | ||
|
|
||
| this.cacheDir = Objects.requireNonNull(cacheDir); | ||
| this.liveGvlUrl = HttpUtil.validateUrl(Objects.requireNonNull(liveGvlUrl)); | ||
| this.refreshPeriodMs = refreshPeriodMs; | ||
| this.defaultTimeoutMs = defaultTimeoutMs; | ||
| this.vertx = Objects.requireNonNull(vertx); | ||
| this.httpClient = Objects.requireNonNull(httpClient); | ||
| this.vendorListFileStore = Objects.requireNonNull(vendorListFileStore); | ||
| this.metrics = Objects.requireNonNull(metrics); | ||
| this.mapper = Objects.requireNonNull(mapper); | ||
| this.clock = Objects.requireNonNull(clock); | ||
| } | ||
|
|
||
| public boolean isDeleted(Integer id) { | ||
| final Set<Integer> ids = deletedVendorIds; | ||
| return !ids.isEmpty() && ids.contains(id); | ||
| } | ||
|
|
||
| @Override | ||
| public void initialize(Promise<Void> initializePromise) { | ||
| initializeWithLatestCachedVersion(); | ||
| vertx.setPeriodic(0, refreshPeriodMs, ignored -> refresh()); | ||
|
|
||
| initializePromise.tryComplete(); | ||
| } | ||
|
|
||
| private void initializeWithLatestCachedVersion() { | ||
| vendorListFileStore.getLatestVendorListFromCache(cacheDir).ifPresent(vendorList -> { | ||
| saveDeletedVendorsFromVendorList(vendorList); | ||
| logger.info("Initialized live GVL from cache with version %d".formatted(vendorList.getVendorListVersion())); | ||
| }); | ||
| } | ||
|
|
||
| void refresh() { | ||
| httpClient.get(liveGvlUrl, defaultTimeoutMs) | ||
| .map(this::processResponse) | ||
| .map(this::saveDeletedVendorsFromVendorList) | ||
| .otherwise(this::handleError); | ||
| } | ||
|
|
||
| private Void saveDeletedVendorsFromVendorList(VendorList vendorList) { | ||
| updateDeletedVendorIds(extractDeletedVendorIds(vendorList)); | ||
| return null; | ||
| } | ||
|
|
||
| private VendorList processResponse(HttpClientResponse response) { | ||
| final int statusCode = response.getStatusCode(); | ||
| if (statusCode != 200) { | ||
| throw new PreBidException("HTTP status code " + statusCode); | ||
| } | ||
|
|
||
| final String body = response.getBody(); | ||
| final VendorList vendorList = VendorListUtil.parseVendorList(body, mapper); | ||
|
|
||
| if (!VendorListUtil.vendorListIsValid(vendorList)) { | ||
| throw new PreBidException("Fetched vendor list parsed but has invalid data: " + body); | ||
| } | ||
|
|
||
| return vendorList; | ||
| } | ||
|
|
||
| Set<Integer> extractDeletedVendorIds(VendorList vendorList) { | ||
| final Instant now = clock.instant(); | ||
| return vendorList.getVendors().values().stream() | ||
| .filter(vendor -> VendorListUtil.vendorIsDeletedAt(vendor, now)) | ||
| .map(Vendor::getId) | ||
| .filter(Objects::nonNull) | ||
| .collect(Collectors.toUnmodifiableSet()); | ||
| } | ||
|
|
||
| private Void updateDeletedVendorIds(Set<Integer> ids) { | ||
| deletedVendorIds = ids; | ||
| metrics.updatePrivacyTcfVendorListLatestOkMetric(); | ||
| return null; | ||
| } | ||
|
|
||
| private Void handleError(Throwable exception) { | ||
| logger.warn("Error occurred while fetching live GVL", exception); | ||
| metrics.updatePrivacyTcfVendorListLatestErrorMetric(); | ||
| return null; | ||
| } | ||
| } |
134 changes: 134 additions & 0 deletions
134
src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListFileStore.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| package org.prebid.server.privacy.gdpr.vendorlist; | ||
|
|
||
| import com.github.benmanes.caffeine.cache.Caffeine; | ||
| import io.vertx.core.Future; | ||
| import io.vertx.core.Promise; | ||
| import io.vertx.core.buffer.Buffer; | ||
| import io.vertx.core.file.FileProps; | ||
| import io.vertx.core.file.FileSystem; | ||
| import io.vertx.core.file.FileSystemException; | ||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.apache.commons.lang3.exception.ExceptionUtils; | ||
| import org.prebid.server.exception.PreBidException; | ||
| import org.prebid.server.json.JacksonMapper; | ||
| import org.prebid.server.log.ConditionalLogger; | ||
| import org.prebid.server.log.Logger; | ||
| import org.prebid.server.log.LoggerFactory; | ||
| import org.prebid.server.privacy.gdpr.vendorlist.proto.Vendor; | ||
| import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorList; | ||
|
|
||
| import java.io.File; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Paths; | ||
| import java.util.Comparator; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class VendorListFileStore { | ||
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(VendorListFileStore.class); | ||
| private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); | ||
|
|
||
| private static final String JSON_SUFFIX = ".json"; | ||
|
|
||
| private final double logSamplingRate; | ||
| private final FileSystem fileSystem; | ||
| private final JacksonMapper mapper; | ||
|
|
||
| public VendorListFileStore(double logSamplingRate, | ||
| FileSystem fileSystem, | ||
| JacksonMapper mapper) { | ||
|
|
||
| this.logSamplingRate = logSamplingRate; | ||
| this.fileSystem = Objects.requireNonNull(fileSystem); | ||
| this.mapper = Objects.requireNonNull(mapper); | ||
| } | ||
|
|
||
| Map<Integer, Map<Integer, Vendor>> createCacheFromDisk(String cacheDir) { | ||
| createAndCheckWritePermissionsForCacheDir(cacheDir); | ||
| final Map<Integer, String> versionToFileContent = readFileSystemCache(cacheDir); | ||
|
|
||
| final Map<Integer, Map<Integer, Vendor>> cache = Caffeine.newBuilder() | ||
| .<Integer, Map<Integer, Vendor>>build() | ||
| .asMap(); | ||
|
|
||
| for (Map.Entry<Integer, String> versionAndFileContent : versionToFileContent.entrySet()) { | ||
| final VendorList vendorList = VendorListUtil.parseVendorList(versionAndFileContent.getValue(), mapper); | ||
|
|
||
| cache.put(versionAndFileContent.getKey(), vendorList.getVendors()); | ||
| } | ||
| return cache; | ||
| } | ||
|
|
||
| private void createAndCheckWritePermissionsForCacheDir(String cacheDir) { | ||
| final FileProps props = fileSystem.existsBlocking(cacheDir) ? fileSystem.propsBlocking(cacheDir) : null; | ||
| if (props == null || !props.isDirectory()) { | ||
| try { | ||
| fileSystem.mkdirsBlocking(cacheDir); | ||
| } catch (FileSystemException e) { | ||
| throw new PreBidException("Cannot create directory: " + cacheDir, e); | ||
| } | ||
| } else if (!Files.isWritable(Paths.get(cacheDir))) { | ||
| throw new PreBidException("No write permissions for directory: " + cacheDir); | ||
| } | ||
| } | ||
|
|
||
| private Map<Integer, String> readFileSystemCache(String cacheDir) { | ||
| return fileSystem.readDirBlocking(cacheDir).stream() | ||
| .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) | ||
| .collect(Collectors.toMap(VendorListFileStore::parseCachedFileVersion, | ||
| filename -> fileSystem.readFileBlocking(filename).toString())); | ||
|
And1sS marked this conversation as resolved.
|
||
| } | ||
|
|
||
| Optional<VendorList> getLatestVendorListFromCache(String cacheDir) { | ||
| createAndCheckWritePermissionsForCacheDir(cacheDir); | ||
| return fileSystem.readDirBlocking(cacheDir).stream() | ||
| .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) | ||
| .max(Comparator.comparing(VendorListFileStore::parseCachedFileVersion)) | ||
| .map(fileSystem::readFileBlocking) | ||
| .map(Buffer::toString) | ||
| .map(content -> VendorListUtil.parseVendorList(content, mapper)); | ||
| } | ||
|
|
||
| private static Integer parseCachedFileVersion(String filepath) { | ||
| final String filename = new File(filepath).getName(); | ||
| final String filenameWithoutExtension = StringUtils.removeEnd(filename, JSON_SUFFIX); | ||
| return Integer.valueOf(filenameWithoutExtension); | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
And1sS marked this conversation as resolved.
Dismissed
|
||
| } | ||
|
|
||
| Future<VendorListResult> saveToFile(VendorListResult vendorListResult, String cacheDir, String generationVersion) { | ||
| final Promise<VendorListResult> promise = Promise.promise(); | ||
| final int version = vendorListResult.getVersion(); | ||
| final String filepath = new File(cacheDir, version + JSON_SUFFIX).getPath(); | ||
|
|
||
| fileSystem.writeFile(filepath, Buffer.buffer(vendorListResult.getVendorListAsString()), result -> { | ||
| if (result.succeeded()) { | ||
| promise.complete(vendorListResult); | ||
| } else { | ||
| conditionalLogger.error( | ||
| "Could not create new vendor list for version %s.%s, file: %s, trace: %s".formatted( | ||
| generationVersion, version, filepath, ExceptionUtils.getStackTrace(result.cause())), | ||
| logSamplingRate); | ||
| promise.fail(result.cause()); | ||
| } | ||
| }); | ||
|
|
||
| return promise.future(); | ||
| } | ||
|
|
||
| Map<Integer, Vendor> readFallbackVendorList(String fallbackVendorListPath) { | ||
| if (StringUtils.isBlank(fallbackVendorListPath)) { | ||
| return null; | ||
| } | ||
|
|
||
| final String vendorListContent = fileSystem.readFileBlocking(fallbackVendorListPath).toString(); | ||
| final VendorList vendorList = VendorListUtil.parseVendorList(vendorListContent, mapper); | ||
| if (!VendorListUtil.vendorListIsValid(vendorList)) { | ||
| throw new PreBidException("Fallback vendor list parsed but has invalid data: " + vendorListContent); | ||
| } | ||
|
|
||
| return vendorList.getVendors(); | ||
| } | ||
| } | ||
14 changes: 14 additions & 0 deletions
14
src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListResult.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.prebid.server.privacy.gdpr.vendorlist; | ||
|
|
||
| import lombok.Value; | ||
| import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorList; | ||
|
|
||
| @Value(staticConstructor = "of") | ||
| class VendorListResult { | ||
|
|
||
| int version; | ||
|
|
||
| String vendorListAsString; | ||
|
|
||
| VendorList vendorList; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
startup-cache-dir