[vpj] Materialize executor-side SSL before Spark TTL filter and chunk assembly - #2975
[vpj] Materialize executor-side SSL before Spark TTL filter and chunk assembly#2975pthirun wants to merge 4 commits into
Conversation
… assembly Only SparkPubSubPartitionReaderFactory.createReader() materialized SSL properties from the Hadoop token file before creating a PubSub consumer. Two other executor-side code paths that can also create a PubSub consumer - the TTL mapPartitions filter and the chunk-assembly flatMapGroups closure - built SparkKafkaInputTTLFilter/SparkChunkAssembler directly from broadcast properties with no SSL setup. When TTL filtering with dictionary compression is enabled, these components create a second PubSub (dictionary) consumer internally and failed with errors like "Missing required property ssl.keystore.type". Extract the existing SSL materialization logic into a shared VPJSSLUtils.setupSSLForExecutor() and call it from all three executor-side paths before constructing the PubSub-consuming component. Tests: - TestVPJSSLUtils: no-op path (no SSL configurator) and failure path (unresolvable Hadoop token file) for the new shared utility. - DataWriterSparkJobRepushTest: two regression tests that force actual Spark execution (collectAsList()) of the TTL filter and chunk assembly closures with an SSL configurator configured but no resolvable token file, asserting the SSL setup failure now surfaces from both paths. Co-authored-by: Copilot <[email protected]>
Ports the real behavioral tests from PR linkedin#2955 (SparkExecutorTestUtils, testApplyTTLFilterMaterializesSSLBeforeReadingZstdDictionary, testCreateReaderMaterializesExecutorSSL) to validate this fix against the same scenarios, plus an adapted chunk-assembly test (testApplyChunkAssemblyMaterializesSSLBeforePostAssemblyTTL) with invocation-count assertions relaxed from 1 to 2 since this fix does not include per-task SSL/consumer caching. Verified these tests reproduce the exact bug (UndefinedPropertyException: Missing required property 'ssl.keystore.type') when run against the pre-fix code, and pass with the fix applied. Co-authored-by: Copilot <[email protected]>
|
Verified this fix against the real behavioral tests from #2955 to confirm it resolves the same bug. Ported from #2955 into this branch:
To confirm the tests actually catch the bug (not just pass trivially), I reverted the fix locally and reran them. Both TTL-filter and chunk-assembly tests failed with the exact real-world error from #2955: reproduced through Scope difference from #2955: this PR materializes SSL per key-group/task invocation rather than caching a single |
ymuppala
left a comment
There was a problem hiding this comment.
Thanks for splitting this out — the shared VPJSSLUtils.setupSSLForExecutor() extraction is the right shape, and the SparkPubSubPartitionReaderFactory change is a faithful code move (identical logic; only the thrown type changes from RuntimeException to VeniceException, which is fine).
I verified the change is config-safe: getSslProperties() only emits ssl.*, ssl.enabled, pubsub.security.protocol, and ssl.configurator.class. It never writes a broker key. VeniceProperties.toProperties() returns a copy and the backing map is unmodifiable, so the shared Spark Broadcast<Properties> can't be mutated by concurrent task threads. The returned properties stay local to each closure and never reach the VeniceWriter. testCrossFabricRepushBrokerIsolation still passes. I compiled and ran DataWriterSparkJobRepushTest, TestVPJSSLUtils, and SparkPubSubPartitionReaderFactoryTest locally on JDK 17 — all green.
The tests are also good quality: they cover the no-op path and a real failure path, not just the happy path.
That said, I think there is one blocking issue and two things worth addressing before merge. Details are in the inline comments; summarizing here:
- (blocking) SSL materialization and
SparkChunkAssemblerconstruction happen once per key group inapplyChunkAssembly, i.e. once per unique key. See inline comment onAbstractDataWriterSparkJob.java. - SSL setup is unconditional on the TTL/chunk-assembly paths, which converts a currently-working configuration into one that can fail.
- A pre-existing broker-precedence issue in
KafkaInputUtils.getCompressor()becomes reachable for the first time because of this fix (below).
Broker precedence in KafkaInputUtils.getCompressor() (not in this diff, but newly reachable)
if (strategy.equals(CompressionStrategy.ZSTD_WITH_DICT)) {
Properties props = properties.toProperties();
props.setProperty(KAFKA_BOOTSTRAP_SERVERS, kafkaUrl); // only sets kafka.bootstrap.servers
ByteBuffer dict = DictionaryUtils.readDictionaryFromKafka(topic, new VeniceProperties(props));PubSubUtil.getPubSubBrokerAddress() resolves pubsub.broker.address first, falling back to kafka.bootstrap.servers only when the former is absent:
public static String getPubSubBrokerAddress(VeniceProperties properties) {
return properties.getStringWithAlternative(PUBSUB_BROKER_ADDRESS, KAFKA_BOOTSTRAP_SERVERS);
}pubsub. is one of DataWriterComputeJob.PASS_THROUGH_CONFIG_PREFIXES, so any pubsub.broker.address present in the job properties is copied verbatim into the Spark conf, flows into the broadcast filter properties, and would silently take precedence over the source broker that getCompressor() intended to use — meaning the dictionary consumer could read from a different cluster than the repush source.
This is pre-existing, but before this PR the path failed fast on missing SSL properties, so it was never exercised. After this PR it succeeds, and a misrouted read would be silent rather than loud. KafkaInputUtils.getConsumerProperties() already guards against this at line 85 by setting PUBSUB_BROKER_ADDRESS explicitly. Suggest the same one-liner here:
props.setProperty(PUBSUB_BROKER_ADDRESS, kafkaUrl);Even if no current job sets pubsub.broker.address, making the intended broker explicit at the point of use is cheap insurance for a failure mode that is very hard to detect after the fact.
Minor: wasted filter construction when chunking is enabled
In applyTTLFilter, SparkKafkaInputTTLFilter is constructed eagerly at the top of the mapPartitions closure, but a few lines later every row short-circuits when isChunkingEnabled is true (TTL is applied post-assembly instead). So for chunked stores the filter — including its HDFS schema fetch and, for ZSTD_WITH_DICT, a dictionary consumer — is built per partition and never used. Previously this was masked because construction failed anyway; now it will run. Worth skipping the whole mapPartitions (or constructing lazily) when isChunkingEnabled is true.
Addresses the blocking review comment on linkedin#2975: applyChunkAssembly's flatMapGroups closure was rebuilding SparkChunkAssembler (and materializing executor SSL) once per key group, not once per task, which meant a chunked + TTL + ZSTD_WITH_DICT repush could create one PubSub dictionary consumer and a fresh keystore/truststore temp-file pair per unique key. - VPJSSLUtils.setupSSLForExecutor now memoizes the materialized SSL properties per-JVM behind a double-checked lock, since the Hadoop token file and SSL configurator class are fixed for the lifetime of an executor. Failures are never cached so a transient issue can be retried. - applyChunkAssembly's flatMapGroups closure is replaced with a named ChunkAssemblyFunction that lazily builds one SparkChunkAssembler per Spark task and reuses it across all key groups in that task, instead of constructing a new one per key group. The assembler is released via a TaskContext completion listener. - SparkChunkAssembler now implements AutoCloseable, closing its post-assembly TTL filter (and the schema reader it holds). The ported test from linkedin#2955 (testApplyChunkAssemblyReusesExecutorSSLForPostAssemblyTTL) now asserts the SSL configurator/consumer factory are invoked exactly once across both key groups in its single-task test setup, matching PR linkedin#2955's original assertion, since caching now dedups within a task. Co-authored-by: Copilot <[email protected]>
|
Addressed the blocking issue in 0d2ea76.
With this, Ran the full |
Three fixes based on reviewer feedback: 1. Gate SSL executor setup on source compression strategy. SSL setup is only needed to fetch a Zstd dictionary from Kafka (KafkaInputUtils.getCompressor() only creates a dictionary consumer for ZSTD_WITH_DICT). Previously SSL setup ran unconditionally and a failure was silently swallowed, which meant misconfigured SSL would fail open instead of failing closed for the cases where it actually matters. Applied in both the raw TTL filter path (applyTTLFilter) and the post-assembly TTL filter path (ChunkAssemblyFunction.getOrCreateAssembler), since both construct a VeniceRmdTTLFilter that reads the compression strategy. 2. Fix broker precedence in KafkaInputUtils.getCompressor(). The PUBSUB_BROKER_ADDRESS property was not being set from kafkaUrl, so a stale/incorrect value already present in the input properties could take precedence over the intended source broker address. Mirrors the same fix already applied in getConsumerProperties(). 3. Skip constructing SparkKafkaInputTTLFilter when chunking is enabled. applyTTLFilter() now returns early when isChunkingEnabled is true, avoiding wasted filter construction since chunk assembly performs its own TTL filtering after reassembly. Testing Done: - Rewrote testApplyTTLFilterMaterializesExecutorSSL and testApplyChunkAssemblyMaterializesExecutorSSL into testApplyTTLFilterSkipsSSLForNonDictCompressionStrategy and testApplyChunkAssemblySkipsSSLForNonDictCompressionStrategy, which assert the job succeeds with NO_OP compression even when the Hadoop token file used for SSL setup is unresolved, proving SSL setup is now skipped rather than attempted. - Added sourceVersionCompressionStrategy = ZSTD_WITH_DICT to the existing ZSTD regression tests (testApplyTTLFilterMaterializesSSLBeforeReadingZstdDictionary, testApplyChunkAssemblyReusesExecutorSSLForPostAssemblyTTL) so they continue to exercise the SSL-setup path. - Added testGetCompressorOverridesStalePubSubBrokerAddressForZstdWithDict to KafkaInputUtilsTest, which seeds a stale PUBSUB_BROKER_ADDRESS and verifies getCompressor() overrides it with the source kafkaUrl. - Ran the full venice-push-job test suite: BUILD SUCCESSFUL, 829 tests, 0 failures, 0 errors. Co-authored-by: Copilot <[email protected]>
|
Also addressed the two items from the review summary that did not have a diff to comment on directly (fixed in c59bf97): Broker precedence in Wasted filter construction when chunking is enabled: Ran the full |
Problem Statement
SparkPubSubPartitionReaderFactory.createReader()materializes SSL properties from the Hadoop token file before creating its PubSub consumer, but two other executor-side code paths that can also create a PubSub consumer do not:mapPartitionsclosure inAbstractDataWriterSparkJob.applyTTLFilter()constructsSparkKafkaInputTTLFilterdirectly from broadcast job properties.flatMapGroupsclosure inAbstractDataWriterSparkJob.applyChunkAssembly()constructsSparkChunkAssemblerdirectly from broadcast job properties.When TTL filtering is enabled together with dictionary compression (
ZSTD_WITH_DICT), both of these components create a second, internal PubSub (dictionary) consumer on the executor. Since SSL was never materialized for these paths, consumer creation fails with errors like "Missing required property ssl.keystore.type".This is a simplified, minimal fix for the same gap addressed more broadly in #2955. It intentionally does not include that PR's per-task caching refactor for
SparkChunkAssembler(avoiding repeated SSL materialization / dictionary-consumer creation per key group) or theUserCredentialsFactoryfail-fast hardening. Those are separate, deferrable follow-ups tracked independently.Solution
SparkPubSubPartitionReaderFactoryinto a shared, publicVPJSSLUtils.setupSSLForExecutor(VeniceProperties)utility (no-op when no SSL configurator is configured; wraps failures inVeniceException).SparkPubSubPartitionReaderFactory.createReader()now calls the shared utility instead of its own private copy.AbstractDataWriterSparkJob.applyTTLFilter()'smapPartitionsclosure now callsVPJSSLUtils.setupSSLForExecutor(...)before constructingSparkKafkaInputTTLFilter.AbstractDataWriterSparkJob.applyChunkAssembly()'sflatMapGroupsclosure now callsVPJSSLUtils.setupSSLForExecutor(...)(when TTL is enabled) before constructingSparkChunkAssembler, keeping the existing per-key-group instantiation unchanged.Code changes
Concurrency-Specific Checks
Both reviewer and PR author to verify
synchronized,RWLock) are used where needed.ConcurrentHashMap,CopyOnWriteArrayList).How was this PR tested?
Added
TestVPJSSLUtilscoverage for the new shared utility (no-op without an SSL configurator, and failure when the Hadoop token file can't be resolved), plus two new regression tests inDataWriterSparkJobRepushTestthat force actual Spark execution (collectAsList()) of the TTL filter and chunk assembly closures with an SSL configurator configured but no resolvable token file, asserting the SSL setup failure now surfaces from both paths (proving the pre-fix code would not have attempted SSL setup there at all).Ran the affected test classes locally:
TestVPJSSLUtils,DataWriterSparkJobRepushTest,SparkPubSubPartitionReaderFactoryTest, all passing. Also verifiedspotlessCheckpasses.Does this PR introduce any user-facing or breaking changes?