Jackie-Jiang commented on code in PR #19776:
URL: https://github.com/apache/pinot/pull/19776#discussion_r4449360692
##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java:
##########
@@ -621,6 +627,47 @@ private boolean isPeerUrl(String segmentLocation) {
CommonConstants.Segment.PEER_SEGMENT_DOWNLOAD_SCHEME.length());
}
+ /// Moves the metadata sidecar the server uploaded under a tmp name to
`<segment>.metadata.tar.gz`, like
+ /// [#moveSegmentFile] does for the segment file. Best effort: a failure is
logged and counted, and never fails the
+ /// commit.
+ private void moveMetadataTarFile(String realtimeTableName, String
rawTableName, String segmentName,
+ @Nullable String metadataTarLocation, PinotFS pinotFS) {
+ if (metadataTarLocation == null) {
+ return;
+ }
+ URI uriToMoveTo = createSegmentPath(rawTableName, segmentName +
METADATA_TAR_GZ_FILE_EXT);
+ try {
+ if (pinotFS.move(URIUtils.getUri(metadataTarLocation), uriToMoveTo,
true)) {
+ LOGGER.info("Moved metadata tar of segment: {} from: {} to: {}",
segmentName, metadataTarLocation,
+ uriToMoveTo);
+ return;
+ }
+ LOGGER.warn("Failed to move metadata tar of segment: {} from: {} to:
{}", segmentName, metadataTarLocation,
+ uriToMoveTo);
+ } catch (Exception e) {
+ LOGGER.warn("Caught exception while moving metadata tar of segment: {}
from: {} to: {}", segmentName,
+ metadataTarLocation, uriToMoveTo, e);
+ }
+ _controllerMetrics.addMeteredTableValue(realtimeTableName,
ControllerMeter.REALTIME_METADATA_TAR_MOVE_FAILURE,
+ 1L);
+ }
+
+ /// Best effort, like [#moveMetadataTarFile]: a failure is logged and
counted, and never fails the commit.
+ private void deleteMetadataTarFile(String rawTableName, String segmentName,
PinotFS pinotFS) {
+ URI metadataTarUri = createSegmentPath(rawTableName, segmentName +
METADATA_TAR_GZ_FILE_EXT);
+ try {
+ if (!pinotFS.exists(metadataTarUri) || pinotFS.delete(metadataTarUri,
true)) {
+ return;
+ }
+ LOGGER.warn("Failed to delete previous metadata tar of segment: {} at:
{}", segmentName, metadataTarUri);
+ } catch (Exception e) {
+ LOGGER.warn("Caught exception while deleting previous metadata tar of
segment: {} at: {}", segmentName,
+ metadataTarUri, e);
Review Comment:
**[P1] Require successful sidecar invalidation before replacing the
segment.** If an earlier attempt published its segment and sidecar but failed
the ZK commit, another replica can retry with different segment bytes. Here a
failed existence check or deletion is logged and swallowed, so
`moveSegmentFile()` still replaces the full segment. If the retry has no
successfully published replacement sidecar, the previous metadata remains
beside different segment bytes. `SegmentPushUtils` trusts an existing sidecar
during metadata push without checking it against the full segment. Please
propagate/retry invalidation failures before replacing the segment; optional
creation can remain best effort. Add coverage for failed `exists()` and failed
deletion with an existing sidecar.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java:
##########
@@ -3115,6 +3162,10 @@ String moveSegmentFile(String rawTableName, String
segmentName, String segmentLo
throws IOException {
URI segmentFileURI = URIUtils.getUri(segmentLocation);
URI uriToMoveTo = createSegmentPath(rawTableName, segmentName);
+ // An earlier attempt for this segment (e.g. one that published its files
and then failed the ZK metadata commit)
+ // may have left a sidecar describing a different build. Drop it before
replacing the segment file, on both the
+ // commit and the deep store repair path, so a sidecar only ever describes
the segment file next to it.
+ deleteMetadataTarFile(rawTableName, segmentName, pinotFS);
Review Comment:
**[P2] Invalidate sidecars on controller-upload refreshes as well.** This
only covers the LLC commit/repair move. Upsert compaction can replace a
REALTIME segment under the same name through
`BaseSingleSegmentConversionExecutor`'s TAR upload and
`ZKOperator.copySegmentToDeepStore()`, which overwrites the full segment
without removing its sidecar. A later metadata push can therefore consume the
old CRC/document counts, and deletion preserves a mismatched pair. This
weakness already existed for some OFFLINE producers; this PR newly creates the
durable sidecar for normal REALTIME ingestion. The PR acknowledges the
limitation, but please apply invalidation to the common controller replacement
path too; regeneration is unnecessary when the upload contains only the full
tar.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java:
##########
@@ -621,6 +627,47 @@ private boolean isPeerUrl(String segmentLocation) {
CommonConstants.Segment.PEER_SEGMENT_DOWNLOAD_SCHEME.length());
}
+ /// Moves the metadata sidecar the server uploaded under a tmp name to
`<segment>.metadata.tar.gz`, like
+ /// [#moveSegmentFile] does for the segment file. Best effort: a failure is
logged and counted, and never fails the
+ /// commit.
+ private void moveMetadataTarFile(String realtimeTableName, String
rawTableName, String segmentName,
+ @Nullable String metadataTarLocation, PinotFS pinotFS) {
+ if (metadataTarLocation == null) {
+ return;
+ }
+ URI uriToMoveTo = createSegmentPath(rawTableName, segmentName +
METADATA_TAR_GZ_FILE_EXT);
+ try {
+ if (pinotFS.move(URIUtils.getUri(metadataTarLocation), uriToMoveTo,
true)) {
+ LOGGER.info("Moved metadata tar of segment: {} from: {} to: {}",
segmentName, metadataTarLocation,
+ uriToMoveTo);
+ return;
+ }
+ LOGGER.warn("Failed to move metadata tar of segment: {} from: {} to:
{}", segmentName, metadataTarLocation,
+ uriToMoveTo);
+ } catch (Exception e) {
+ LOGGER.warn("Caught exception while moving metadata tar of segment: {}
from: {} to: {}", segmentName,
+ metadataTarLocation, uriToMoveTo, e);
+ }
+ _controllerMetrics.addMeteredTableValue(realtimeTableName,
ControllerMeter.REALTIME_METADATA_TAR_MOVE_FAILURE,
+ 1L);
+ }
+
+ /// Best effort, like [#moveMetadataTarFile]: a failure is logged and
counted, and never fails the commit.
+ private void deleteMetadataTarFile(String rawTableName, String segmentName,
PinotFS pinotFS) {
+ URI metadataTarUri = createSegmentPath(rawTableName, segmentName +
METADATA_TAR_GZ_FILE_EXT);
+ try {
+ if (!pinotFS.exists(metadataTarUri) || pinotFS.delete(metadataTarUri,
true)) {
+ return;
+ }
+ LOGGER.warn("Failed to delete previous metadata tar of segment: {} at:
{}", segmentName, metadataTarUri);
+ } catch (Exception e) {
+ LOGGER.warn("Caught exception while deleting previous metadata tar of
segment: {} at: {}", segmentName,
+ metadataTarUri, e);
+ }
+
_controllerMetrics.addMeteredTableValue(TableNameBuilder.REALTIME.tableNameWithType(rawTableName),
+ ControllerMeter.REALTIME_METADATA_TAR_MOVE_FAILURE, 1L);
Review Comment:
**Cleanup: distinguish invalidation failures from publication failures.**
`REALTIME_METADATA_TAR_MOVE_FAILURE` counts failed deletion of previous
metadata as well as failed sidecar moves. Separate invalidation and publication
counters would make diagnosis and alerting clearer: failed invalidation can
leave stale metadata, while failed publication after successful invalidation
normally means falling back to the full segment.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/SegmentDeletionManager.java:
##########
@@ -369,6 +382,36 @@ private void moveSegmentsToDeletedDir(String segmentId,
Long deletedSegmentsRete
LOGGER.warn("Could not move segment {} from {} to {}", segmentId,
fileToDeleteURI.toString(),
deletedSegmentMoveDestURI.toString(), e);
}
+ if (_preserveDeletedSegmentMetadata) {
+ // Even if the segment object vanished after it was located, so no
orphan sidecar is left in the live directory.
+ moveSegmentMetadataToDeletedDir(pinotFS, fileToDeleteURI, segmentId,
rawTableName, retentionSuffix);
+ }
+ }
+
+ private void moveSegmentMetadataToDeletedDir(PinotFS pinotFS, URI
segmentFileUri, String segmentId,
+ String rawTableName, String retentionSuffix) {
+ // Retained sidecars live in their own subdirectory, so they can never
take the archive name of a segment (e.g. the
+ // sidecar of "foo" vs. a segment named "foo.metadata"). The name matches
the archived segment, so both carry the
+ // same retention suffix and expire at the same time.
+ try {
+ URI segmentMetadataUri =
SegmentPushUtils.generateSegmentMetadataURI(segmentFileUri.toString(),
segmentId);
+ if (!pinotFS.exists(segmentMetadataUri)) {
+ return;
+ }
+ URI deletedMetadataUri = URIUtils.getUri(_dataDir, DELETED_SEGMENTS,
rawTableName, DELETED_SEGMENT_METADATA_DIR,
+ URIUtils.encode(segmentId) + retentionSuffix);
+ if (pinotFS.move(segmentMetadataUri, deletedMetadataUri, true)) {
Review Comment:
**[P2] Keep the metadata directory outside the segment archive namespace.**
With preservation enabled, archive another segment's sidecar first, then delete
a valid OFFLINE segment named `metadata` using default/null retention. Its
full-segment archive destination is the existing
`Deleted_Segments/<table>/metadata` directory. `BasePinotFS.move(..., true)`
recursively deletes that destination before replacing it, removing all retained
sidecars for the table before their retention expires. On directory-based
stores, archiving the `metadata` segment first instead prevents later sidecar
moves beneath that file. Full segment archives remain intact. This is the
acknowledged reserved-name limitation, distinct from the now-fixed
`foo.metadata` collision. Please use a collision-free layout or safely handle
the collision before overwriting, with tests for both deletion orders.
##########
pinot-server/src/main/java/org/apache/pinot/server/starter/helix/HelixInstanceDataManagerConfig.java:
##########
@@ -121,6 +121,10 @@ public class HelixInstanceDataManagerConfig implements
InstanceDataManagerConfig
public static final String UPLOAD_SEGMENT_TO_DEEP_STORE =
"segment.upload.to.deep.store";
public static final boolean DEFAULT_UPLOAD_SEGMENT_TO_DEEP_STORE = false;
+ public static final String UPLOAD_SEGMENT_METADATA_TAR_GZ =
"segment.upload.metadata.tar.gz";
+ public static final boolean DEFAULT_UPLOAD_SEGMENT_METADATA_TAR_GZ = false;
+ public static final String UPLOAD_SEGMENT_METADATA_TAR_GZ_TIMEOUT_MS =
"segment.upload.metadata.tar.gz.timeout.ms";
Review Comment:
**Cleanup: document the flag's supported modes beside the config.** Please
explain that metadata upload requires the direct server-to-store uploader and
non-pauseless consumption; controller-upload commits and pauseless consumption
do not create sidecars. Also point out that preserving sidecars after deletion
requires `controller.deleted.segments.preserveMetadata=true` and positive
retention. The timeout independently bounds the additional metadata upload
wait. These details are useful at the config/accessor declaration so operators
and callers do not need the PR description or committer implementation to
discover the restrictions.
##########
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/PinotFSSegmentUploader.java:
##########
@@ -112,4 +116,81 @@ public URI uploadSegment(File segmentFile, LLCSegmentName
segmentName, int timeo
return null;
}
+
+ @Override
+ public boolean isMetadataTarUploadSupported() {
+ return true;
+ }
+
+ @Override
+ public URI uploadMetadataTar(File metadataTarFile, LLCSegmentName
segmentName, int timeoutInMillis) {
+ if (_segmentStoreUriStr == null || _segmentStoreUriStr.isEmpty()) {
+ LOGGER.error("Missing segment store uri. Failed to upload metadata tar
{} for {}.", metadataTarFile.getName(),
+ segmentName.getSegmentName());
+ return null;
+ }
+ final String rawTableName =
TableNameBuilder.extractRawTableName(segmentName.getTableName());
+ // Like the segment file, upload to a unique tmp name. The controller
moves it to <segment>.metadata.tar.gz at
+ // commit end. The final object is never written or deleted here, so a
failed or partial upload is never visible
+ // under the final name.
+ final URI destUri;
+ try {
+ destUri = new URI(StringUtil.join(File.separator, _segmentStoreUriStr,
segmentName.getTableName(),
+ SegmentCompletionUtils.generateTmpSegmentFileName(
+ segmentName.getSegmentName() +
Constants.METADATA_TAR_GZ_FILE_EXT)));
+ } catch (URISyntaxException e) {
+ LOGGER.warn("Invalid segment store uri {} for metadata tar of segment
{}", _segmentStoreUriStr, segmentName, e);
+ _serverMetrics.addMeteredTableValue(rawTableName,
ServerMeter.METADATA_TAR_UPLOAD_FAILURE, 1);
+ return null;
+ }
+ Callable<URI> uploadTask = () -> {
+ long startTime = System.currentTimeMillis();
+ try {
+ PinotFS pinotFS = PinotFSFactory.create(new
URI(_segmentStoreUriStr).getScheme());
+ pinotFS.copyFromLocalFile(metadataTarFile, destUri);
Review Comment:
**Optional cleanup: share the upload bookkeeping.** This largely repeats
`uploadSegment()`'s copy, timing, future wait, and outcome recording. A small
private helper could share those mechanics and reduce future drift between the
two upload paths. Keep the segment-specific and metadata-specific
cancellation/cleanup policies explicit when doing so; this suggestion is
non-blocking.
##########
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/SplitSegmentCommitter.java:
##########
@@ -100,4 +126,53 @@ protected String uploadSegment(File segmentTarFile,
SegmentUploader segmentUploa
}
return null;
}
+
+ // A peer:// location means the segment is NOT in deep store, so there is
nothing for a sidecar to describe.
+ // Mirrors the check in PinotLLCRealtimeSegmentManager.commitSegmentFile().
+ private static boolean isPeerSegmentLocation(String segmentLocation) {
+ return segmentLocation.regionMatches(true, 0,
CommonConstants.Segment.PEER_SEGMENT_DOWNLOAD_SCHEME, 0,
+ CommonConstants.Segment.PEER_SEGMENT_DOWNLOAD_SCHEME.length());
+ }
+
+ /// Best-effort: builds a tar containing one directory with
metadata.properties and creation.meta, and uploads it
+ /// under a tmp name next to the segment. Returns the tmp location for the
controller to move at commit end, or
+ /// null on any problem. Never fails or throws; a sidecar problem must not
affect the segment commit.
+ @Nullable
+ private String uploadMetadataTarQuietly(@Nullable Map<String, File>
metadataFiles, String segmentName) {
+ if (metadataFiles == null || metadataFiles.isEmpty()) {
+ return null;
+ }
+ String rawTableName = null;
+ File stagingDir = null;
+ File metadataTarFile = null;
+ try {
+ LLCSegmentName llcSegmentName = new LLCSegmentName(segmentName);
+ rawTableName =
TableNameBuilder.extractRawTableName(llcSegmentName.getTableName());
+ stagingDir = new File(FileUtils.getTempDirectory(), segmentName +
"_meta_" + UUID.randomUUID());
+ FileUtils.forceMkdir(stagingDir);
+ for (Map.Entry<String, File> entry : metadataFiles.entrySet()) {
+ // Byte-for-byte copy: creation.meta is binary.
+ FileUtils.copyFile(entry.getValue(), new File(stagingDir,
entry.getKey()));
+ }
+ metadataTarFile = new File(FileUtils.getTempDirectory(),
+ segmentName + "_" + UUID.randomUUID() +
Constants.METADATA_TAR_GZ_FILE_EXT);
+ // Tar the DIRECTORY so that the first archive entry is a directory, as
DefaultMetadataExtractor expects.
+ TarCompressionUtils.createCompressedTarFile(stagingDir, metadataTarFile);
Review Comment:
**Cleanup: share metadata archive construction with OFFLINE.** This
staging/copy/tar sequence duplicates
`SegmentPushUtils.generateSegmentMetadataFile(File, File, String)`. Consider
extracting an overload/helper accepting the metadata files while keeping each
caller's temporary-directory ownership explicit. That gives OFFLINE and
REALTIME one implementation of the top-level directory layout and byte-for-byte
`creation.meta` handling, reducing the chance that the archive formats diverge.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]