FrankChen021 commented on code in PR #20338:
URL: https://github.com/apache/druid/pull/20338#discussion_r4015561578


##########
server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java:
##########
@@ -347,6 +355,17 @@ private void addFilesToCachedSegments(
       }
 
       if (cacheEntry.checkExists(location.getPath())) {
+        // A complete layout on disk while partial downloads are enabled is a 
leftover from before they were turned
+        // on. Convert it to a partial layout now rather than leaving it to be 
evicted the first time a partial-load
+        // rule targets the segment: that eviction discards a warm cache and 
cannot be undone if the rule's load then
+        // fails. Converting keeps the bytes, and it means a complete entry 
can no longer exist for a range-readable
+        // segment by the time anything applies a rule.
+        if (convertCompleteLayoutToPartial(segment, location)) {

Review Comment:
   [P1] Do not report a lost layout as cached
   
   **Finding:** When the conversion has already moved/deleted the complete 
directory but the final move from staging fails, convertCompleteLayoutToPartial 
returns false. This caller treats every false result as an intact complete 
cache hit, sets removeInfo to false, and returns the segment from 
getCachedSegments even though neither the complete directory nor a partial 
directory is usable. bootstrap then finds no reservation and returns the 
segment without mounting it, so startup can announce an unavailable segment 
instead of retrying or failing the load.
   
   **Suggestion:** Return a distinct outcome for a conversion that lost the 
source layout, or revalidate the installed layout here and leave the info entry 
uncached / fail bootstrap when no usable directory remains.



##########
server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java:
##########
@@ -370,6 +389,101 @@ private void addFilesToCachedSegments(
     }
   }
 
+  /**
+   * Rewrite a complete on-disk cache layout for {@code segment} as a partial 
(bundle) layout, returning whether it
+   * did. Called from {@link #getCachedSegments}, which runs before anything 
is reserved, mounted, or serving, so
+   * there is no live entry to unmap and no cache key to swap. This method 
should never be called on a 'live' entry.
+   * <p>
+   * The conversion reads the local complete file as if it were deep storage, 
via a
+   * {@link DirectoryBackedRangeReader}, and materializes the bundle layout 
from it. Nothing is fetched remotely.
+   * Deep storage is still probed for range-readability, because a partial 
entry that cannot range-read deep storage
+   * could never fetch a bundle it is missing later, and {@link 
#reservePartialForBootstrap} reclaims such a layout
+   * on the next boot anyway.
+   * <p>
+   * Transient disk cost is one segment: the complete file has to stay 
readable until the bundles are written. If
+   * anything fails, the complete layout is left exactly as it was and the 
segment loads the old way, so a location
+   * without the headroom degrades instead of breaking. Staging happens under 
{@link #CONVERT_PATH}, swept at
+   * startup, so a crash leaves garbage rather than a half-converted segment 
directory. The one lossy window is a
+   * crash between removing the complete layout and moving the converted one 
into place, which costs a re-download
+   * of that segment.
+   */
+  private boolean convertCompleteLayoutToPartial(final DataSegment segment, 
final StorageLocation location)
+  {
+    if (!config.isVirtualStorage() || 
!config.isVirtualStoragePartialDownloadsEnabled()) {
+      return false;
+    }
+    final File completeDir = new File(location.getPath(), 
segment.getId().toString());
+    if (!new File(completeDir, IndexIO.V10_FILE_NAME).exists()) {
+      // Only a V10 segment file can be re-sliced into bundles.
+      return false;
+    }
+    try {
+      if (tryOpenRangeReader(segment) == null) {
+        return false;
+      }
+    }
+    catch (Exception e) {
+      log.warn(
+          e,
+          "Failed to open a range reader for segment[%s]; leaving its complete 
cache layout alone",
+          segment.getId()
+      );
+      return false;
+    }
+
+    final File stagingDir = new File(new File(location.getPath(), 
CONVERT_PATH), segment.getId().toString());
+    boolean completeLayoutRemoved = false;
+    try {
+      if (stagingDir.exists()) {
+        FileUtils.deleteDirectory(stagingDir);
+      }
+      try (PartialSegmentFileMapperV10 mapper = 
PartialSegmentFileMapperV10.create(
+          new DirectoryBackedRangeReader(completeDir),
+          jsonMapper,
+          stagingDir,
+          IndexIO.V10_FILE_NAME,
+          List.of(),
+          PartialSegmentDownloadListener.NOOP,
+          config.getVirtualStorageCoalesceGapBytes(),
+          config.getVirtualStorageMaxFetchRunBytes()
+      )) {
+        mapper.ensureAllDownloaded();
+      }
+      atomicMoveAndDeleteCacheEntryDirectory(completeDir);
+      completeLayoutRemoved = true;
+      Files.move(stagingDir.toPath(), completeDir.toPath(), 
StandardCopyOption.ATOMIC_MOVE);

Review Comment:
   [P1] Preserve custom segment factory metadata
   
   **Finding:** The staging directory contains only the mapper header and 
container files, so replacing completeDir here deletes auxiliary files such as 
factory.json. CompleteSegmentCacheEntry.mount explicitly reads factory.json to 
select a non-default SegmentizerFactory, while the partial bootstrap path 
constructs a default PartialQueryableIndexSegment. A range-readable V10 segment 
that was built with a custom factory can therefore silently lose behavior (for 
example, a broadcast-joinable segment) on the first restart after partial 
downloads are enabled.
   
   **Suggestion:** Preserve the complete layout's required auxiliary files and 
carry their semantics into the partial path, or exclude layouts with a 
non-default factory from this migration and cover that case.



##########
server/src/main/java/org/apache/druid/segment/loading/SegmentLocalCacheManager.java:
##########
@@ -370,6 +389,101 @@ private void addFilesToCachedSegments(
     }
   }
 
+  /**
+   * Rewrite a complete on-disk cache layout for {@code segment} as a partial 
(bundle) layout, returning whether it
+   * did. Called from {@link #getCachedSegments}, which runs before anything 
is reserved, mounted, or serving, so
+   * there is no live entry to unmap and no cache key to swap. This method 
should never be called on a 'live' entry.
+   * <p>
+   * The conversion reads the local complete file as if it were deep storage, 
via a
+   * {@link DirectoryBackedRangeReader}, and materializes the bundle layout 
from it. Nothing is fetched remotely.
+   * Deep storage is still probed for range-readability, because a partial 
entry that cannot range-read deep storage
+   * could never fetch a bundle it is missing later, and {@link 
#reservePartialForBootstrap} reclaims such a layout
+   * on the next boot anyway.
+   * <p>
+   * Transient disk cost is one segment: the complete file has to stay 
readable until the bundles are written. If
+   * anything fails, the complete layout is left exactly as it was and the 
segment loads the old way, so a location
+   * without the headroom degrades instead of breaking. Staging happens under 
{@link #CONVERT_PATH}, swept at
+   * startup, so a crash leaves garbage rather than a half-converted segment 
directory. The one lossy window is a
+   * crash between removing the complete layout and moving the converted one 
into place, which costs a re-download
+   * of that segment.
+   */
+  private boolean convertCompleteLayoutToPartial(final DataSegment segment, 
final StorageLocation location)
+  {
+    if (!config.isVirtualStorage() || 
!config.isVirtualStoragePartialDownloadsEnabled()) {
+      return false;
+    }
+    final File completeDir = new File(location.getPath(), 
segment.getId().toString());
+    if (!new File(completeDir, IndexIO.V10_FILE_NAME).exists()) {

Review Comment:
   [P1] Honor the incomplete-download marker
   
   **Finding:** CompleteSegmentCacheEntry.mount uses downloadStartMarker to 
detect a directory that may have been damaged by an interrupted or failed full 
download and deliberately deletes and re-fetches it. This migration checks only 
for druid.segment, so it can read such a directory, write a partial layout, and 
discard the marker before the normal corruption-recovery path runs. If the main 
file is present but contains damaged bytes, the mapper's header/range reads do 
not restore that integrity check and the damaged cache can be announced as 
usable.
   
   **Suggestion:** Skip conversion whenever isPossiblyCorrupted(completeDir) is 
true and let the existing complete-entry mount path reclaim and re-download the 
marked directory.



-- 
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]

Reply via email to