Add an opt-in controller-copy METADATA push for same-name segment refreshes - #19488
Open
KKcorps wants to merge 1 commit into
Open
Add an opt-in controller-copy METADATA push for same-name segment refreshes#19488KKcorps wants to merge 1 commit into
KKcorps wants to merge 1 commit into
Conversation
KKcorps
force-pushed
the
kk/ssce-metadata-push-same-name-refresh
branch
from
September 7, 2026 09:06
937bf6e to
27b5ecc
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19488 +/- ##
=============================================
+ Coverage 39.44% 67.73% +28.28%
- Complexity 1429 1450 +21
=============================================
Files 3488 3488
Lines 224424 224483 +59
Branches 35429 35437 +8
=============================================
+ Hits 88528 152051 +63523
+ Misses 127912 60421 -67491
- Partials 7984 12011 +4027
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…reshes The METADATA mode of BaseSingleSegmentConversionExecutor registers the staged tar's URI as the segment's download URL. For a task that refreshes a segment under its own name that path collides with a live segment stored as <segment>.tar.gz, drops the TAR path's If-Match and REFRESH_ONLY guards, and moves the download URL away from the deep store location so the second refresh of the same segment fails with "already exists". Add a protected hook, isCopyToDeepStoreForMetadataPush(), default false, so the existing behavior is unchanged for every current task. A task that returns true gets a refresh-safe path: the tar is staged under <segment>.<taskId>.tar.gz, the push carries the same If-Match, REFRESH_ONLY and custom-map headers as TAR plus COPY_SEGMENT_TO_DEEP_STORE=true, the controller copies the bytes into the segment's existing deep store location, and the staged tar is deleted in a finally. The metadata tar is built from the local converted segment instead of downloading the staged tar back, and an unchanged segment (same CRC) only re-registers its metadata against the current download URL. SegmentConversionUtils gains uploadSegmentMetadata sharing the retry loop with uploadSegment. BaseTaskExecutor gains a moveSegmentToOutputPinotFS overload with an explicit target name and overwrite flag, deleteFromOutputPinotFS and createSegmentMetadataTarFile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y375AgHYsh1YqNSsfvF1a8
KKcorps
force-pushed
the
kk/ssce-metadata-push-same-name-refresh
branch
from
September 7, 2026 10:23
27b5ecc to
c0a33a7
Compare
KKcorps
marked this pull request as ready for review
September 8, 2026 09:13
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
TL;DR
BaseSingleSegmentConversionExecutorgets an opt-in hook,isCopyToDeepStoreForMetadataPush(),default
false. A task that returnstruegets a METADATA push that is safe for refreshing asegment under its own name: the tar is staged under a name that can never be a live segment path,
the push keeps the TAR path's
If-MatchandREFRESH_ONLYguards, the controller copies the bytesinto the segment's existing deep store location, and the staged tar is deleted afterwards. Nothing
changes for PurgeTask, RefreshSegmentTask or UpsertCompactionTask unless they opt in.
The problem
#17632 added a METADATA mode to this executor. It stages the converted tar at
<outputDir>/<segment>.tar.gzand registers that URI as the segment's download URL, withCOPY_SEGMENT_TO_DEEP_STORE=false. That is the right shape for a new segment. For a task thatrefreshes an existing segment under the same name it has three gaps:
If-Match(CRC) andREFRESH_ONLYheaders that the TAR path sends are dropped. A pushfrom a stale task can overwrite a segment refreshed after the task was planned, and a push for a
segment deleted in the meantime recreates it.
<table>/<segment>.tar.gzis the live file for a segment that was ingested by a metadata-pushingestion task. With
overwriteOutput=falsethe refresh fails. Withtrueit overwrites livebytes before ZooKeeper is updated, and the failure cleanup then deletes the live file.
never be deleted, the previous deep store object is orphaned, and the second refresh of the same
segment fails with "already exists".
flowchart LR subgraph Before["Existing METADATA push (unchanged default)"] A[stage at table/segment.tar.gz] --> B[register staged URI as download URL] B --> C[old object orphaned, next refresh collides] end subgraph After["Opt-in controller copy"] D[stage at table/segment.taskId.tar.gz] --> E[push metadata + If-Match + REFRESH_ONLY + copy flag] E --> F[controller copies into table/segment] F --> G[download URL unchanged, staged tar deleted] endThe approach
isCopyToDeepStoreForMetadataPush()returnsfalseby default. The existing METADATA method iskept byte for byte and stays the default branch. The TAR path is untouched.
true, the executor builds the metadata tar (metadata.propertiesandcreation.meta) from the local converted segment before the segment directories are deleted.The old path downloaded the staged tar back from the output filesystem to extract those two files.
executor skips tarring and staging and only re-registers the metadata against that URL.
<outputDir>/<segment>.<taskId>.tar.gz. Live segments arestored as
<segment>or<segment>.tar.gz, so the name can never collide. It is stable acrossretries of the same task, so a retry overwrites what an interrupted attempt left, and unique
across tasks. A plain-path output dir (local deep store) is sent as a
fileURI, because thecontroller picks the filesystem by scheme.
DOWNLOAD_URI,UPLOAD_TYPE=METADATAandCOPY_SEGMENT_TO_DEEP_STORE=true. The controller runs its existing CRC check and parallel-pushlock, copies the staged tar server-side into
<dataDir>/<table>/<segment>, updates ZooKeeperand sends the refresh message. That is the same order the TAR path already uses.
finally. Nothing references it after the controller copied it,so this is safe on success, failure and timeout.
Key components
BaseSingleSegmentConversionExecutoruploadSegmentMetadataWithControllerCopy, staging name, header assembly. ExistinguploadSegmentWithMetadatarestored as the default branch.BaseTaskExecutormoveSegmentToOutputPinotFS(configs, file, outputFileName, overwrite)overload (the 2-arg version delegates to it),deleteFromOutputPinotFS,createSegmentMetadataTarFile.SegmentConversionUtilsuploadSegmentMetadata, sharing the retry, round-robin andHOSTheader logic withuploadSegmentthrough one privateuploadWithRetry.Flow (opt-in path)
sequenceDiagram participant M as Minion participant O as Output PinotFS participant C as Controller participant D as Deep store participant Z as ZooKeeper M->>M: convert, build metadata tar from local segment alt segment unchanged (same CRC) M->>C: POST metadata, DOWNLOAD_URI = current download URL, copy=false C->>Z: update custom map / refresh time else segment changed M->>O: put table/segment.taskId.tar.gz (overwrite ok) M->>C: POST metadata, If-Match, REFRESH_ONLY, DOWNLOAD_URI = staged, copy=true C->>C: CRC check, parallel-push lock C->>D: server-side copy staged -> table/segment C->>Z: update segment metadata, send refresh message C-->>M: 200 M->>O: delete staged tar (always, in finally) endCompatibility
test that pinned its failure cleanup is kept.
on by accident from a table config.
If-Match,REFRESH_ONLY, the custom-map header,UPLOAD_TYPE=METADATA,DOWNLOAD_URIandCOPY_SEGMENT_TO_DEEP_STORE, including the deep store copy on an existing-segment refresh. A newminion works against an old controller.
uploadSegmentkeeps its signature. Only additions toBaseTaskExecutor.Failure handling
Controller dies after the deep store copy, before the ZooKeeper update. The deep store then holds the new
bytes while ZooKeeper still has the old CRC and the segment's upload lock. This window is not new: the TAR path
copies into the deep store before the ZooKeeper write in the same method (
copyFromSegmentFileToDeepStore). Itis closed by rerunning the task, which is what Helix does on failure:
controller.segment.upload.timeoutInMillis, default10 minutes), then the task attempt fails and Helix reruns it. The rerun passes the CRC pre-check (ZooKeeper still
has the original CRC), downloads the already-converted bytes from the canonical location, converts them again
(idempotent), stages, and pushes with the same
If-Match. Once the lock has expired the controller copies againand commits. ZooKeeper and the deep store converge on the new CRC.
If-Match, the attempt fails, and the rerun stops at the CRC pre-check: the segment is already refreshed.matches, so the refresh message costs it nothing.
Minion dies after the upload, before deleting the staged tar. One
<segment>.<taskId>.tar.gzis left in thetable's deep store directory. A retry of the same task reuses the name and overwrites it, then deletes it. If the
task never runs again, the file is an untracked segment for
RetentionManager's deep store sweep(
controller.retentionManager.untrackedSegmentDeletionEnabled, default off in OSS, retentioncontroller.retentionManager.untrackedSegmentsRetentionTimeInDays, default 3), which moves it toDeleted_Segmentsand then purges it. That sweep is non-recursive, which is why the staged tar stays flat in thetable directory instead of a subdirectory. Deleting a staged tar is always safe because the controller never points
a download URL at it.
Testing
BaseSingleSegmentConversionExecutorTest:SegmentPushUtils.sendSegmentUriAndMetadataand never calls the new helper; still deletes the staged tar when the push fails (existing test).
(
If-Match,REFRESH_ONLY, custom-map,DOWNLOAD_URI,UPLOAD_TYPE, copy flag); the metadatatar contains exactly
metadata.propertiesandcreation.metaand describes the converted segment;a leftover staged tar from an earlier attempt is overwritten; a scheme-less output dir is sent as a
fileURI; an unchanged segment is not staged and is re-registered against its download URL.PurgeMinionClusterIntegrationTestpasses, includingtestFirstRunMetadataPushPurgeon theunchanged default path. The opt-in path was exercised end to end on a local-FS cluster from the
StarTree purge task that overrides the hook.
Follow-up: #19490 (stacked on this PR) builds the metadata tar from the local segment for every METADATA push, including the default path, so no path downloads the staged tar back.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Y375AgHYsh1YqNSsfvF1a8