qgambit2 opened a new issue, #19680:
URL: https://github.com/apache/druid/issues/19680

   ### Affected version
   
   `master` (feature introduced in #19535, "on-demand partial segment 
loading"). The feature is gated off by default behind 
`druid.segmentCache.virtualStorage` + 
`druid.segmentCache.virtualStoragePartialDownloadsEnabled`.
   
   > Note: this was found by static analysis while reading the #19535 change; I 
have **not** reproduced it at runtime yet (a repro sketch is included below). 
Filing it because the code path looks deterministically broken across restarts 
and is currently untested.
   
   ### Description
   
   With partial downloads enabled, a segment can be cached with only some of 
its containers on local disk. After a process restart, 
`PartialSegmentCacheBootstrap.reserveFromDisk` restores such entries from disk 
— but builds each restored `PartialSegmentMetadataCacheEntry` with 
`BootstrapRangeReader.INSTANCE`, whose `readRange()` unconditionally throws:
   
   ```java
   // PartialSegmentCacheBootstrap.BootstrapRangeReader
   public InputStream readRange(String filename, long offset, long length) {
     throw DruidException.defensive(
         "BootstrapRangeReader was asked to fetch [%s] @[%d:%d]; bootstrap 
should only read from local disk", ...);
   }
   ```
   
   A partially-downloaded segment restores with `isFullyDownloaded() == false` 
(that requires `downloadedFiles.containsAll(metadata.getFiles().keySet())`, 
while `restoreBundlesFromDisk` admits any bundle whose container files merely 
exist). When a query later acquires that segment, 
`SegmentLocalCacheManager.acquireSegment` opens a real working reader via 
`tryOpenRangeReader`, but `findOrReservePartial` reuses the pre-existing 
restored entry and **discards** that reader:
   
   ```java
   final ReservedPartial existing = 
findExistingPartialWithHold(dataSegment.getId());
   if (existing != null) { return existing; }          // the real rangeReader 
arg is dropped
   return reservePartial(dataSegment, rangeReader);     // only reached when no 
entry exists yet
   ```
   
   The reader can't be replaced afterward — the entry's and the file mapper's 
`rangeReader` fields are both `final`, and `doMount` builds the mapper exactly 
once. So the download then runs through the restored entry's throw-only 
`BootstrapRangeReader`:
   
   - **`AcquireMode.FULL`:** `ensureAllDownloaded()` → `downloadContainer` → 
`streamRangeIntoContainer` → `rangeReader.readRange(...)` → throws.
   - **`AcquireMode.PARTIAL`:** at cursor build, `mapFile` → 
`ensureFileDownloaded` → `streamRangeIntoContainer` → 
`rangeReader.readRange(...)` → throws.
   
   ### Impact
   
   After any restart, a query that needs not-yet-cached bytes of a 
partially-downloaded segment fails with a `DruidException` (defensive) instead 
of fetching the missing bytes from deep storage — i.e., the feature's core 
lazy-load path is broken across restarts. Only deployments running partial 
downloads (the off-by-default experimental flags) are affected.
   
   ### Existing mitigation doesn't cover this path
   
   `doMount`'s cleanup (and `testMountFailureRemovesLingeringWeakEntry`) handle 
the **mount-time** header-refetch case: on mount failure, 
`removeUnheldWeakEntry` drops the stale entry. But that only fires on mount 
failure. When the header file is present the mount **succeeds**, the entry 
persists, and the later container/file download throws with no fallback. 
`SegmentLocalCacheManagerPartialAcquireTest` exercises `acquireSegment` + 
download only via **fresh** reservations (real reader), so the 
restore-then-download path is untested.
   
   ### Suggested fix
   
   When an on-demand acquire reuses a pre-existing (bootstrap-restored) 
metadata entry, replace its `BootstrapRangeReader` with the real reader from 
`tryOpenRangeReader` — or evict + re-reserve the entry via `reservePartial` — 
so restored partial segments can complete downloads. A regression test that 
restores a partially-downloaded entry and then drives both a FULL and a PARTIAL 
acquire would lock it in.
   
   ### Suggested reproduction (not yet run)
   
   1. Enable `druid.segmentCache.virtualStorage=true` and 
`druid.segmentCache.virtualStoragePartialDownloadsEnabled=true` on a 
historical/MSQ worker (non-ephemeral, the default).
   2. Run a query that lazily downloads only part of a segment, so its on-disk 
state is partial.
   3. Restart the process (triggering `bootstrap()` → `reserveFromDisk`).
   4. Run a query touching a column/range that wasn't previously downloaded.
   5. Expected: the missing bytes are fetched from deep storage. Observed (per 
this trace): `DruidException` from `BootstrapRangeReader.readRange`.
   
   *Traced across `PartialSegmentCacheBootstrap`, `SegmentLocalCacheManager`, 
`PartialSegmentMetadataCacheEntry`, and `PartialSegmentFileMapperV10` by 
inspection.*
   


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