cshuo commented on code in PR #19805: URL: https://github.com/apache/hudi/pull/19805#discussion_r3903903144
########## hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/PartitionedRLIBootstrapOperator.java: ########## @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hudi.sink.bootstrap; + +import org.apache.hudi.client.common.HoodieFlinkEngineContext; +import org.apache.hudi.client.model.HoodieFlinkInternalRow; +import org.apache.hudi.common.data.HoodiePairData; +import org.apache.hudi.common.function.SerializableFunctionUnchecked; +import org.apache.hudi.common.model.FileSlice; +import org.apache.hudi.common.model.HoodieRecordGlobalLocation; +import org.apache.hudi.common.table.HoodieTableConfig; +import org.apache.hudi.common.table.HoodieTableMetaClient; +import org.apache.hudi.common.util.VisibleForTesting; +import org.apache.hudi.configuration.FlinkOptions; +import org.apache.hudi.metadata.HoodieBackedTableMetadata; +import org.apache.hudi.metadata.MetadataPartitionType; +import org.apache.hudi.util.StreamerUtil; +import org.apache.hudi.utils.RuntimeContextUtils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.runtime.state.StateInitializationContext; +import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Bootstrap operator that preload of time bounded partitioned record level index (RLI) data + * from the metadata table. + * + * <p>Only data table partitions that fall within the last {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} + * days are eagerly preloaded; the partition path of each partition is parsed as a date using + * {@link FlinkOptions#PARTITION_FORMAT} (default {@link FlinkOptions#PARTITION_FORMAT_DAY}) to + * determine whether it falls inside the window. Partitions outside the window, and partitions whose + * path cannot be parsed as a date, are skipped here and are expected to be loaded on demand later. + * + * <p>Setting {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} to {@code 0} disables preloading + * entirely, which is the expected fallback for non-temporal (non date-partitioned) tables. + */ +@Slf4j +public class PartitionedRLIBootstrapOperator Review Comment: `PartitionedRLIBootstrapOperator` sounds like a general bootstrap operator, while this implementation only preloads RLI data for a configured time window. Could we rename it to make the scope and behavior clearer, like `TimeBoundedRLIBootstrapOperator`? ########## hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/OptionsResolver.java: ########## @@ -262,6 +265,14 @@ public static boolean isGlobalRecordLevelIndex(Configuration conf) { return indexType == HoodieIndex.IndexType.GLOBAL_RECORD_LEVEL_INDEX; } + /** + * Returns whether the table uses partitioned record level index served by the local RocksDB-based + * partitioned index cache, i.e. {@link FlinkOptions#INDEX_RLI_BACKEND_TYPE} is configured as {@code rocksdb}. + */ + public static boolean isPartitionedRLIWithRocksDBBackend(Configuration conf) { Review Comment: Selecting the RocksDB backend does not necessarily mean this bootstrap is applicable, since it only supports time-partitioned tables with a configured preload window. Could we change `INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS` to default to `-1`, and enable the preload only when the user configures a positive value? This predicate could then be renamed to something like `isTimeBoundedRLIBootstrapEnabled` and require the partitioned RLI type and `BOOTSTRAP_DAYS > 0`. ########## hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/PartitionedRLIBootstrapOperator.java: ########## @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hudi.sink.bootstrap; + +import org.apache.hudi.client.common.HoodieFlinkEngineContext; +import org.apache.hudi.client.model.HoodieFlinkInternalRow; +import org.apache.hudi.common.data.HoodiePairData; +import org.apache.hudi.common.function.SerializableFunctionUnchecked; +import org.apache.hudi.common.model.FileSlice; +import org.apache.hudi.common.model.HoodieRecordGlobalLocation; +import org.apache.hudi.common.table.HoodieTableConfig; +import org.apache.hudi.common.table.HoodieTableMetaClient; +import org.apache.hudi.common.util.VisibleForTesting; +import org.apache.hudi.configuration.FlinkOptions; +import org.apache.hudi.metadata.HoodieBackedTableMetadata; +import org.apache.hudi.metadata.MetadataPartitionType; +import org.apache.hudi.util.StreamerUtil; +import org.apache.hudi.utils.RuntimeContextUtils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.runtime.state.StateInitializationContext; +import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Bootstrap operator that preload of time bounded partitioned record level index (RLI) data + * from the metadata table. + * + * <p>Only data table partitions that fall within the last {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} + * days are eagerly preloaded; the partition path of each partition is parsed as a date using + * {@link FlinkOptions#PARTITION_FORMAT} (default {@link FlinkOptions#PARTITION_FORMAT_DAY}) to + * determine whether it falls inside the window. Partitions outside the window, and partitions whose + * path cannot be parsed as a date, are skipped here and are expected to be loaded on demand later. + * + * <p>Setting {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} to {@code 0} disables preloading + * entirely, which is the expected fallback for non-temporal (non date-partitioned) tables. + */ +@Slf4j +public class PartitionedRLIBootstrapOperator + extends AbstractBootstrapOperator { + + private transient HoodieBackedTableMetadata tableMetadata; + private transient long loadedCnt; + + public PartitionedRLIBootstrapOperator(Configuration conf) { + super(conf); + } + + @Override + public void initializeState(StateInitializationContext context) throws Exception { + loadedCnt = 0; + int taskID = RuntimeContextUtils.getIndexOfThisSubtask(getRuntimeContext()); + + int bootstrapDays = conf.get(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS); + if (bootstrapDays <= 0) { + log.info("Skip preloading partitioned RLI records because bootstrap days is configured as {}, taskId = {}", + bootstrapDays, taskID); + waitForBootstrapReady(taskID); + return; + } + + HoodieTableMetaClient metaClient = StreamerUtil.createMetaClient(conf); + this.tableMetadata = createTableMetadata(metaClient); + + preLoadPartitionedRLIRecords(metaClient.getTableConfig(), bootstrapDays); + } + + @Override + public void close() throws Exception { + closeMetadataTable(); + super.close(); + } + + // ------------------------------------------------------------------------- + // Utilities + // ------------------------------------------------------------------------- + + @VisibleForTesting + HoodieBackedTableMetadata createTableMetadata(HoodieTableMetaClient metaClient) { + return new HoodieBackedTableMetadata( + HoodieFlinkEngineContext.DEFAULT, + metaClient.getStorage(), + StreamerUtil.metadataConfig(conf), + conf.get(FlinkOptions.PATH)); + } + + private void preLoadPartitionedRLIRecords(HoodieTableConfig tableConfig, int bootstrapDays) { + int taskID = RuntimeContextUtils.getIndexOfThisSubtask(getRuntimeContext()); + int parallelism = RuntimeContextUtils.getNumberOfParallelSubtasks(getRuntimeContext()); + + if (!tableMetadata.enabled()) { + if (tableConfig.isMetadataTableAvailable()) { + throw new RuntimeException("Can not initialize the table metadata"); + } + log.info("Skip preloading partitioned RLI records because table metadata is not initialized, taskId = {}", taskID); + waitForBootstrapReady(taskID); + closeMetadataTable(); + return; + } + + if (!tableConfig.isMetadataPartitionAvailable(MetadataPartitionType.RECORD_INDEX)) { + log.info("Skip preloading partitioned RLI records because record index is not available yet, taskId = {}", taskID); + waitForBootstrapReady(taskID); + closeMetadataTable(); + return; + } + + Map<String, List<FileSlice>> partitionedFileGroups = + tableMetadata.getBucketizedFileGroupsForPartitionedRLI(MetadataPartitionType.RECORD_INDEX); + List<String> partitionsInWindow = filterPartitionsInWindow(partitionedFileGroups.keySet(), bootstrapDays); + + log.info("Start preloading partitioned RLI records from metadata table for {}/{} partitions within the last {} days, " + + "taskId = {}, parallelism = {}", + partitionsInWindow.size(), partitionedFileGroups.size(), bootstrapDays, taskID, parallelism); + + long startTime = System.currentTimeMillis(); + for (String partitionPath : partitionsInWindow) { + preLoadPartition(partitionPath, partitionedFileGroups.get(partitionPath), taskID, parallelism); + } + long costMs = System.currentTimeMillis() - startTime; + log.info("Finish preloading partitioned RLI records, total records: {}, cost: {} ms, taskId = {}", loadedCnt, costMs, taskID); + + // Wait for other tasks to complete + waitForBootstrapReady(taskID); + + // Cleanup resources + closeMetadataTable(); + } + + private void preLoadPartition(String partitionPath, List<FileSlice> fileSlices, int taskID, int parallelism) { + List<FileSlice> filteredFileSlices = new ArrayList<>(); + for (int i = 0; i < fileSlices.size(); i++) { + if (shouldLoadBucket(i, parallelism, taskID)) { + filteredFileSlices.add(fileSlices.get(i)); + } + } + if (filteredFileSlices.isEmpty()) { + return; + } + log.info("Subtask: {} will preload partition {} from file groups: {}, total file groups: {}.", + taskID, partitionPath, filteredFileSlices.stream().map(FileSlice::getFileId).collect(Collectors.joining(",")), + fileSlices.size()); + + // readRecordIndexLocations() discovers the full set of RLI file slices internally and passes it to + // the filter; the filter here ignores that argument and substitutes the file slices already scoped + // to this data partition, mirroring RecordLevelIndexBackend#bootstrapPartition. + SerializableFunctionUnchecked<List<FileSlice>, List<FileSlice>> fileSlicesFilter = fileSlicesToFilter -> filteredFileSlices; + HoodiePairData<String, HoodieRecordGlobalLocation> rliData = tableMetadata.readRecordIndexLocations(fileSlicesFilter); + rliData.forEach(locationPair -> emitIndexRecord(partitionPath, locationPair.getLeft(), locationPair.getRight())); + } + + private void emitIndexRecord(String partitionPath, String recordKey, HoodieRecordGlobalLocation location) { + output.collect(new StreamRecord<>( Review Comment: Just a reminder that these index records are not currently handled by the downstream `DynamicBucketAssignFunction`, which treats them as normal data records. If the downstream handling is planned for a follow-up PR, please keep this preload disabled by default until the complete processing path is available. ########## hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/PartitionedRLIBootstrapOperator.java: ########## @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hudi.sink.bootstrap; + +import org.apache.hudi.client.common.HoodieFlinkEngineContext; +import org.apache.hudi.client.model.HoodieFlinkInternalRow; +import org.apache.hudi.common.data.HoodiePairData; +import org.apache.hudi.common.function.SerializableFunctionUnchecked; +import org.apache.hudi.common.model.FileSlice; +import org.apache.hudi.common.model.HoodieRecordGlobalLocation; +import org.apache.hudi.common.table.HoodieTableConfig; +import org.apache.hudi.common.table.HoodieTableMetaClient; +import org.apache.hudi.common.util.VisibleForTesting; +import org.apache.hudi.configuration.FlinkOptions; +import org.apache.hudi.metadata.HoodieBackedTableMetadata; +import org.apache.hudi.metadata.MetadataPartitionType; +import org.apache.hudi.util.StreamerUtil; +import org.apache.hudi.utils.RuntimeContextUtils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.runtime.state.StateInitializationContext; +import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Bootstrap operator that preload of time bounded partitioned record level index (RLI) data + * from the metadata table. + * + * <p>Only data table partitions that fall within the last {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} + * days are eagerly preloaded; the partition path of each partition is parsed as a date using + * {@link FlinkOptions#PARTITION_FORMAT} (default {@link FlinkOptions#PARTITION_FORMAT_DAY}) to + * determine whether it falls inside the window. Partitions outside the window, and partitions whose + * path cannot be parsed as a date, are skipped here and are expected to be loaded on demand later. + * + * <p>Setting {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} to {@code 0} disables preloading + * entirely, which is the expected fallback for non-temporal (non date-partitioned) tables. + */ +@Slf4j +public class PartitionedRLIBootstrapOperator + extends AbstractBootstrapOperator { + + private transient HoodieBackedTableMetadata tableMetadata; + private transient long loadedCnt; + + public PartitionedRLIBootstrapOperator(Configuration conf) { + super(conf); + } + + @Override + public void initializeState(StateInitializationContext context) throws Exception { + loadedCnt = 0; + int taskID = RuntimeContextUtils.getIndexOfThisSubtask(getRuntimeContext()); + + int bootstrapDays = conf.get(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS); + if (bootstrapDays <= 0) { + log.info("Skip preloading partitioned RLI records because bootstrap days is configured as {}, taskId = {}", + bootstrapDays, taskID); + waitForBootstrapReady(taskID); + return; + } + + HoodieTableMetaClient metaClient = StreamerUtil.createMetaClient(conf); + this.tableMetadata = createTableMetadata(metaClient); + + preLoadPartitionedRLIRecords(metaClient.getTableConfig(), bootstrapDays); + } + + @Override + public void close() throws Exception { + closeMetadataTable(); + super.close(); + } + + // ------------------------------------------------------------------------- + // Utilities + // ------------------------------------------------------------------------- + + @VisibleForTesting + HoodieBackedTableMetadata createTableMetadata(HoodieTableMetaClient metaClient) { + return new HoodieBackedTableMetadata( + HoodieFlinkEngineContext.DEFAULT, + metaClient.getStorage(), + StreamerUtil.metadataConfig(conf), + conf.get(FlinkOptions.PATH)); + } + + private void preLoadPartitionedRLIRecords(HoodieTableConfig tableConfig, int bootstrapDays) { + int taskID = RuntimeContextUtils.getIndexOfThisSubtask(getRuntimeContext()); + int parallelism = RuntimeContextUtils.getNumberOfParallelSubtasks(getRuntimeContext()); + + if (!tableMetadata.enabled()) { + if (tableConfig.isMetadataTableAvailable()) { + throw new RuntimeException("Can not initialize the table metadata"); + } + log.info("Skip preloading partitioned RLI records because table metadata is not initialized, taskId = {}", taskID); + waitForBootstrapReady(taskID); + closeMetadataTable(); + return; + } + + if (!tableConfig.isMetadataPartitionAvailable(MetadataPartitionType.RECORD_INDEX)) { + log.info("Skip preloading partitioned RLI records because record index is not available yet, taskId = {}", taskID); + waitForBootstrapReady(taskID); + closeMetadataTable(); + return; + } + + Map<String, List<FileSlice>> partitionedFileGroups = + tableMetadata.getBucketizedFileGroupsForPartitionedRLI(MetadataPartitionType.RECORD_INDEX); + List<String> partitionsInWindow = filterPartitionsInWindow(partitionedFileGroups.keySet(), bootstrapDays); + + log.info("Start preloading partitioned RLI records from metadata table for {}/{} partitions within the last {} days, " + + "taskId = {}, parallelism = {}", + partitionsInWindow.size(), partitionedFileGroups.size(), bootstrapDays, taskID, parallelism); + + long startTime = System.currentTimeMillis(); + for (String partitionPath : partitionsInWindow) { + preLoadPartition(partitionPath, partitionedFileGroups.get(partitionPath), taskID, parallelism); + } + long costMs = System.currentTimeMillis() - startTime; + log.info("Finish preloading partitioned RLI records, total records: {}, cost: {} ms, taskId = {}", loadedCnt, costMs, taskID); + + // Wait for other tasks to complete + waitForBootstrapReady(taskID); + + // Cleanup resources + closeMetadataTable(); + } + + private void preLoadPartition(String partitionPath, List<FileSlice> fileSlices, int taskID, int parallelism) { + List<FileSlice> filteredFileSlices = new ArrayList<>(); + for (int i = 0; i < fileSlices.size(); i++) { + if (shouldLoadBucket(i, parallelism, taskID)) { + filteredFileSlices.add(fileSlices.get(i)); + } + } + if (filteredFileSlices.isEmpty()) { + return; + } + log.info("Subtask: {} will preload partition {} from file groups: {}, total file groups: {}.", + taskID, partitionPath, filteredFileSlices.stream().map(FileSlice::getFileId).collect(Collectors.joining(",")), + fileSlices.size()); + + // readRecordIndexLocations() discovers the full set of RLI file slices internally and passes it to + // the filter; the filter here ignores that argument and substitutes the file slices already scoped + // to this data partition, mirroring RecordLevelIndexBackend#bootstrapPartition. + SerializableFunctionUnchecked<List<FileSlice>, List<FileSlice>> fileSlicesFilter = fileSlicesToFilter -> filteredFileSlices; + HoodiePairData<String, HoodieRecordGlobalLocation> rliData = tableMetadata.readRecordIndexLocations(fileSlicesFilter); + rliData.forEach(locationPair -> emitIndexRecord(partitionPath, locationPair.getLeft(), locationPair.getRight())); + } + + private void emitIndexRecord(String partitionPath, String recordKey, HoodieRecordGlobalLocation location) { + output.collect(new StreamRecord<>( + new HoodieFlinkInternalRow( + recordKey, + partitionPath, + location.getFileId(), + String.valueOf(location.getInstantTime())))); + loadedCnt += 1; + } + + /** + * Determines if the given file group should be loaded by this task. + * Uses round-robin assignment: file group i is assigned to task (i % parallelism). + */ + @VisibleForTesting + boolean shouldLoadBucket(int fileGroupIdx, int parallelism, int taskID) { + return fileGroupIdx % parallelism == taskID; + } + + /** + * Filters the data table partitions whose partition path can be parsed as a date within the last + * {@code bootstrapDays} days, inclusive of today. + */ + @VisibleForTesting + List<String> filterPartitionsInWindow(Iterable<String> partitionPaths, int bootstrapDays) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern( + conf.getOptional(FlinkOptions.PARTITION_FORMAT).orElse(FlinkOptions.PARTITION_FORMAT_DAY)); + boolean hiveStylePartitioning = conf.get(FlinkOptions.HIVE_STYLE_PARTITIONING); + LocalDate today = conf.get(FlinkOptions.WRITE_UTC_TIMEZONE) ? LocalDate.now(ZoneOffset.UTC) : LocalDate.now(); + LocalDate cutoff = today.minusDays(bootstrapDays); + + List<String> partitionsInWindow = new ArrayList<>(); + for (String partitionPath : partitionPaths) { + LocalDate partitionDate = parsePartitionDate(partitionPath, formatter, hiveStylePartitioning); + if (partitionDate != null && partitionDate.isAfter(cutoff) && !partitionDate.isAfter(today)) { + partitionsInWindow.add(partitionPath); + } + } + return partitionsInWindow; + } + + private LocalDate parsePartitionDate(String partitionPath, DateTimeFormatter formatter, boolean hiveStylePartitioning) { Review Comment: Can you check if there is any existing utility we can reuse to parse dates from partition paths? ########## hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/PartitionedRLIBootstrapOperator.java: ########## @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hudi.sink.bootstrap; + +import org.apache.hudi.client.common.HoodieFlinkEngineContext; +import org.apache.hudi.client.model.HoodieFlinkInternalRow; +import org.apache.hudi.common.data.HoodiePairData; +import org.apache.hudi.common.function.SerializableFunctionUnchecked; +import org.apache.hudi.common.model.FileSlice; +import org.apache.hudi.common.model.HoodieRecordGlobalLocation; +import org.apache.hudi.common.table.HoodieTableConfig; +import org.apache.hudi.common.table.HoodieTableMetaClient; +import org.apache.hudi.common.util.VisibleForTesting; +import org.apache.hudi.configuration.FlinkOptions; +import org.apache.hudi.metadata.HoodieBackedTableMetadata; +import org.apache.hudi.metadata.MetadataPartitionType; +import org.apache.hudi.util.StreamerUtil; +import org.apache.hudi.utils.RuntimeContextUtils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.runtime.state.StateInitializationContext; +import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Bootstrap operator that preload of time bounded partitioned record level index (RLI) data + * from the metadata table. + * + * <p>Only data table partitions that fall within the last {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} + * days are eagerly preloaded; the partition path of each partition is parsed as a date using + * {@link FlinkOptions#PARTITION_FORMAT} (default {@link FlinkOptions#PARTITION_FORMAT_DAY}) to + * determine whether it falls inside the window. Partitions outside the window, and partitions whose + * path cannot be parsed as a date, are skipped here and are expected to be loaded on demand later. + * + * <p>Setting {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} to {@code 0} disables preloading + * entirely, which is the expected fallback for non-temporal (non date-partitioned) tables. + */ +@Slf4j +public class PartitionedRLIBootstrapOperator + extends AbstractBootstrapOperator { + + private transient HoodieBackedTableMetadata tableMetadata; + private transient long loadedCnt; + + public PartitionedRLIBootstrapOperator(Configuration conf) { + super(conf); + } + + @Override + public void initializeState(StateInitializationContext context) throws Exception { + loadedCnt = 0; + int taskID = RuntimeContextUtils.getIndexOfThisSubtask(getRuntimeContext()); + + int bootstrapDays = conf.get(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS); + if (bootstrapDays <= 0) { + log.info("Skip preloading partitioned RLI records because bootstrap days is configured as {}, taskId = {}", + bootstrapDays, taskID); + waitForBootstrapReady(taskID); + return; + } + + HoodieTableMetaClient metaClient = StreamerUtil.createMetaClient(conf); + this.tableMetadata = createTableMetadata(metaClient); + + preLoadPartitionedRLIRecords(metaClient.getTableConfig(), bootstrapDays); + } + + @Override + public void close() throws Exception { + closeMetadataTable(); + super.close(); + } + + // ------------------------------------------------------------------------- + // Utilities + // ------------------------------------------------------------------------- + + @VisibleForTesting + HoodieBackedTableMetadata createTableMetadata(HoodieTableMetaClient metaClient) { + return new HoodieBackedTableMetadata( + HoodieFlinkEngineContext.DEFAULT, + metaClient.getStorage(), + StreamerUtil.metadataConfig(conf), + conf.get(FlinkOptions.PATH)); + } + + private void preLoadPartitionedRLIRecords(HoodieTableConfig tableConfig, int bootstrapDays) { + int taskID = RuntimeContextUtils.getIndexOfThisSubtask(getRuntimeContext()); + int parallelism = RuntimeContextUtils.getNumberOfParallelSubtasks(getRuntimeContext()); + + if (!tableMetadata.enabled()) { + if (tableConfig.isMetadataTableAvailable()) { + throw new RuntimeException("Can not initialize the table metadata"); + } + log.info("Skip preloading partitioned RLI records because table metadata is not initialized, taskId = {}", taskID); + waitForBootstrapReady(taskID); + closeMetadataTable(); + return; + } + + if (!tableConfig.isMetadataPartitionAvailable(MetadataPartitionType.RECORD_INDEX)) { + log.info("Skip preloading partitioned RLI records because record index is not available yet, taskId = {}", taskID); + waitForBootstrapReady(taskID); + closeMetadataTable(); + return; + } + + Map<String, List<FileSlice>> partitionedFileGroups = + tableMetadata.getBucketizedFileGroupsForPartitionedRLI(MetadataPartitionType.RECORD_INDEX); + List<String> partitionsInWindow = filterPartitionsInWindow(partitionedFileGroups.keySet(), bootstrapDays); + + log.info("Start preloading partitioned RLI records from metadata table for {}/{} partitions within the last {} days, " + + "taskId = {}, parallelism = {}", + partitionsInWindow.size(), partitionedFileGroups.size(), bootstrapDays, taskID, parallelism); + + long startTime = System.currentTimeMillis(); + for (String partitionPath : partitionsInWindow) { + preLoadPartition(partitionPath, partitionedFileGroups.get(partitionPath), taskID, parallelism); + } + long costMs = System.currentTimeMillis() - startTime; + log.info("Finish preloading partitioned RLI records, total records: {}, cost: {} ms, taskId = {}", loadedCnt, costMs, taskID); + + // Wait for other tasks to complete + waitForBootstrapReady(taskID); + + // Cleanup resources + closeMetadataTable(); + } + + private void preLoadPartition(String partitionPath, List<FileSlice> fileSlices, int taskID, int parallelism) { + List<FileSlice> filteredFileSlices = new ArrayList<>(); + for (int i = 0; i < fileSlices.size(); i++) { + if (shouldLoadBucket(i, parallelism, taskID)) { + filteredFileSlices.add(fileSlices.get(i)); + } + } + if (filteredFileSlices.isEmpty()) { + return; + } + log.info("Subtask: {} will preload partition {} from file groups: {}, total file groups: {}.", + taskID, partitionPath, filteredFileSlices.stream().map(FileSlice::getFileId).collect(Collectors.joining(",")), + fileSlices.size()); + + // readRecordIndexLocations() discovers the full set of RLI file slices internally and passes it to + // the filter; the filter here ignores that argument and substitutes the file slices already scoped + // to this data partition, mirroring RecordLevelIndexBackend#bootstrapPartition. + SerializableFunctionUnchecked<List<FileSlice>, List<FileSlice>> fileSlicesFilter = fileSlicesToFilter -> filteredFileSlices; + HoodiePairData<String, HoodieRecordGlobalLocation> rliData = tableMetadata.readRecordIndexLocations(fileSlicesFilter); + rliData.forEach(locationPair -> emitIndexRecord(partitionPath, locationPair.getLeft(), locationPair.getRight())); + } + + private void emitIndexRecord(String partitionPath, String recordKey, HoodieRecordGlobalLocation location) { + output.collect(new StreamRecord<>( + new HoodieFlinkInternalRow( + recordKey, + partitionPath, + location.getFileId(), + String.valueOf(location.getInstantTime())))); + loadedCnt += 1; + } + + /** + * Determines if the given file group should be loaded by this task. + * Uses round-robin assignment: file group i is assigned to task (i % parallelism). + */ + @VisibleForTesting + boolean shouldLoadBucket(int fileGroupIdx, int parallelism, int taskID) { Review Comment: This may cause a hotspot. Since `fileGroupIdx` restarts from zero for each partition, for e.g., if each partition has 4 RLI shards, all shards will always be loaded by subtasks 0–3, leaving the remaining subtasks idle. The distribution should also account for the partition path; you can refer to the shuffle strategy used by the bucket index partitioner via `BucketIndexUtil#getPartitionIndexFunc`. -- 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]
