[server] Isolate consumers by store encryption policy - #2953
Conversation
Resolve consumer decryption from the target store only when the cluster encryption property is absent. Keep explicit cluster true and false authoritative, and key shared consumer services by resolved broker URL plus decryption mode so stores cannot inherit another store's policy. Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
🟡 Not ready to approve
There are a couple of concrete correctness/diagnostics issues (notably a potential NPE regression on Kafka URL resolution and diagnostic map-merging overwrites) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR updates Da Vinci server ingestion to prevent shared PubSub consumer pools from leaking decryption/encryption policy across stores that share the same broker, by keying shared consumer services on both resolved broker URL and the resolved “decryption enabled” mode (derived from cluster.encryption.enabled when explicitly set, otherwise falling back to Store.isEncryptionEnabled()).
Changes:
- Resolve per-consumer decryption mode with explicit cluster property precedence and store-level fallback, defaulting missing stores to plaintext.
- Key
AggKafkaConsumerService’s shared consumer-service pools by(resolvedKafkaUrl, decryptionEnabled)and route subscriptions/diagnostics across both pools. - Add/extend unit tests to cover precedence, isolation across encrypted/plaintext stores on the same broker, and retryable creation failures.
File summaries
| File | Description |
|---|---|
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/AggKafkaConsumerService.java | Introduces typed consumer-service key including decryption mode; updates lookup, subscribe, metrics, and diagnostics to handle both pools. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/StoreIngestionTask.java | Resolves cluster.encryption.enabled with store fallback and stamps the resolved value into consumer properties; subscription uses the exact selected consumer service. |
| clients/da-vinci-client/src/test/java/com/linkedin/davinci/kafka/consumer/AggKafkaConsumerServiceTest.java | Adds tests for config precedence, pool isolation, retryable creation failure, and diagnostics across both consumer modes. |
Review details
Suppressed comments (1)
clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/AggKafkaConsumerService.java:719
- In
getIngestionInfoFor(..., regionName), merging results withputAll(...)can overwrite entries if both consumer modes return the samePubSubTopicPartitionkey, which would hide one mode's diagnostics. Consider merging withputIfAbsent(...)and logging when a duplicate key is encountered.
if (consumerService != null) {
consumerServiceFound = true;
topicPartitionIngestionInfoMap
.putAll(consumerService.getIngestionInfoFor(versionTopic, pubSubTopicPartition, true));
}
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| AbstractKafkaConsumerService getKafkaConsumerService(final String kafkaURL, boolean decryptionEnabled) { | ||
| String resolvedKafkaUrl = kafkaClusterUrlResolver == null ? kafkaURL : kafkaClusterUrlResolver.apply(kafkaURL); | ||
| return kafkaServerToConsumerServiceMap.get(new ConsumerServiceKey(resolvedKafkaUrl, decryptionEnabled)); | ||
| } |
| for (Map.Entry<ConsumerServiceKey, AbstractKafkaConsumerService> consumerServiceEntry: kafkaServerToConsumerServiceMap | ||
| .entrySet()) { | ||
| String kafkaUrl = consumerServiceEntry.getKey().getResolvedKafkaUrl(); | ||
| AbstractKafkaConsumerService consumerService = consumerServiceEntry.getValue(); | ||
| Map<PubSubTopicPartition, TopicPartitionIngestionInfo> topicPartitionIngestionInfoMap = | ||
| consumerService.getIngestionInfoFor(versionTopic, pubSubTopicPartition, false); | ||
| for (Map.Entry<PubSubTopicPartition, TopicPartitionIngestionInfo> entry: topicPartitionIngestionInfoMap |
Problem Statement
Server ingestion currently pools shared PubSub consumers only by resolved broker URL. When
cluster.encryption.enabledis absent, that design cannot honor a target store encryption setting, and the first consumer created for a broker can incorrectly determine decryption behavior for other stores sharing the same broker.The cluster property must remain authoritative when explicitly set to either
trueorfalse. Store-level fallback applies only when it is absent. Admin topics and metadata fetchers remain plaintext because they do not have a target store.Solution
Resolve server-ingestion consumer decryption from the explicit cluster property when present; otherwise fall back to the target
Store.isEncryptionEnabled()value. A missing store resolves to plaintext.Shared consumer services are now keyed by a typed key containing the resolved broker URL and decryption mode. Creation returns the exact service selected for that policy, and subscription uses that service directly, preventing policy leakage between encrypted and plaintext stores on the same broker. Lookup, diagnostics, stale-consumer reporting, unsubscribe, and shutdown paths cover both pools.
This change depends on the configured PubSub consumer adapter honoring
cluster.encryption.enabled:truemust construct a decrypting/raw consumer path, whilefalsemust construct the normal plaintext consumer.Code changes
cluster.encryption.enabled; explicittrueorfalsewins, while an absent property falls back to store encryption and defaults missing stores to plaintext.Concurrency-Specific Checks
Both reviewer and PR author to verify
synchronized,RWLock) are used where needed.ConcurrentHashMap,CopyOnWriteArrayList).How was this PR tested?
Testing Done
./gradlew :clients:da-vinci-client:test --tests com.linkedin.davinci.kafka.consumer.AggKafkaConsumerServiceTest./gradlew :clients:da-vinci-client:test --tests com.linkedin.davinci.kafka.consumer.StoreIngestionTaskTest --tests com.linkedin.davinci.kafka.consumer.KafkaStoreIngestionServiceTest./gradlew spotlessJavaCheck :clients:da-vinci-client:assemblespotlessApplyandgenerateGHCICoverage includes explicit cluster true, explicit cluster false with an encrypted store, absent property with encrypted/plaintext/missing stores, mixed encrypted and plaintext stores sharing one broker, property propagation to consumer creation, reporting and cleanup across both modes, and creation failure propagation with a successful retry.
Does this PR introduce any user-facing or breaking changes?
🤖 Generated with GitHub Copilot CLI