Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/generated/flink_connector_configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@
<td><p>Enum</p></td>
<td>The mode used by StaticFileStoreSplitEnumerator to assign splits.<br /><br />Possible values:<ul><li>"fair": Distribute splits evenly when batch reading to prevent a few tasks from reading all.</li><li>"preemptive": Distribute splits preemptively according to the consumption speed of the task.</li></ul></td>
</tr>
<tr>
<td><h5>scan.split-enumerator.weight-mode</h5></td>
<td style="word-wrap: break-word;">row-count</td>
<td><p>Enum</p></td>
<td>The weight metric used by StaticFileStoreSplitEnumerator. 'row-count' balances by split row count. 'file-size' only works with 'scan.split-enumerator.mode' = 'fair', balances by total data file size for DataSplit, and falls back to row count otherwise.<br /><br />Possible values:<ul><li>"row-count": Balance splits by row count.</li><li>"file-size": Balance splits by total data file size for DataSplit and fall back to row count otherwise. Only works with fair assign mode.</li></ul></td>
</tr>
<tr>
<td><h5>scan.watermark.alignment.group</h5></td>
<td style="word-wrap: break-word;">(none)</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ public static <T> List<List<T>> packForOrdered(
return packed;
}

/** A bin packing implementation for fixed bin number. */
/** A bin packing implementation for fixed bin number using longest processing time first. */
public static <T> List<List<T>> packForFixedBinNumber(
Iterable<T> items, Function<T, Long> weightFunc, int binNumber) {
// 1. sort items first
List<T> sorted = new ArrayList<>();
items.forEach(sorted::add);
sorted.sort(comparingLong(weightFunc::apply));
sorted.sort(comparingLong(weightFunc::apply).reversed());

// 2. packing
PriorityQueue<FixedNumberBin<T>> bins = new PriorityQueue<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -33,9 +35,19 @@ public void testPackForFixedBinNumber() {
List<List<Integer>> pack =
BinPacking.packForFixedBinNumber(
Arrays.asList(1, 5, 1, 2, 3, 6, 2), Integer::longValue, 3);
assertThat(pack)
.containsExactlyInAnyOrder(
Arrays.asList(1, 3), Arrays.asList(2, 5), Arrays.asList(1, 2, 6));
assertThat(pack.stream().mapToInt(bin -> bin.stream().mapToInt(i -> i).sum()))
.containsExactlyInAnyOrder(6, 7, 7);
}

@Test
public void testPackForFixedBinNumberAssignsLargestItemsFirst() {
List<Integer> items = new ArrayList<>(Collections.nCopies(100, 1));
items.add(100);

List<List<Integer>> pack = BinPacking.packForFixedBinNumber(items, Integer::longValue, 2);

assertThat(pack.stream().mapToInt(bin -> bin.stream().mapToInt(i -> i).sum()))
.containsExactlyInAnyOrder(100, 100);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ public class FlinkConnectorOptions {
.withDescription(
"The mode used by StaticFileStoreSplitEnumerator to assign splits.");

public static final ConfigOption<SplitWeightMode> SCAN_SPLIT_ENUMERATOR_WEIGHT_MODE =
key("scan.split-enumerator.weight-mode")
.enumType(SplitWeightMode.class)
.defaultValue(SplitWeightMode.ROW_COUNT)
.withDescription(
"The weight metric used by StaticFileStoreSplitEnumerator. "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Apply the weight mode to bounded system-table sources

This option is currently consumed only by FlinkSourceBuilder. SystemTableSource also constructs StaticFileStoreSource for bounded system-table reads and already propagates the batch size and assign mode, but it calls the overload that leaves the weight function null. A query such as a bounded $ro read with scan.split-enumerator.weight-mode=file-size therefore silently continues balancing by row count, and file-size with preemptive is accepted without the documented validation. Please share the parsing/validation and weight function with SystemTableSource (including copy()), or explicitly reject/document the unsupported path, and add a bounded system-table regression test.

+ "'row-count' balances by split row count. "
+ "'file-size' only works with 'scan.split-enumerator.mode' = 'fair', "
+ "balances by total data file size for DataSplit, and falls back to row count otherwise.");

/* Sink writer allocate segments from managed memory. */
public static final ConfigOption<Boolean> SINK_USE_MANAGED_MEMORY =
ConfigOptions.key("sink.use-managed-memory-allocator")
Expand Down Expand Up @@ -682,6 +692,34 @@ public InlineElement getDescription() {
}
}

/**
* Split weight mode for {@link org.apache.paimon.flink.source.StaticFileStoreSplitEnumerator}.
*/
public enum SplitWeightMode implements DescribedEnum {
ROW_COUNT("row-count", "Balance splits by row count."),
FILE_SIZE(
"file-size",
"Balance splits by total data file size for DataSplit and fall back to row count otherwise. Only works with fair assign mode.");

private final String value;
private final String description;

SplitWeightMode(String value, String description) {
this.value = value;
this.description = description;
}

@Override
public String toString() {
return value;
}

@Override
public InlineElement getDescription() {
return text(description);
}
}

/**
* Split assign mode for {@link org.apache.paimon.flink.source.StaticFileStoreSplitEnumerator}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.PostponeMergePlan;
import org.apache.paimon.table.source.PostponeMergeReadBuilder;
import org.apache.paimon.table.source.QueryAuthSplit;
import org.apache.paimon.table.source.ReadBuilder;
import org.apache.paimon.table.source.Split;
import org.apache.paimon.utils.SerializableFunction;
import org.apache.paimon.utils.StringUtils;

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
Expand Down Expand Up @@ -219,6 +222,7 @@ private ReadBuilder createReadBuilder(@Nullable org.apache.paimon.types.RowType

private DataStream<RowData> buildStaticFileSource() {
Options options = Options.fromMap(table.options());
validateSplitWeightMode(options);
return toDataStream(
new StaticFileStoreSource(
createReadBuilder(projectedRowType()),
Expand All @@ -227,10 +231,58 @@ private DataStream<RowData> buildStaticFileSource() {
options.get(FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_ASSIGN_MODE),
dynamicPartitionFilteringInfo,
outerProject(),
splitWeightFunc(options),
null,
options.get(CoreOptions.BLOB_AS_DESCRIPTOR),
skipPreloadTargetSnapshot));
}

private static SerializableFunction<FileStoreSourceSplit, Long> splitWeightFunc(
Options options) {
if (isFileSizeWeightMode(options)) {
return FlinkSourceBuilder::splitFileSizeOrRowCount;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Please assign the largest byte-weighted splits first. The supplied weight is consumed by BinPacking.packForFixedBinNumber, which sorts items in ascending order before placing each item in the lightest bin. With two readers, 100 splits of weight 1, and one split of weight 100, the current algorithm produces loads of 50 and 150, while largest-first placement produces 100 and 100. A large file plus many small files is a realistic case for this option, so the new mode can preserve the long-tail skew it is intended to remove. Please use descending/LPT order for this path (or fix the shared packer if compatible) and add this distribution as a regression test.

}
switch (options.get(FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_WEIGHT_MODE)) {
case ROW_COUNT:
return split -> split.split().rowCount();
default:
throw new UnsupportedOperationException(
"Unsupported split weight mode "
+ options.get(
FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_WEIGHT_MODE));
}
}

private static void validateSplitWeightMode(Options options) {
checkArgument(
!isFileSizeWeightMode(options)
|| options.get(FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_ASSIGN_MODE)
== FlinkConnectorOptions.SplitAssignMode.FAIR,
"'%s' = '%s' only works with '%s' = '%s'.",
FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_WEIGHT_MODE.key(),
FlinkConnectorOptions.SplitWeightMode.FILE_SIZE,
FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_ASSIGN_MODE.key(),
FlinkConnectorOptions.SplitAssignMode.FAIR);
}

private static boolean isFileSizeWeightMode(Options options) {
return options.get(FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_WEIGHT_MODE)
== FlinkConnectorOptions.SplitWeightMode.FILE_SIZE;
}

@VisibleForTesting
static long splitFileSizeOrRowCount(FileStoreSourceSplit sourceSplit) {
Split split = sourceSplit.split();
while (split instanceof QueryAuthSplit) {
split = ((QueryAuthSplit) split).split();
}
if (split instanceof DataSplit) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Please preserve file-size weighting through QueryAuthSplit. When query-auth.enabled is true and REST authorization returns a row filter or column mask, TableQueryAuthResult.convertPlan wraps each underlying DataSplit in QueryAuthSplit. This outer-type check then falls back to rowCount, so authenticated bounded reads silently ignore file-size mode even though QueryAuthSplit exposes the wrapped split. Please unwrap transparent QueryAuthSplit layers before checking for DataSplit and add a wrapped-split test.

return ((DataSplit) split)
.dataFiles().stream().mapToLong(file -> file.fileSize()).sum();
}
return split.rowCount();
}

private @Nullable DataStream<RowData> buildPostponeMergeSource() {
FileStoreTable fileStoreTable = (FileStoreTable) table;
if (fileStoreTable.coreOptions().startupMode() == CoreOptions.StartupMode.COMPACTED_FULL) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

import org.apache.flink.api.connector.source.SplitEnumeratorContext;
import org.apache.flink.table.connector.source.DynamicFilteringData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;

Expand All @@ -52,6 +54,8 @@
*/
public class PreAssignSplitAssigner implements SplitAssigner {

private static final Logger LOG = LoggerFactory.getLogger(PreAssignSplitAssigner.class);

/** Default batch splits size to avoid exceed `akka.framesize`. */
private final int splitBatchSize;

Expand Down Expand Up @@ -145,9 +149,42 @@ public PreAssignSplitAssigner(
this.groupFunc = groupFunc;
this.pendingSplitAssignment =
createBatchFairSplitAssignment(splits, parallelism, this.weightFunc, groupFunc);
logSplitAssignmentSummary(
this.pendingSplitAssignment, parallelism, splits.size(), this.weightFunc);
this.numberOfPendingSplits = new AtomicInteger(splits.size());
}

private static void logSplitAssignmentSummary(
Map<Integer, LinkedList<FileStoreSourceSplit>> assignment,
int parallelism,
int totalSplits,
SerializableFunction<FileStoreSourceSplit, Long> weightFunc) {
if (!LOG.isInfoEnabled()) {
return;
}

long totalWeight = 0L;
List<Integer> splitCounts = new ArrayList<>(parallelism);
List<Long> assignedWeights = new ArrayList<>(parallelism);
for (int i = 0; i < parallelism; i++) {
Collection<FileStoreSourceSplit> assignedSplits =
assignment.getOrDefault(i, new LinkedList<>());
long assignedWeight = assignedSplits.stream().mapToLong(weightFunc::apply).sum();
splitCounts.add(assignedSplits.size());
assignedWeights.add(assignedWeight);
totalWeight += assignedWeight;
}

LOG.info(
"Created FAIR split assignment summary: parallelism={}, totalSplits={}, "
+ "totalWeight={}, splitCountsPerSubtask={}, assignedWeightsPerSubtask={}",
parallelism,
totalSplits,
totalWeight,
splitCounts,
assignedWeights);
}

@Override
public List<FileStoreSourceSplit> getNext(int subtask, @Nullable String hostname) {
// The following batch assignment operation is for two purposes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
package org.apache.paimon.flink;

import org.apache.paimon.CoreOptions;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.flink.sink.FixedBucketSink;
import org.apache.paimon.flink.sink.FlinkSinkBuilder;
import org.apache.paimon.flink.source.ContinuousFileStoreSource;
Expand All @@ -32,11 +34,14 @@
import org.apache.paimon.schema.SchemaManager;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.FileStoreTableFactory;
import org.apache.paimon.table.sink.BatchTableCommit;
import org.apache.paimon.table.sink.BatchTableWrite;
import org.apache.paimon.utils.BlockingIterator;
import org.apache.paimon.utils.FailingFileIO;

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.connector.source.Boundedness;
import org.apache.flink.api.dag.Transformation;
import org.apache.flink.streaming.api.datastream.DataStream;
Expand Down Expand Up @@ -226,6 +231,48 @@ public void testNonPartitioned() throws Exception {
assertThat(results).containsExactlyInAnyOrder(expected);
}

@TestTemplate
public void testFileSizeSplitWeightModeForBoundedSource() throws Exception {
assumeTrue(isBatch);

FileStoreTable table = buildFileStoreTable(new int[0], new int[0]);
// Use equal row counts with skewed payload sizes to verify byte-aware assignment.
writeSingleRecordFile(table, 1, repeat("a", 8), 1);
writeSingleRecordFile(table, 2, repeat("b", 8), 2);
writeSingleRecordFile(table, 3, repeat("c", 32 * 1024), 3);
writeSingleRecordFile(table, 4, repeat("d", 32 * 1024), 4);

Map<String, String> options = new HashMap<>();
options.put(CoreOptions.SOURCE_SPLIT_TARGET_SIZE.key(), "1 B");
options.put(CoreOptions.SOURCE_SPLIT_OPEN_FILE_COST.key(), "1 B");
options.put(FlinkConnectorOptions.SCAN_PARALLELISM.key(), "2");
options.put(
FlinkConnectorOptions.SCAN_SPLIT_ENUMERATOR_WEIGHT_MODE.key(),
FlinkConnectorOptions.SplitWeightMode.FILE_SIZE.toString());
table = table.copy(options);

List<Row> results =
executeAndCollectRow(
new FlinkSourceBuilder(table)
.sourceBounded(true)
.env(env)
.build()
.map(new SubtaskAndPayloadSize())
.setParallelism(2));

Map<Integer, Integer> largePayloadSubtasks = new HashMap<>();
for (Row row : results) {
int subtask = (int) row.getField(0);
int payloadSize = (int) row.getField(2);
if (payloadSize > 1024) {
largePayloadSubtasks.put((int) row.getField(1), subtask);
}
}

assertThat(largePayloadSubtasks).hasSize(2);
assertThat(largePayloadSubtasks.values()).containsExactlyInAnyOrder(0, 1);
}

@TestTemplate
public void testOverwrite() throws Exception {
assumeTrue(isBatch);
Expand Down Expand Up @@ -462,6 +509,32 @@ private void sinkAndValidate(
assertThat(iterator.collect(expected.length)).containsExactlyInAnyOrder(expected);
}

private static void writeSingleRecordFile(FileStoreTable table, int v, String p, int k)
throws Exception {
try (BatchTableWrite write = table.newBatchWriteBuilder().newWrite();
BatchTableCommit commit = table.newBatchWriteBuilder().newCommit()) {
write.write(GenericRow.of(v, BinaryString.fromString(p), k));
commit.commit(write.prepareCommit());
}
}

private static String repeat(String value, int count) {
char[] chars = new char[count];
Arrays.fill(chars, value.charAt(0));
return new String(chars);
}

private static class SubtaskAndPayloadSize extends RichMapFunction<RowData, Row> {

@Override
public Row map(RowData value) {
return Row.of(
getRuntimeContext().getTaskInfo().getIndexOfThisSubtask(),
value.getInt(0),
value.getString(1).toString().length());
}
}

public FileStoreTable buildFileStoreTable(int[] partitions, int[] primaryKey) throws Exception {
return buildFileStoreTable(isBatch, getTempDirPath(), partitions, primaryKey);
}
Expand Down
Loading
Loading