voonhous commented on code in PR #19869:
URL: https://github.com/apache/hudi/pull/19869#discussion_r3976033506


##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/index/BaseIndexer.java:
##########
@@ -71,4 +75,15 @@ public void postInitialization(HoodieTableMetaClient 
metadataMetaClient, HoodieD
   public List<IndexPartitionAndRecords> buildRestore(IndexRestoreContext 
context) {
     return Collections.emptyList();
   }
+
+  /**
+   * Fails when a clustering commit reaches a table without record keys. The 
record index and the secondary index
+   * key the rows of such a table by file path and row position, and 
clustering rewrites the rows into new files,
+   * so neither index could follow them. Every other operation type either 
keeps the keys or registers new files.
+   */
+  protected void checkClusteringKeepsRecordKeys(HoodieCommitMetadata 
commitMetadata) {

Review Comment:
   **blocker:** This guard runs only on the batch metadata path, and a Spark 
`CLUSTER` commit bypasses that path under the default 
`hoodie.metadata.streaming.write.enabled=true`, so it never fires. The replaced 
groups' positional entries are then never deleted either, so after clustering 
SI/RLI resolve to a dead fileId and a query on the indexed column returns zero 
rows. `TestExternalFileRecordAndSecondaryIndex:227` pins streaming off and 
replays the commit instead of clustering, so it cannot catch this.
   
   Our earlier ask said "both indexers", which is where this landed, so the 
placement is on us. Could the check move to a point both paths share, e.g. 
`HoodieBackedTableMetadataWriter` ahead of the streaming/batch fork, with the 
functional case running a real clustering under the Spark default?
   
   <details>
   <summary>Chain (by reading, not executed end to end)</summary>
   
   - `HoodieMetadataConfig:1445` defaults streaming writes on for Spark, table 
version >= 8
   - `SparkRDDTableServiceClient.writeToMetadataTable:106` routes to the 
streaming handler on that flag alone
   - `HoodieBackedTableMetadataWriter.completeStreamingCommit:990` runs 
`buildUpdate` only for `getNonStreamingMetadataPartitionsToUpdate()`; RLI and 
SI are the streaming partitions (`:872`, `:915`)
   - `BaseRecordIndexer.getRecordIndexAdditionalUpserts` (batch only) is the 
only place replaced groups' entries are deleted
   - `RecordIndexMapper:78` still passes the raw config encoding rather than 
`getFileIdEncoding`
   - Read side: `RecordLevelIndexSupport.filterCandidateFiles` matches on live 
slice fileIds, so a stale fileId yields an empty candidate set and 
`HoodieFileIndex` prunes every slice
   </details>



##########
hudi-common/src/main/java/org/apache/hudi/common/util/ExternalFilePathUtil.java:
##########
@@ -143,7 +175,12 @@ private static String getOriginalFileName(String fileName) 
{
    */
   public static StoragePath getFullPathOfPartition(StoragePath parent, String 
fileName) {
     return getExternalFileGroupPrefix(fileName)
-        .map(prefix -> new StoragePath(parent.toString().substring(0, 
parent.toString().length() - prefix.length() - 1)))
+        .map(prefix -> {
+          String parentPath = parent.toString();
+          checkArgument(parentPath.endsWith(StoragePath.SEPARATOR + prefix),
+              "External file " + fileName + " carries the file group prefix " 
+ prefix + " but its parent " + parentPath + " does not end with it");

Review Comment:
   **minor:** Not blocking. This `checkArgument` builds its message eagerly on 
every call, and `getFullPathOfPartition` sits on the per-base-file view-build 
path (`HoodieBaseFile:86`, `AbstractTableFileSystemView:180`), so every 
prefixed external file pays the concatenation on each view build. Could we use 
the `Supplier<String>` overload (`ValidationUtils:49`) so the message is only 
built when the check fails?
   
   ```suggestion
             checkArgument(parentPath.endsWith(StoragePath.SEPARATOR + prefix),
                 () -> "External file " + fileName + " carries the file group 
prefix " + prefix + " but its parent " + parentPath + " does not end with it");
   ```



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/SecondaryIndexRecordGenerationUtils.java:
##########
@@ -245,35 +326,32 @@ public static <T> HoodieData<HoodieRecord> 
readSecondaryKeysFromFileSlices(Hoodi
       return engineContext.emptyHoodieData();
     }
     final int parallelism = Math.min(fileSlices.size(), 
secondaryIndexMaxParallelism);
-    final StoragePath basePath = metaClient.getBasePath();
-    HoodieSchema tableSchema;
-    try {
-      tableSchema = new TableSchemaResolver(metaClient).getTableSchema();
-    } catch (Exception e) {
-      throw new HoodieException("Failed to get latest schema for " + 
metaClient.getBasePath(), e);
-    }
     ReaderContextFactory<T> readerContextFactory = 
engineContext.getReaderContextFactory(metaClient);
     engineContext.setJobStatus(activeModule, "Secondary Index: reading 
secondary keys from " + fileSlices.size() + " file slices");
     HoodieFileFormat baseFileFormat = 
metaClient.getTableConfig().getBaseFileFormat();
     return engineContext.parallelize(fileSlices, 
parallelism).flatMap(partitionAndBaseFile -> {
-      final String partition = partitionAndBaseFile.getPartitionPath();
       final FileSlice fileSlice = partitionAndBaseFile.getFileSlice();
-      Option<StoragePath> dataFilePath = 
Option.ofNullable(fileSlice.getBaseFile().map(baseFile -> 
FSUtils.getAbsoluteFilePath(basePath, partition, 
baseFile.getFileName())).orElseGet(null));
-      HoodieSchema readerSchema;
-      if (dataFilePath.isPresent()) {
-        readerSchema = HoodieIOFactory.getIOFactory(metaClient.getStorage())
-            .getFileFormatUtils(baseFileFormat)
-            .readSchema(metaClient.getStorage(), dataFilePath.get());
-      } else {
-        readerSchema = tableSchema;
-      }
+      // the storage path keeps the directory prefix of a file written outside 
Hudi, which its file name alone loses
+      Option<StoragePath> dataFilePath = 
fileSlice.getBaseFile().map(HoodieBaseFile::getStoragePath);
+      // a file slice without a base file has only log files, whose schema is 
the table schema
+      HoodieSchema readerSchema = dataFilePath.isPresent()
+          ? 
HoodieIOFactory.getIOFactory(metaClient.getStorage()).getFileFormatUtils(baseFileFormat).readSchema(metaClient.getStorage(),
 dataFilePath.get())
+          : resolveTableSchema(metaClient);

Review Comment:
   **minor:** Not blocking. Resolving the schema inside the lambda answers the 
laziness ask, but it now runs `new 
TableSchemaResolver(metaClient).getTableSchema()` once per log-only file slice 
on the executors, where b10d8167 ran it once on the driver; a MOR table with 
many base-file-less slices pays that many timeline loads. Could we resolve it 
once on the driver, guarded by `fileSlices.stream().anyMatch(s -> 
!s.getFileSlice().getBaseFile().isPresent())`, and capture the result in the 
closure?



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

Reply via email to