[controller] Remove topic-based concurrent push detection and clarify push-blocking messaging - #2896
Conversation
(ConcurrentPushDetectionStrategy) Removes the ConcurrentPushDetectionStrategy flag and makes parent-version-status tracking the only path for detecting an in-flight push. Production parent controllers were already running PARENT_VERSION_STATUS_ONLY, so this is behavior-preserving for LinkedIn; it changes the OSS default (was DUAL). - VeniceParentHelixAdmin.getTopicForCurrentPushJob now always delegates to the parent-version-status path. Deletes getTopicForCurrentPushJobTopicBasedTracking and the orphaned helpers getKafkaTopicsByAge and truncateTopicsBasedOnMaxErroredTopicNumToKeep. - Removes the now-dead isTopicWriteNeeded() gates: the parent no longer creates or truncates the parent version topic (addVersion, shouldSkipTruncatingTopic, rollForwardToFutureVersion, the push-completion path, killOfflinePush, and DeferredVersionSwapService). - Deletes the ConcurrentPushDetectionStrategy enum, the concurrent.push.detection.strategy config key, and the cluster-config field/parse/getter. - Updates controller tests accordingly. DeferredVersionSwapService (controller.deferred.version.swap.service.enabled) is a separate, prod-enabled feature and is intentionally kept. Testing Done: - :services:venice-controller:test for TestVeniceParentHelixAdmin (88), TestVeniceHelixAdmin (43), TestDeferredVersionSwapService (19), and TestDeferredVersionSwapServiceWithSequentialRollout (8) -- all pass. - integrationTest TestDeferredVersionSwap and TestTargetedRegionPushWithNativeReplication -- all pass (incl. target-region deferred swap). - spotbugsMain (venice-controller + venice-common) and spotlessApply clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rent push When a new push is blocked, the rejection message branched only on version.isVersionSwapDeferred(), so a deferred-swap-enabled version whose push was still in progress was wrongly reported as "make that version current" (implying it only needed a roll-forward). That ambiguity misled a prior investigation that was actually a concurrent push, not a deferred-swap wait. - ConcurrentBatchPushException now distinguishes the two cases by the version's actual status: a deferred version that is ONLINE but not yet current in all regions is reported as waiting on deferred version swap (with per-region current versions and the target swap region); everything else is reported as an in-progress concurrent push (including the version status). - validateChildCurrentVersions now logs the full per-region picture and names the deferred (colo-by-colo) version swap, instead of logging only the first mismatching region. Testing Done: - :services:venice-controller:test TestVeniceParentHelixAdmin (89, incl. the new testDeferredVersionSwapWaitMessageIsDistinctFromConcurrentPush) -- all pass. - Updated the concurrent-push assertions in TestParentControllerWithMultiDataCenter to the new wording (this integration test was not re-run locally). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR removes the legacy/topic-based concurrent push detection path from the parent controller and standardizes on parent-version-status-based tracking, while also improving the push-blocking messaging to clearly distinguish “in-progress concurrent push” vs “waiting on deferred version swap”.
Changes:
- Remove
ConcurrentPushDetectionStrategyand theconcurrent.push.detection.strategyconfig, along with topic-based current-push detection and related topic truncation gates on the parent controller. - Update parent/controller logic and tests to reflect that parent controllers no longer create/truncate version topics (and simplify
shouldSkipTruncatingTopicaccordingly). - Clarify
ConcurrentBatchPushExceptionmessaging and logging to explicitly call out deferred version swap waits (with per-region current versions and target swap region).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java | Removes topic-based detection path; updates push-blocking message; removes parent VT truncation gates. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java | Stops parent controllers from creating version topics; simplifies truncation-skip logic for parents. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceControllerClusterConfig.java | Removes parsing/storage of the concurrent push detection strategy config. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/DeferredVersionSwapService.java | Removes parent VT truncation behavior from deferred swap promotion. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceController.java | Updates TODO/commentary now that parent no longer writes version topics. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/ConcurrentPushDetectionStrategy.java | Deletes the enum (strategy fully removed). |
| internal/venice-common/src/main/java/com/linkedin/venice/ConfigKeys.java | Removes the concurrent.push.detection.strategy key. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/TestVeniceParentHelixAdmin.java | Updates tests for new behavior and adds coverage for distinct deferred-swap vs concurrent-push messaging. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/TestVeniceHelixAdmin.java | Simplifies truncation-skip tests now that parent always skips. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/AbstractTestVeniceParentHelixAdmin.java | Removes now-dead strategy stubbing. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/TestDeferredVersionSwapService.java | Removes now-dead strategy stubbing. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/TestDeferredVersionSwapServiceWithSequentialRollout.java | Removes now-dead strategy stubbing and truncation assertions. |
| internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/controller/TestParentControllerWithMultiDataCenter.java | Updates assertions for the clarified push-blocking message text. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…b topic lookup CI fix: getTopicForCurrentPushJob blocked a new push whenever the latest parent version was STARTED, without polling child job status. The parent Version status only advances out of STARTED when a job-status poll observes a terminal child status (handleTerminalJobStatus); there is no synchronous push-completion callback. So a completed empty/batch push left the parent version at STARTED and blocked every subsequent push until the ~10-minute background checker ran. The removed topic-based path used to poll and resolve this; with version-status now the only path, the regression surfaced in integration tests (empty_push / request_topic / update_store rejected with "ongoing push ... status STARTED"). Fix: let STARTED fall through to the existing child-status polling branch (which also drives the STARTED -> ONLINE transition). CREATED and PUSHED still block outright -- PUSHED is a target-region deferred-swap push awaiting roll-forward, where the children already report terminal status and polling would wrongly unblock the next push. Cleanups (no behavior change): - Inline getTopicForCurrentPushJobParentVersionStatusBasedTracking into getTopicForCurrentPushJob; the separate name implied a sibling strategy that no longer exists. - Remove dead existingVersionTopicsForStore() and the unused TOPIC_DELETION_DELAY_MS constant (last callers were the removed topic-based tracking). - Correct the stale maxErroredTopicNumToKeep Javadoc that described parent version-topic truncation the parent no longer performs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s review testDeferredVersionSwap regression: the prior STARTED-polling change let a second push through while the first push was deferring its version swap. Polling a deferred STARTED version advances it to a terminal status via getOffLineJobStatus and returns empty, unblocking the next push. Decide deferred swaps up front instead: a version that defers its swap blocks the next push until it is rolled forward and current in every region, regardless of push status. testGetClusterStaleStores regression: getTopicForCurrentPushJob re-polled a non-deferred ONLINE version. When a region has that version deleted/rolled back (a stale-store condition), the aggregate child status is non-terminal, so the version was misreported as an in-flight push and the store was dropped from the stale audit. A non-deferred ONLINE version has no ongoing push, so return empty without polling. Only STARTED now polls to disambiguate. Address Copilot review comment: the post-terminal block no longer truncates the parent version topic, so only append a status detail when the stream-reprocessing topic is actually truncated, and name it accordingly. Remove testDeferredVersionSwapWaitMessageIsDistinctFromConcurrentPush. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| return latestTopic; | ||
| } | ||
|
|
||
| if (lastVersion.getStatus() == CREATED || lastVersion.getStatus() == PUSHED) { |
There was a problem hiding this comment.
Why did we remove the STARTED status check?
There was a problem hiding this comment.
🤖 Good catch — STARTED was removed from this block intentionally, but the reason wasn't spelled out clearly. A STARTED version may already be terminal in child regions but not yet reflected on the parent (the parent only transitions out of STARTED once a job-status poll observes a terminal child status). Polling PUSHED children, on the other hand, would always return terminal and incorrectly unblock the next push. So STARTED is handled by the polling branch further below, while CREATED and PUSHED block unconditionally here. Added a comment to the code to make this explicit.
There was a problem hiding this comment.
I think polling job status based on STARTED version status can allow concurrent push to happen because polling on job status will only verify that ingestion is completed and doesn't check that version swap is complete too. There will be a small race window where concurrent push can happen between started -> pushed version transition if we want to poll job status for started version statuses. Can you check if that is true?
There was a problem hiding this comment.
🤖 You're right, and good catch. Verified: the STARTED-polling branch only observed ingestion completion via getOffLinePushStatus, which for a deferred-swap version does not imply the version swap is done — so a terminal poll could unblock the next push during the STARTED → PUSHED transition, exactly the race you described.
Fixed in ff18c59b7: a STARTED latest version now blocks the next push unconditionally, the same as CREATED and PUSHED, and I removed the polling loop entirely. This matches the getTopicForCurrentPushJobParentVersionStatusBasedTracking behavior on main (STARTED/PUSHED/CREATED all block; the poll below it there is effectively unreachable). Updated testGetTopicForCurrentPushJobBlocksInProgressVersion to assert all three non-terminal statuses block immediately without polling.
| : ". An ongoing push with pushJobId " + existingPushJobId + " and topic " + currentPushTopic.get() | ||
| + " is found and it must be terminated before another push can be started."; | ||
| String msg; | ||
| if (version.isVersionSwapDeferred() && version.getStatus() == ONLINE) { |
There was a problem hiding this comment.
Should we check for PUSHED instead? ONLINE means ingestion and version swap completed so the log below will never log
There was a problem hiding this comment.
🤖 Yes, good catch. Changed to PUSHED. PUSHED is the parent-side status when a deferred-swap version has completed its push but its version swap is still pending — i.e., when a new push should be blocked. ONLINE on the parent for a deferred-swap version means the swap has already completed (and validateChildCurrentVersions would have unblocked the push upstream), so the ONLINE branch in incrementVersionIdempotent would never actually be reached. Fixed and re-added the corresponding test (testDeferredVersionSwapWaitMessageIsDistinctFromConcurrentPush) with VersionStatus.PUSHED.
- Document that isIncrementalPush/isRepush params in getTopicForCurrentPushJob are unused by the parent implementation (kept for call-site compatibility) - Add pushJobId to CREATED/PUSHED log in getTopicForCurrentPushJob - Fix deferred-swap push-blocking check: PUSHED (not ONLINE) is the correct status when a push is done but its deferred version swap is still pending - Update shouldSkipTruncatingTopic @PARAM javadoc: clusterName is unused, kept for override compatibility - Re-add testDeferredVersionSwapWaitMessageIsDistinctFromConcurrentPush with VersionStatus.PUSHED (was removed in prior commit when ONLINE was used) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…test setup Parent controllers no longer write version topics, so the parent-fabric topic cleanup subclass is dead code. Remove TopicCleanupServiceForParentController and always use the base TopicCleanupService; drop its dedicated tests along with TestTopicCleanupServiceForMultiKafkaClusters, whose entire premise (iterating multiple parent Kafka fabrics) no longer exists. Also: - Simplify shouldSkipTruncatingTopic(clusterName) call site to isParent() directly and remove the now-redundant wrapper method and its tests. - Simplify testDeferredVersionSwapWaitMessageIsDistinctFromConcurrentPush's incrementVersionIdempotent stubbing/invocation to use the 5-arg overload. - Ignore the impeccable hook cache file (.impeccable/) which is regenerated on every run. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Was accidentally committed in 46953d9; it is a regenerated pre-commit hook cache file and should not be version controlled. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Correct maxErroredTopicNumToKeep Javadoc: it also gates truncation/ status-detail behavior in truncateTopicsOptionally, not just cleanup in killOfflinePush and a diagnostic detail in getOffLineJobStatus. - getTopicForCurrentPushJob: separate the NON_EXISTING_VERSION check from the lastVersion == null check and log an accurate message for the latter (largestUsedVersionNumber can point at a version that doesn't exist, e.g. after deleteVersion or during data recovery); both still return empty. - validateChildCurrentVersions: take the Version instead of just its number so the log line can include the version status, since the method can be invoked for a deferred-swap version in any status, not only once it has entered the swap phase. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Hi there. This pull request has been inactive for 30 days. To keep our review queue healthy, we plan to close it in 7 days unless there is new activity. If you are still working on this, please push a commit, leave a comment, or convert it to draft to signal intent. Thank you for your time and contributions. |
|
Closing this pull request due to 37 days of inactivity. This is not a judgment on the value of the work. If you would like to continue, please reopen or open a new PR and we will be happy to take another look. Thank you again for contributing. |
kailim/remove-concurrent-push-detection-strategy # Conflicts: # internal/venice-common/src/main/java/com/linkedin/venice/ConfigKeys.java # services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java # services/venice-controller/src/test/java/com/linkedin/venice/controller/TestVeniceParentHelixAdmin.java
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java:3694
- In
truncateTopicsOptionally, the maxErroredTopicNumToKeep>0/error branch still appends/logs "Parent Kafka topic won't be truncated". After removing parent version-topic truncation, this status detail is misleading (the only truncation that can happen here now is for the stream-reprocessing topic). Consider rewording the status detail/log to avoid implying the parent version topic would otherwise be truncated.
boolean isTargetRegionPushWithDeferredSwap =
isDeferredVersionSwap && !StringUtils.isEmpty(version.getTargetSwapRegion());
if ((failedBatchPush || nonIncPushBatchSuccess && !isDeferredVersionSwap || incPushEnabledBatchPushSuccess
…ling job status A STARTED latest version now blocks the next push unconditionally, the same as CREATED and PUSHED, rather than polling child job status to decide. A job-status poll only observes ingestion completion, which for a deferred-swap version does not imply the version swap has finished, so unblocking on a terminal poll left a race window where a concurrent push could slip in during the STARTED -> PUSHED transition. This matches the parent-version-status-based tracking behavior on main. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java:1420
getTopicForCurrentPushJobtreats any non-terminal status as blocking, butVersionStatus.NOT_CREATEDis explicitly documented as an inert fallback for unknown status ids during rolling deploys (it “won't block new pushes”). With the current switch, aNOT_CREATEDlatest version will block indefinitely and return a version topic.
Consider explicitly treating NOT_CREATED as a non-blocking status here (similar to other terminal statuses) to avoid stuck pushes when the parent reads an unrecognized status value.
switch (lastVersion.getStatus()) {
case KILLED:
case ERROR:
case ROLLED_BACK:
case PARTIALLY_ONLINE:
Treat a STARTED latest version as blocking only while a non-deferred push has not yet become current in all fabrics. Keep deferred-swap STARTED versions blocking to preserve concurrent-push protection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java:1423
getTopicForCurrentPushJobcurrently treatsVersionStatus.NOT_CREATEDas a non-terminal status and will block the next push. However,VersionStatus.getVersionStatusFromIntintentionally maps unknown/new status IDs toNOT_CREATEDas the safest inert fallback specifically so older controllers do not block new pushes during rolling deployments (seeinternal/venice-common/.../VersionStatus.java). TreatingNOT_CREATEDas blocking here could cause unnecessary push rejections in mixed-version windows.
// CREATED/PUSHED block the next push outright. STARTED can still represent a normal push whose
// current-version promotion has already completed but whose parent version status has not caught up.
switch (lastVersion.getStatus()) {
case KILLED:
case ERROR:
case ROLLED_BACK:
case PARTIALLY_ONLINE:
case ONLINE:
LOGGER.info(
getTopicForCurrentPushJob VersionStatus#getVersionStatusFromInt documents NOT_CREATED as the safe, inert fallback for an unrecognized status id during rolling deployments and that it "won't block new pushes". getTopicForCurrentPushJob previously let it fall through the switch's default case and block the next push, contradicting that contract. Handle NOT_CREATED alongside the other non-blocking statuses and add regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java:1444
- In
getTopicForCurrentPushJob, the STARTED/non-deferred branch always callsgetCurrentVersionsForMultiColoseven whenstore.getCurrentVersion() == lastVersionNumwould already allow the push to proceed.getCurrentVersionsForMultiColoscan be relatively expensive (fan-out to child controllers), so this adds unnecessary remote work for the common “already current but status still STARTED” case. Consider short-circuiting onstore.getCurrentVersion()before fetching per-region current versions.
if (lastVersion.getStatus() == STARTED && !lastVersion.isVersionSwapDeferred()) {
Map<String, Integer> currentVersions = getCurrentVersionsForMultiColos(clusterName, storeName);
boolean allFabricsServingLastVersion = currentVersions != null && !currentVersions.isEmpty()
&& currentVersions.values()
.stream()
.allMatch(currentVersion -> Objects.equals(currentVersion, lastVersionNum));
if (store.getCurrentVersion() == lastVersionNum || allFabricsServingLastVersion) {
LOGGER.info(
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java:1465
- PR description states that STARTED versions are polled via child job status, but the final
getTopicForCurrentPushJobimplementation explicitly avoids polling (nogetOffLinePushStatuspath) and always blocks in non-terminal states unless the version is already current. Please align the PR description/commit message with the implemented behavior to avoid confusion for reviewers/operators.
// push completed in its target region but whose swap is still pending). In all of these the version
// is not yet done from the user's perspective, so the next push must wait. STARTED is treated the
// same as CREATED/PUSHED for deferred-swap versions, or when the version is not current yet, rather
// than polling the child job status: a job-status poll only observes ingestion completion, which for
// a deferred-swap version does not imply the version swap has finished, so unblocking on a terminal
// poll would let a concurrent push slip in during the STARTED -> PUSHED transition.
LOGGER.info(
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceController.java:301
createTopicCleanupServicenow always wires the baseTopicCleanupService, butTopicCleanupServiceonly enumerates topics viaadmin.getTopicManager()(local cluster) and does not iteratemultiClusterConfigs.getParentFabrics(). Previously, the parent-specific service cleaned deprecated topics across all parent-fabric Kafka clusters. IfparentFabricsis non-empty in a deployment, this change can leave deprecated topics in non-local parent fabrics uncleaned, which may cause operational topic leaks.
private TopicCleanupService createTopicCleanupService() {
Admin admin = controllerService.getVeniceHelixAdmin();
return new TopicCleanupService(
admin,
multiClusterConfigs,
pubSubTopicRepository,
new TopicCleanupServiceStats(metricsRepository),
pubSubClientsFactory);
…leanupService The deleted TopicCleanupServiceForParentController iterated all parent fabrics' Kafka clusters when cleaning up deprecated topics. The base TopicCleanupService only cleaned the default cluster, so after the subclass removal, topics truncated across non-default parent fabrics (via VeniceHelixAdmin#truncateKafkaTopicInParentFabrics) would leak. Parameterize the cleanup loop by TopicManager and run it against every parent fabric's Kafka cluster when the controller is a parent; all other deployments keep cleaning up their single default cluster. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java:1464
- The PR description says the parent will "Poll child status for STARTED versions", but the final
getTopicForCurrentPushJobimplementation does not poll child offline-push status at all (it explicitly blocks without polling). Please either update the PR description to match the current behavior, or reintroduce the STARTED-only polling behavior if that was a requirement.
// The only statuses left after the terminal cases above are non-terminal: CREATED (version exists
// but its push has not begun), STARTED (push in flight), and PUSHED (a deferred-swap version whose
// push completed in its target region but whose swap is still pending). In all of these the version
// is not yet done from the user's perspective, so the next push must wait. STARTED is treated the
// same as CREATED/PUSHED for deferred-swap versions, or when the version is not current yet, rather
// than polling the child job status: a job-status poll only observes ingestion completion, which for
// a deferred-swap version does not imply the version swap has finished, so unblocking on a terminal
// poll would let a concurrent push slip in during the STARTED -> PUSHED transition.
Two fixes for the multi-parent-fabric cleanup loop: - The VT-deletion delay countdown was keyed only by topic name, so with the same topic present in every parent fabric's Kafka cluster the countdown was decremented once per fabric per cycle, deleting topics earlier than the configured delay intends. The countdown key now includes the Kafka cluster address, keeping a separate countdown per fabric (matching the removed TopicCleanupServiceForParentController behavior). - getTopicManagersForCleanup() assumed every parent fabric has an entry in getChildDataCenterKafkaUrlMap(). A missing entry produced a null bootstrap server and would throw in getTopicManager(null), crashing the whole cleanup loop. Misconfigured fabrics are now skipped with a warning, falling back to the default topic manager if none are configured. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java:1464
- PR description says STARTED versions “poll the child job status”, but the current implementation explicitly does not poll (it blocks based on parent version status and current-version checks only). Please update the PR description (and/or any commit message) to match the final behavior so reviewers/operators aren’t misled about how concurrent-push detection works now.
// is not yet done from the user's perspective, so the next push must wait. STARTED is treated the
// same as CREATED/PUSHED for deferred-swap versions, or when the version is not current yet, rather
// than polling the child job status: a job-status poll only observes ingestion completion, which for
// a deferred-swap version does not imply the version swap has finished, so unblocking on a terminal
// poll would let a concurrent push slip in during the STARTED -> PUSHED transition.
…opicsToCleanup The version-topic deletion delay countdown key unconditionally appended "_" + pubSubClusterAddress, so the legacy overload (which passes an empty address) produced "<topic>_" instead of the original "<topic>", diverging from single-cluster behavior. Only append the cluster address when it is non-empty, keeping the parent multi-fabric disambiguation while leaving the single-cluster key unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Problem Statement
Two related issues in the parent controller's concurrent-push detection:
Dead flag.
ConcurrentPushDetectionStrategy(concurrent.push.detection.strategy) gated two ways of detecting an in-flight push: the legacy "topic-based" path (derives the current push from parent version topics) and the newer "parent-version-status" path. Production parent controllers have fully migrated toPARENT_VERSION_STATUS_ONLY, so the topic-based path and the flag are dead in production, yet they still carried significant code (the parent creating/truncating version topics, orphan-topic cleanup, etc.). ATODOinVeniceControllercalled for removing it once the new mode was fully rolled out.Ambiguous push-blocking message. When a new push is rejected because a prior version exists, the rejection message branched only on
version.isVersionSwapDeferred(). A deferred-swap-enabled version whose push was still in progress was therefore reported as "make that version current" — implying it only needed a roll-forward. This ambiguity sent an investigation down the wrong path on an issue that was actually a concurrent push, not a deferred (colo-by-colo) version-swap wait.Solution
Remove topic-based tracking; make version-status tracking the only path:
getTopicForCurrentPushJobalways delegates to the parent-version-status path. DeletesgetTopicForCurrentPushJobTopicBasedTrackingand the now-orphaned helpersgetKafkaTopicsByAge/truncateTopicsBasedOnMaxErroredTopicNumToKeep.isTopicWriteNeeded()gates: the parent no longer creates or truncates the parent version topic (addVersion,shouldSkipTruncatingTopic,rollForwardToFutureVersion, the push-completion path,killOfflinePush,DeferredVersionSwapService).ConcurrentPushDetectionStrategyenum, theconcurrent.push.detection.strategyconfig key, and the cluster-config field/parse/getter.PARENT_VERSION_STATUS_ONLY); it changes the OSS default, which wasDUAL.DeferredVersionSwapService(controller.deferred.version.swap.service.enabled) is a separate, prod-enabled feature and is intentionally kept.Clarify the push-blocking message:
ConcurrentBatchPushExceptionnow distinguishes the cases by the version's actual status: a deferred version that isPUSHED(push complete, swap not yet done) but not yet current in all regions is reported as waiting on deferred version swap (with per-region current versions and the target swap region); everything else is reported as an in-progress concurrent push (including the version status).validateChildCurrentVersionslogs the full per-region picture and names the deferred (colo-by-colo) version swap, and takes theVersionso its status is included in the log line.Version-status-based push-job topic lookup.
getTopicForCurrentPushJobbranches on the latest version's status:ONLINE,ERROR,KILLED,ROLLED_BACK,PARTIALLY_ONLINE) andNOT_CREATED— no ongoing push, never block.NOT_CREATEDis included becauseVersionStatusdocuments it as the safe rolling-deployment fallback for an unrecognized status id that must not block new pushes. (ONLINEalso fixes a prior stale-store misdetection.)STARTEDversion that is already current in the store / serving in all regions has effectively completed and is allowed to proceed.CREATED, in-flightSTARTED,PUSHED, and any deferred-swap version not yet current in every region — blocks, because a future version still occupies the store. In particular aSTARTEDversion blocks unconditionally rather than polling child job status: job-status polling only confirms ingestion completion, which for a deferred-swap version does not imply the version swap is done, so polling would open a race where a concurrent push slips in during theSTARTED → PUSHEDtransition.isIncrementalPush/isRepushare accepted for call-site/interface compatibility but are unused by this parent-only implementation (documented in the Javadoc).Parent-fabric topic cleanup moved into the base
TopicCleanupService. With the parent no longer relying on topic-based tracking,TopicCleanupServiceForParentControllerwas removed andVeniceControlleralways constructs the baseTopicCleanupService. Because the parent still truncates topics across every parent fabric (VeniceHelixAdmin#truncateKafkaTopicInParentFabrics), the base service now runs cleanup against each parent fabric's Kafka cluster whenadmin.isParent()(and the single default cluster otherwise), preserving the old subclass's multi-fabric behavior:getTopicManagersForCleanup()returns oneTopicManagerper configured parent fabric, skipping any fabric missing agetChildDataCenterKafkaUrlMap()entry (with a warning) instead of crashing ongetTopicManager(null), and falling back to the default manager if none are configured.shouldSkipTruncatingTopic(clusterName)(return isParent()) at its one call site.Code changes
concurrent.push.detection.strategy); no new config added.validateChildCurrentVersions,getTopicForCurrentPushJob); volume is unchanged (at most one line per blocked push), so no new rate-limiting is needed.Concurrency-Specific Checks
STARTEDversion now blocks the next push unconditionally (closing theSTARTED → PUSHEDswap race for deferred-swap versions) rather than being unblocked by ingestion-only job-status polling.How was this PR tested?
testDeferredVersionSwapWaitMessageIsDistinctFromConcurrentPush,testGetTopicManagersForCleanup,NOT_CREATEDcoverage intestGetTopicForCurrentPushJob).testGetTopicForCurrentPushJobBlocksInProgressVersion).TestVeniceParentHelixAdmin,TestVeniceHelixAdmin,TestDeferredVersionSwapService,TestDeferredVersionSwapServiceWithSequentialRollout,TestTopicCleanupService— all pass.TestDeferredVersionSwapandTestTargetedRegionPushWithNativeReplication— all pass (incl. target-region deferred swap).TestParentControllerWithMultiDataCenterassertions were updated to the new wording.spotbugsMain(venice-controller + venice-common) andspotlessApplyare clean.Does this PR introduce any user-facing or breaking changes?
DUAL(ran both paths, returned the topic-based result) to parent-version-status-only. Theconcurrent.push.detection.strategyconfig is removed and ignored if still set. Behavior is equivalent for the supported deferred-swap and concurrent-push scenarios (validated by the deferred-swap / target-region integration tests); LinkedIn production already ran the version-status path.ConcurrentBatchPushExceptiontext shown to push jobs now distinguishes "waiting on deferred version swap" (with per-region status) from "ongoing concurrent push" (with the version status).TopicCleanupServiceForParentController: it now uses the same baseTopicCleanupServiceas child controllers, which iterates all parent fabrics' Kafka clusters when running on a parent. Behavior for operators is unchanged.🤖 Generated with Claude Code