Copilot commented on code in PR #11428:
URL: https://github.com/apache/ozone/pull/11428#discussion_r4266550249


##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java:
##########
@@ -165,13 +166,14 @@ public class SnapshotDiffManager implements 
AutoCloseable, SnapshotDiffManagerMX
   private static final Logger LOG =
       LoggerFactory.getLogger(SnapshotDiffManager.class);
   private static final Map<DiffType, String> DIFF_TYPE_STRING_MAP =
-      new EnumMap<>(ImmutableMap.of(DELETE, "1", RENAME, "2", CREATE, "3", 
MODIFY, "4"));
+      new EnumMap<>(ImmutableMap.of(DELETE, "1", MODIFY, "2", RENAME, "3", 
CREATE, "4"));

Review Comment:
   Changing `DIFF_TYPE_STRING_MAP` redefines the persisted report-key prefixes 
for diff types. Existing DONE jobs written with the old prefix mapping may be 
misinterpreted during pagination/token parsing after upgrade (e.g., old '2' 
keys that meant RENAME now map to MODIFY). Consider preserving the legacy 
mapping for backward compatibility, or explicitly versioning the report-key 
layout and supporting both mappings when reading.



##########
hadoop-hdds/rocks-native/src/main/java/org/apache/hadoop/hdds/utils/db/LatestVersionedKWayMergeIterator.java:
##########
@@ -59,41 +60,42 @@ public final class LatestVersionedKWayMergeIterator 
implements
   private List<MergedKeyValue> emitQueue;
   private boolean initialized;
 
-  public static LatestVersionedKWayMergeIterator 
overRawSstFiles(Collection<Path> sstFiles) {
-    return overRawSstFiles(sstFiles, DEFAULT_READ_AHEAD_SIZE, null);
-  }
-
-  public static LatestVersionedKWayMergeIterator 
overRawSstFiles(Collection<Path> sstFiles,
-      int readAheadSizePerFile) {
-    return overRawSstFiles(sstFiles, readAheadSizePerFile, null);
-  }
-
   /**
-   * Opens one iterator per SST file and merges them.
+   * Opens one iterator per SST file and merges them within an optional 
user-key range.
    *
    * @param exclusiveMinSequenceNumber when non-null, each source skips 
entries with
    *        sequence {@code <=} this value while advancing; when null, no 
entries are skipped
+   * @param fromUserKeyInclusive when non-null and non-empty, each source 
seeks to this lower bound
+   * @param toUserKeyExclusive when non-null and non-empty, each source stops 
before this upper bound
+   * @apiNote {@code fromUserKeyInclusive} and {@code toUserKeyExclusive} are 
wrapped by native
+   *          {@link org.apache.hadoop.hdds.utils.db.managed.ManagedSlice} 
objects without copying.
+   *          Callers must not mutate either array until the returned iterator 
is closed.
    */
   public static LatestVersionedKWayMergeIterator 
overRawSstFiles(Collection<Path> sstFiles,
-      int readAheadSizePerFile, Long exclusiveMinSequenceNumber) {
+      int readAheadSizePerFile, Long exclusiveMinSequenceNumber,
+      byte[] fromUserKeyInclusive, byte[] toUserKeyExclusive) {
     Objects.requireNonNull(sstFiles, "sstFiles cannot be null");
-    ManagedOptions options = new ManagedOptions();
     List<ClosableIterator<? extends MergeHead>> sources = new 
ArrayList<>(sstFiles.size());
-    try {
+    try (ManagedOptions options = new ManagedOptions()) {
       for (Path file : sstFiles) {
-        sources.add(new RawSstIterator(options, file, readAheadSizePerFile));
+        sources.add(new RawSstIterator(options, file, readAheadSizePerFile, 
fromUserKeyInclusive,
+            toUserKeyExclusive));
       }
       return new LatestVersionedKWayMergeIterator(options, sources, 
exclusiveMinSequenceNumber);
     } catch (RuntimeException e) {
       IOUtils.closeQuietly(sources);
-      options.close();
       throw e;
     }
   }

Review Comment:
   The `ManagedOptions` is created in a try-with-resources but then passed into 
`LatestVersionedKWayMergeIterator` and returned; the try-with-resources will 
close `options` before the caller uses the iterator, leaving the iterator 
holding a closed native resource. Allocate `ManagedOptions` without 
try-with-resources here and let `LatestVersionedKWayMergeIterator.close()` 
own/close it (and still close it in the exception path).



##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/diff/SnapDiffJobStore.java:
##########
@@ -39,118 +59,174 @@
  * Owns the per-job temporary RocksDB column families and batched writes shared
  * between optimized snapshot diff pipeline stages.
  *
- * <p>Diff-candidate {@code objectId}s are held in memory while their count is 
at
- * most {@code maxInMemoryEntries}; larger sets spill to a temporary column 
family.
+ * <p>Diff-candidate {@code objectId}s and DAG directory tombstone table keys 
are held in
+ * memory while their count is at most {@code maxInMemoryEntries}; larger sets 
spill to a
+ * temporary column family. Deleted and renamed directory object ids collected 
during merge
+ * join use the same in-memory limit and spill policy.
  *
  * <p>All RocksDB puts use raw {@code byte[]} keys and values (the JNI 
boundary). Callers
  * serialize {@link EntryValue} via {@link EntryValue#toBytes()} before 
writing to
  * {@code newList}/{@code oldList}.
  *
- * <p>This initial version supports the full diff sequential reader ({@link 
FullDiffSequentialReader}).
- * DAG diff support extends this store in HDDS-15393.
+ * <p>Temporary column families disable RocksDB auto-compactions and are 
dropped once a pipeline
+ * stage no longer reads them. Individual key deletes are avoided so 
short-lived CFs do not
+ * accumulate tombstones before drop.
+ *
+ * <p>Shared by the full diff sequential reader ({@link 
FullDiffSequentialReader}) and the
+ * DAG diff sequential reader ({@link DagDiffSequentialReader}).
  */
 public final class SnapDiffJobStore implements AutoCloseable {
 
   /** Default RocksDB {@code WriteBatch} commit size for job-store puts. */
-  public static final int DEFAULT_WRITE_BATCH_SIZE = 1000;
+  public static final int DEFAULT_BATCH_SIZE = 1000;
+
+  /** Reverse edge value header: parentId (8 BE) + nameLen (4 BE). */
+  static final int EDGE_LINK_HEADER_BYTES = Long.BYTES + Integer.BYTES;
+
+  private static final Comparator<byte[]> TOMBSTONE_KEY_COMPARATOR =
+      UnsignedBytes.lexicographicalComparator();
 
   private static final String NEW_LIST_SUFFIX = "-new-list";
   private static final String OLD_LIST_SUFFIX = "-old-list";
   private static final String CAND_IDS_SUFFIX = "-cand-ids";
+  private static final String DIR_TOMBSTONE_KEYS_SUFFIX = 
"-dir-tombstone-keys";
   private static final String TO_EDGES_SUFFIX = "-to-edges";
   private static final String FROM_EDGES_SUFFIX = "-from-edges";
+  private static final String CLASSIFIED_CREATE_SUFFIX = "-cls-create";
+  private static final String CLASSIFIED_DELETE_SUFFIX = "-cls-delete";
+  private static final String CLASSIFIED_MODIFY_SUFFIX = "-cls-modify";
+  private static final String CLASSIFIED_RENAME_SUFFIX = "-cls-rename";
+  private static final String DEPENDENCY_NODE_SUFFIX = "-dependency-nodes";
+  private static final String DELETED_DIR_IDS_SUFFIX = "-deleted-dir-ids";
+  private static final String RENAMED_DIR_IDS_SUFFIX = "-renamed-dir-ids";
 
   private final ManagedRocksDB db;
+  private final String jobId;
   private final boolean fso;
+  private final CodecRegistry codecRegistry;
+  private final ColumnFamilyHandle reportCfh;
   private final byte[] presentMarker;
   private final ManagedColumnFamilyOptions familyOptions;
   private final long maxInMemoryEntries;
+  private final List<ColumnFamilyHandle> temporaryColumnFamilies = new 
ArrayList<>();
 
   private ColumnFamilyHandle newListCf;
   private ColumnFamilyHandle oldListCf;
   private ColumnFamilyHandle toEdgesCf;
   private ColumnFamilyHandle fromEdgesCf;
+  private ColumnFamilyHandle classifiedCreateCf;
+  private ColumnFamilyHandle classifiedDeleteCf;
+  private ColumnFamilyHandle classifiedModifyCf;
+  private ColumnFamilyHandle classifiedRenameCf;
+  private ColumnFamilyHandle dependencyNodesCf;
 
   private String diffCandCfName;
   private Set<Long> diffCandidates;
   private ColumnFamilyHandle diffCandidatesCf;
   private boolean diffCandidatesSpilled;
 
+  private String dirTombstoneKeysCfName;
+  private NavigableSet<byte[]> dirTombstoneKeys;
+  private ColumnFamilyHandle dirTombstoneKeysCf;
+  private boolean dirTombstoneKeysSpilled;
+
+  private Set<Long> deletedDirectoryIds;
+  private Set<Long> renamedDirectoryIds;
+  private String deletedDirectoryIdsCfName;
+  private String renamedDirectoryIdsCfName;
+  private ColumnFamilyHandle deletedDirectoryIdsCf;
+  private ColumnFamilyHandle renamedDirectoryIdsCf;
+  private boolean deletedDirectoryIdsSpilled;
+  private boolean renamedDirectoryIdsSpilled;
+
   private final ManagedWriteBatch writeBatch;
   private final ManagedWriteOptions writeOptions;
   private final int writeBatchSize;
   private int pendingOps;
 
+  private long reportIndex;
+  private String largestReportKey;
+  private boolean reportWriteStarted;
+
   /** Reusable big-endian key buffers; safe because RocksDB copies keys on 
put/get. */
   private final byte[] objectIdKeyBuffer = new byte[Long.BYTES];
-  private final byte[] edgeKeyBuffer = new byte[2 * Long.BYTES];
+  private final byte[] intKeyBuffer = new byte[Integer.BYTES];
 
   /** Full diff: shared new/old lists plus FSO edge column families. */
   public enum Mode {
-    FULL
+    FULL,
+    DAG
   }
 
-  private SnapDiffJobStore(ManagedRocksDB db, CodecRegistry codecRegistry, 
boolean fso,
-      int writeBatchSize, ManagedColumnFamilyOptions familyOptions, long 
maxInMemoryEntries)
-      throws IOException {
+  @SuppressWarnings("checkstyle:ParameterNumber")
+  private SnapDiffJobStore(ManagedRocksDB db, String jobId, CodecRegistry 
codecRegistry, boolean fso,
+      int writeBatchSize, ManagedColumnFamilyOptions familyOptions, long 
maxInMemoryEntries,
+      @Nonnull ColumnFamilyHandle reportCfh) throws IOException {
     this.db = db;
+    this.jobId = jobId;
     this.fso = fso;
+    this.codecRegistry = codecRegistry;
+    this.reportCfh = reportCfh;
     this.writeBatchSize = writeBatchSize;
     this.familyOptions = familyOptions;
+    this.familyOptions.setDisableAutoCompactions(true);

Review Comment:
   `SnapDiffJobStore` mutates the passed-in `ManagedColumnFamilyOptions` 
instance by disabling auto-compactions. If the same options object is 
shared/reused elsewhere (including for non-temporary CFs), this can 
unintentionally affect RocksDB behavior beyond the job-store CFs. Consider 
using a dedicated options instance for temporary CFs (copy/clone if supported) 
instead of mutating the caller-provided one.



##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/diff/SnapDiffJobStore.java:
##########
@@ -39,118 +59,174 @@
  * Owns the per-job temporary RocksDB column families and batched writes shared
  * between optimized snapshot diff pipeline stages.
  *
- * <p>Diff-candidate {@code objectId}s are held in memory while their count is 
at
- * most {@code maxInMemoryEntries}; larger sets spill to a temporary column 
family.
+ * <p>Diff-candidate {@code objectId}s and DAG directory tombstone table keys 
are held in
+ * memory while their count is at most {@code maxInMemoryEntries}; larger sets 
spill to a
+ * temporary column family. Deleted and renamed directory object ids collected 
during merge
+ * join use the same in-memory limit and spill policy.
  *
  * <p>All RocksDB puts use raw {@code byte[]} keys and values (the JNI 
boundary). Callers
  * serialize {@link EntryValue} via {@link EntryValue#toBytes()} before 
writing to
  * {@code newList}/{@code oldList}.
  *
- * <p>This initial version supports the full diff sequential reader ({@link 
FullDiffSequentialReader}).
- * DAG diff support extends this store in HDDS-15393.
+ * <p>Temporary column families disable RocksDB auto-compactions and are 
dropped once a pipeline
+ * stage no longer reads them. Individual key deletes are avoided so 
short-lived CFs do not
+ * accumulate tombstones before drop.
+ *
+ * <p>Shared by the full diff sequential reader ({@link 
FullDiffSequentialReader}) and the
+ * DAG diff sequential reader ({@link DagDiffSequentialReader}).
  */
 public final class SnapDiffJobStore implements AutoCloseable {
 
   /** Default RocksDB {@code WriteBatch} commit size for job-store puts. */
-  public static final int DEFAULT_WRITE_BATCH_SIZE = 1000;
+  public static final int DEFAULT_BATCH_SIZE = 1000;
+
+  /** Reverse edge value header: parentId (8 BE) + nameLen (4 BE). */
+  static final int EDGE_LINK_HEADER_BYTES = Long.BYTES + Integer.BYTES;
+
+  private static final Comparator<byte[]> TOMBSTONE_KEY_COMPARATOR =
+      UnsignedBytes.lexicographicalComparator();
 
   private static final String NEW_LIST_SUFFIX = "-new-list";
   private static final String OLD_LIST_SUFFIX = "-old-list";
   private static final String CAND_IDS_SUFFIX = "-cand-ids";
+  private static final String DIR_TOMBSTONE_KEYS_SUFFIX = 
"-dir-tombstone-keys";
   private static final String TO_EDGES_SUFFIX = "-to-edges";
   private static final String FROM_EDGES_SUFFIX = "-from-edges";
+  private static final String CLASSIFIED_CREATE_SUFFIX = "-cls-create";
+  private static final String CLASSIFIED_DELETE_SUFFIX = "-cls-delete";
+  private static final String CLASSIFIED_MODIFY_SUFFIX = "-cls-modify";
+  private static final String CLASSIFIED_RENAME_SUFFIX = "-cls-rename";
+  private static final String DEPENDENCY_NODE_SUFFIX = "-dependency-nodes";
+  private static final String DELETED_DIR_IDS_SUFFIX = "-deleted-dir-ids";
+  private static final String RENAMED_DIR_IDS_SUFFIX = "-renamed-dir-ids";
 
   private final ManagedRocksDB db;
+  private final String jobId;
   private final boolean fso;
+  private final CodecRegistry codecRegistry;
+  private final ColumnFamilyHandle reportCfh;
   private final byte[] presentMarker;
   private final ManagedColumnFamilyOptions familyOptions;
   private final long maxInMemoryEntries;
+  private final List<ColumnFamilyHandle> temporaryColumnFamilies = new 
ArrayList<>();
 
   private ColumnFamilyHandle newListCf;
   private ColumnFamilyHandle oldListCf;
   private ColumnFamilyHandle toEdgesCf;
   private ColumnFamilyHandle fromEdgesCf;
+  private ColumnFamilyHandle classifiedCreateCf;
+  private ColumnFamilyHandle classifiedDeleteCf;
+  private ColumnFamilyHandle classifiedModifyCf;
+  private ColumnFamilyHandle classifiedRenameCf;
+  private ColumnFamilyHandle dependencyNodesCf;
 
   private String diffCandCfName;
   private Set<Long> diffCandidates;
   private ColumnFamilyHandle diffCandidatesCf;
   private boolean diffCandidatesSpilled;
 
+  private String dirTombstoneKeysCfName;
+  private NavigableSet<byte[]> dirTombstoneKeys;
+  private ColumnFamilyHandle dirTombstoneKeysCf;
+  private boolean dirTombstoneKeysSpilled;
+
+  private Set<Long> deletedDirectoryIds;
+  private Set<Long> renamedDirectoryIds;
+  private String deletedDirectoryIdsCfName;
+  private String renamedDirectoryIdsCfName;
+  private ColumnFamilyHandle deletedDirectoryIdsCf;
+  private ColumnFamilyHandle renamedDirectoryIdsCf;
+  private boolean deletedDirectoryIdsSpilled;
+  private boolean renamedDirectoryIdsSpilled;
+
   private final ManagedWriteBatch writeBatch;
   private final ManagedWriteOptions writeOptions;
   private final int writeBatchSize;
   private int pendingOps;
 
+  private long reportIndex;
+  private String largestReportKey;
+  private boolean reportWriteStarted;
+
   /** Reusable big-endian key buffers; safe because RocksDB copies keys on 
put/get. */
   private final byte[] objectIdKeyBuffer = new byte[Long.BYTES];
-  private final byte[] edgeKeyBuffer = new byte[2 * Long.BYTES];
+  private final byte[] intKeyBuffer = new byte[Integer.BYTES];
 
   /** Full diff: shared new/old lists plus FSO edge column families. */
   public enum Mode {
-    FULL
+    FULL,
+    DAG
   }
 
-  private SnapDiffJobStore(ManagedRocksDB db, CodecRegistry codecRegistry, 
boolean fso,
-      int writeBatchSize, ManagedColumnFamilyOptions familyOptions, long 
maxInMemoryEntries)
-      throws IOException {
+  @SuppressWarnings("checkstyle:ParameterNumber")
+  private SnapDiffJobStore(ManagedRocksDB db, String jobId, CodecRegistry 
codecRegistry, boolean fso,
+      int writeBatchSize, ManagedColumnFamilyOptions familyOptions, long 
maxInMemoryEntries,
+      @Nonnull ColumnFamilyHandle reportCfh) throws IOException {
     this.db = db;
+    this.jobId = jobId;
     this.fso = fso;
+    this.codecRegistry = codecRegistry;
+    this.reportCfh = reportCfh;
     this.writeBatchSize = writeBatchSize;
     this.familyOptions = familyOptions;
+    this.familyOptions.setDisableAutoCompactions(true);
     this.maxInMemoryEntries = maxInMemoryEntries;
     this.presentMarker = codecRegistry.asRawData(Boolean.TRUE);
     this.writeBatch = new ManagedWriteBatch();
     this.writeOptions = new ManagedWriteOptions();
     this.pendingOps = 0;
     this.diffCandidates = new HashSet<>();
+    this.dirTombstoneKeys = new TreeSet<>(TOMBSTONE_KEY_COMPARATOR);
+    this.deletedDirectoryIds = new HashSet<>();
+    this.renamedDirectoryIds = new HashSet<>();
   }
 
+  @SuppressWarnings("checkstyle:ParameterNumber")
   public static SnapDiffJobStore open(@Nonnull ManagedRocksDB db,
       @Nonnull CodecRegistry codecRegistry,
       @Nonnull ManagedColumnFamilyOptions familyOptions,
       @Nonnull String jobId,
       boolean fso,
-      @Nonnull Mode mode) throws IOException {
-    return open(db, codecRegistry, familyOptions, jobId, fso, mode, 
DEFAULT_WRITE_BATCH_SIZE,
-        OZONE_OM_SNAPSHOT_DIFF_MAX_IN_MEMORY_ENTRIES_PER_JOB_DEFAULT);
-  }
-
-  public static SnapDiffJobStore open(@Nonnull ManagedRocksDB db,
-      @Nonnull CodecRegistry codecRegistry,
-      @Nonnull ManagedColumnFamilyOptions familyOptions,
-      @Nonnull String jobId,
-      boolean fso,
-      @Nonnull Mode mode,
-      int writeBatchSize) throws IOException {
-    return open(db, codecRegistry, familyOptions, jobId, fso, mode, 
writeBatchSize,
-        OZONE_OM_SNAPSHOT_DIFF_MAX_IN_MEMORY_ENTRIES_PER_JOB_DEFAULT);
-  }
-
-  @SuppressWarnings("parameternumber")
-  public static SnapDiffJobStore open(@Nonnull ManagedRocksDB db,
-      @Nonnull CodecRegistry codecRegistry,
-      @Nonnull ManagedColumnFamilyOptions familyOptions,
-      @Nonnull String jobId,
-      boolean fso,
-      @Nonnull Mode mode,
-      int writeBatchSize,
-      long maxInMemoryEntries) throws IOException {
-    if (mode != Mode.FULL) {
-      throw new IllegalArgumentException("Unsupported mode: " + mode);
-    }
-    SnapDiffJobStore store = new SnapDiffJobStore(db, codecRegistry, fso, 
writeBatchSize,
-        familyOptions, maxInMemoryEntries);
+      @Nonnull ColumnFamilyHandle reportCfh,
+      @Nullable Integer writeBatchSize,
+      @Nullable Long maxInMemoryEntries) throws IOException {
+    int resolvedWriteBatchSize = writeBatchSize != null ? writeBatchSize : 
DEFAULT_BATCH_SIZE;
+    long resolvedMaxInMemoryEntries = maxInMemoryEntries != null ? 
maxInMemoryEntries
+        : OZONE_OM_SNAPSHOT_DIFF_MAX_IN_MEMORY_ENTRIES_PER_JOB_DEFAULT;
+    SnapDiffJobStore store = new SnapDiffJobStore(db, jobId, codecRegistry, 
fso, resolvedWriteBatchSize,
+        familyOptions, resolvedMaxInMemoryEntries, reportCfh);
     try {
-      store.initColumnFamilies(familyOptions, jobId);
+      store.initColumnFamilies(familyOptions);
       return store;
     } catch (RocksDBException e) {
       store.closeQuietly();
       throw new IOException("Failed to open SnapDiff job store for job " + 
jobId, e);
     }
   }
 
-  public boolean isFso() {
-    return fso;
+  long getMaxInMemoryEntries() {
+    return maxInMemoryEntries;
+  }
+
+  int getWriteBatchSize() {
+    return writeBatchSize;
+  }
+
+  /** Returns report entries written during a test run. */
+  List<DiffReportEntry> readReportEntriesForTest() throws IOException {
+    List<DiffReportEntry> entries = new ArrayList<>();
+    try (ManagedRocksIterator iterator = new 
ManagedRocksIterator(db.get().newIterator(reportCfh))) {
+      iterator.get().seek(codecRegistry.asRawData(jobId));
+      while (iterator.get().isValid()) {
+        String key = codecRegistry.asObject(iterator.get().key(), 
String.class);
+        if (!key.startsWith(jobId)) {
+          break;
+        }
+        entries.add(codecRegistry.asObject(iterator.get().value(), 
DiffReportEntry.class));
+        iterator.get().next();
+      }
+    }
+    return entries;
   }

Review Comment:
   This test helper seeks to `jobId` (without the delimiter) and filters with 
`key.startsWith(jobId)`, which can accidentally include rows from other jobs 
like `job10` when the current jobId is `job1` (lexicographic ordering places 
`job10|...` between `job1` and `job1|...`). Seek to `jobId + DELIMITER` and 
guard with `startsWith(jobId + DELIMITER)` to avoid cross-job 
contamination/flaky tests.



##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/diff/SnapDiffJobStore.java:
##########
@@ -185,13 +263,153 @@ public byte[] getOldList(long objectId) throws 
IOException {
     return get(oldListCf, objectIdKeyBuffer(objectId));
   }
 
-  public boolean hasNewListEntry(long objectId) throws IOException {
-    return getNewList(objectId) != null;
+  public String getJobId() {
+    return jobId;
+  }
+
+  /** Returns whether the stored new-list value is a present-marker 
(unchanged). */
+  public boolean isPresentMarker(byte[] value) {
+    return value != null && Arrays.equals(value, presentMarker);
+  }
+
+  /** Ascending merge-join iterator over {@code newList}. */
+  public ListIterator newListIterator() throws RocksDatabaseException {
+    return new ListIterator(newListCf);
+  }
+
+  /** Ascending merge-join iterator over {@code oldList}. */
+  public ListIterator oldListIterator() throws RocksDatabaseException {
+    return new ListIterator(oldListCf);
+  }
+
+  /** Builds a from-snapshot path resolver over the reverse edge index. */
+  SnapDiffPathResolver newFromPathResolver(long bucketObjectId) {
+    requireFso();
+    return new SnapDiffPathResolver(db, fromEdgesCf, bucketObjectId, 
pathResolverLruCapacity());
+  }
+
+  /** Builds a to-snapshot path resolver over the reverse edge index. */
+  SnapDiffPathResolver newToPathResolver(long bucketObjectId) {
+    requireFso();
+    return new SnapDiffPathResolver(db, toEdgesCf, bucketObjectId, 
pathResolverLruCapacity());
+  }
+
+  private int pathResolverLruCapacity() {
+    return (int) maxInMemoryEntries / 2;

Review Comment:
   Casting `maxInMemoryEntries` (long) to int before dividing can overflow for 
large configured values, producing a negative/incorrect LRU capacity. Clamp to 
`Integer.MAX_VALUE` (or perform division in long first, then safely cast) to 
ensure a valid capacity.



##########
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java:
##########
@@ -1399,97 +1403,118 @@ private String resolveBucketRelativePath(boolean 
isFSOBucket,
    * chain cannot be resolved in the live from-snapshot directory graph, so the
    * delete entry can be skipped from the final report.
    *
-   * @param objectIdToParentId from-snapshot directory parent graph keyed by 
stable
-   *     objectId; must not be the to-snapshot graph
-   * @param renamedDirectoryIds objectIds of directories renamed in the diff 
(from
-   *     snapshot objectIds, not destination paths)
+   * @param deletedDirectoryIds membership test for deleted directory object 
ids
+   * @param renamedDirectoryIds membership test for renamed directory object 
ids
+   *     (from snapshot objectIds, not destination paths)
+   * @param ancestorMemo LRU cache of ancestor object ids to deleted-ancestor 
results
    */
   @VisibleForTesting
-  boolean hasDeletedAncestor(long parentObjectId, Set<Long> 
deletedDirectoryIds,
-      Set<Long> renamedDirectoryIds, Map<Long, Long> objectIdToParentId,
-      long bucketObjectId, Map<Long, Boolean> ancestorMemo) {
-    Objects.requireNonNull(objectIdToParentId, "objectIdToParentId must not be 
null");
+  public boolean[] hasDeletedAncestors(List<Long> parentObjectIds,
+      DirectoryIdCheck deletedDirectoryIds, DirectoryIdCheck 
renamedDirectoryIds,
+      ParentIdBatchLookup parentLookup, long bucketObjectId,
+      Map<Long, Boolean> ancestorMemo) throws IOException {
+    Objects.requireNonNull(parentObjectIds, "parentObjectIds must not be 
null");
+    Objects.requireNonNull(deletedDirectoryIds, "deletedDirectoryIds must not 
be null");
+    Objects.requireNonNull(parentLookup, "parentLookup must not be null");
     Objects.requireNonNull(renamedDirectoryIds, "renamedDirectoryIds must not 
be null");
-
-    if (parentObjectId == bucketObjectId) {
-      return false;
-    }
-    Boolean cached = ancestorMemo.get(parentObjectId);
-    if (cached != null) {
-      return cached;
+    Objects.requireNonNull(ancestorMemo, "ancestorMemo must not be null");
+
+    int batchSize = parentObjectIds.size();
+    boolean[] hasDeletedAncestor = new boolean[batchSize];
+    long[] currentAncestors = new long[batchSize];
+    boolean[] resolved = new boolean[batchSize];
+    List<List<Long>> ancestorPaths = new ArrayList<>(batchSize);
+    for (int i = 0; i < batchSize; i++) {
+      ancestorPaths.add(new ArrayList<>());
+      currentAncestors[i] = parentObjectIds.get(i);
     }
 
-    List<Long> path = new ArrayList<>();
-    long current = parentObjectId;
-    boolean result;
     while (true) {
-      if (current == bucketObjectId) {
-        result = false;
-        break;
-      }
-      cached = ancestorMemo.get(current);
-      if (cached != null) {
-        result = cached;
-        break;
+      Map<Long, List<Integer>> frontier = new HashMap<>();
+      boolean hasUnresolved = false;
+      for (int i = 0; i < batchSize; i++) {
+        if (resolved[i]) {
+          continue;
+        }
+        hasUnresolved = true;
+        long current = currentAncestors[i];
+        if (current == bucketObjectId) {
+          resolved[i] = true;
+          continue;
+        }
+        Boolean cached = ancestorMemo.get(current);
+        if (cached != null) {
+          resolved[i] = true;
+          hasDeletedAncestor[i] = cached;
+          continue;
+        }
+        if (renamedDirectoryIds.contains(current)) {
+          resolved[i] = true;
+          ancestorMemo.put(current, false);
+          continue;
+        }
+        if (deletedDirectoryIds.contains(current)) {
+          resolved[i] = true;
+          ancestorMemo.put(current, true);
+          hasDeletedAncestor[i] = true;
+          continue;
+        }
+        frontier.computeIfAbsent(current, ignored -> new ArrayList<>()).add(i);
       }
-      if (renamedDirectoryIds.contains(current)) {
-        result = false;
-        ancestorMemo.put(current, false);
+
+      if (!hasUnresolved || frontier.isEmpty()) {
         break;
       }
-      if (deletedDirectoryIds.contains(current)) {
-        result = true;
-        ancestorMemo.put(current, true);
-        break;
+
+      List<Long> lookupObjectIds = new ArrayList<>(frontier.keySet());
+      List<Long> nextParents = parentLookup.getParentIds(lookupObjectIds);
+      Objects.requireNonNull(nextParents, "nextParents must not be null");
+      if (nextParents.size() != lookupObjectIds.size()) {
+        throw new IOException("Batch parent lookup returned " + 
nextParents.size()
+            + " values for " + lookupObjectIds.size() + " objectIds");
       }
-      Long nextParent = objectIdToParentId.get(current);
-      if (nextParent == null) {
-        result = true;
-        ancestorMemo.put(current, true);
-        break;
+      for (int i = 0; i < lookupObjectIds.size(); i++) {
+        long current = lookupObjectIds.get(i);
+        Long nextParent = nextParents.get(i);
+        List<Integer> positions = frontier.get(current);
+        if (nextParent == null) {
+          ancestorMemo.put(current, true);
+          for (int position : positions) {
+            resolved[position] = true;
+            hasDeletedAncestor[position] = true;
+          }
+          continue;
+        }
+        for (int position : positions) {
+          ancestorPaths.get(position).add(current);
+          currentAncestors[position] = nextParent;
+        }
       }
-      path.add(current);
-      current = nextParent;
     }
-    for (Long node : path) {
-      ancestorMemo.put(node, result);
+
+    for (int i = 0; i < batchSize; i++) {
+      // if hasDeletedAncestor == true only ??
+      for (Long ancestor : ancestorPaths.get(i)) {
+        ancestorMemo.put(ancestor, hasDeletedAncestor[i]);
+      }
     }

Review Comment:
   The inline comment `// if hasDeletedAncestor == true only ??` reads like an 
unresolved question and makes the intended behavior unclear. Please remove it 
or replace it with a clear explanation of why all visited ancestors are 
memoized with the final result for that position.



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