Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,42 @@ public class FlinkConnectorOptions {
+ "The format is 'timestamp' or 'yyyy-MM-dd HH:mm:ss'. "
+ "Like '1678883047356' or '2023-12-09 23:09:12'.");

public static final ConfigOption<ScanBoundedMode> SCAN_BOUNDED_MODE =
ConfigOptions.key("scan.bounded.mode")
.enumType(ScanBoundedMode.class)
.defaultValue(ScanBoundedMode.UNBOUNDED)
.withDescription(
String.format(
"Bounded mode for the Fluss source. Default is '%s'. In batch "
+ "execution mode, '%s' behaves the same as '%s': the "
+ "source reads up to the latest log offsets captured "
+ "at startup. In streaming execution mode, a bounded "
+ "mode other than '%s' makes the source stop at the "
+ "given stopping offsets and then the job finishes "
+ "(a bounded streaming read). Bounded modes other "
+ "than '%s' are supported for log tables and the "
+ "changelog of primary key tables (earliest/latest/"
+ "timestamp startup mode), but not for the full "
+ "startup mode of primary key tables or the datalake "
+ "union read.",
ScanBoundedMode.UNBOUNDED.value,
ScanBoundedMode.UNBOUNDED.value,
ScanBoundedMode.LATEST_OFFSET.value,
ScanBoundedMode.UNBOUNDED.value,
ScanBoundedMode.UNBOUNDED.value));

public static final ConfigOption<String> SCAN_BOUNDED_TIMESTAMP =
ConfigOptions.key("scan.bounded.timestamp")
.stringType()
.noDefaultValue()
.withDescription(
"Optional timestamp for Fluss source in case of bounded mode is timestamp. "
+ "The source stops before the first record batch whose commit "
+ "timestamp is greater than or equal to the given timestamp, i.e. "
+ "only records with a commit timestamp smaller than the given "
+ "timestamp are read. The format is 'timestamp' or "
+ "'yyyy-MM-dd HH:mm:ss'. Like '1678883047356' or '2023-12-09 23:09:12'.");

public static final ConfigOption<Duration> SCAN_PARTITION_DISCOVERY_INTERVAL =
ConfigOptions.key("scan.partition.discovery.interval")
.durationType()
Expand Down Expand Up @@ -333,4 +369,41 @@ public InlineElement getDescription() {
return description;
}
}

/** Bounded mode for the fluss scanner, see {@link #SCAN_BOUNDED_MODE}. */
public enum ScanBoundedMode implements DescribedEnum {
UNBOUNDED(
"unbounded",
text(
"In streaming execution mode, the source never stops. In batch execution "
+ "mode, the source reads up to the latest log offsets captured "
+ "at startup.")),
LATEST_OFFSET(
"latest-offset",
text("Bounded by the latest log offsets captured when the source starts.")),
TIMESTAMP(
"timestamp",
text(
"Bounded by a user-supplied timestamp. The source stops before the first "
+ "record batch whose commit timestamp is greater than or equal "
+ "to the given timestamp."));

private final String value;
private final InlineElement description;

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

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

@Override
public InlineElement getDescription() {
return description;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ public DynamicTableSource createDynamicTableSource(Context context) {
context.getConfiguration().get(TableConfigOptions.LOCAL_TIME_ZONE));
final FlinkConnectorOptionsUtils.StartupOptions startupOptions =
FlinkConnectorOptionsUtils.getStartupOptions(tableOptions, timeZone);
final FlinkConnectorOptionsUtils.BoundedOptions boundedOptions =
FlinkConnectorOptionsUtils.getBoundedOptions(tableOptions, timeZone);

ResolvedSchema resolvedSchema = context.getCatalogTable().getResolvedSchema();
ResolvedCatalogTable resolvedCatalogTable = context.getCatalogTable();
Expand Down Expand Up @@ -161,6 +163,7 @@ public DynamicTableSource createDynamicTableSource(Context context) {
partitionKeyIndexes,
isStreamingMode,
startupOptions,
boundedOptions,
tableOptions.get(FlinkConnectorOptions.LOOKUP_ASYNC),
tableOptions.get(FlinkConnectorOptions.LOOKUP_INSERT_IF_NOT_EXISTS),
cache,
Expand Down Expand Up @@ -236,6 +239,8 @@ public Set<ConfigOption<?>> optionalOptions() {
FlinkConnectorOptions.BUCKET_NUMBER,
FlinkConnectorOptions.SCAN_STARTUP_MODE,
FlinkConnectorOptions.SCAN_STARTUP_TIMESTAMP,
FlinkConnectorOptions.SCAN_BOUNDED_MODE,
FlinkConnectorOptions.SCAN_BOUNDED_TIMESTAMP,
FlinkConnectorOptions.SCAN_PARTITION_DISCOVERY_INTERVAL,
FlinkConnectorOptions.SCAN_SPLIT_ASSIGNMENT_BATCH_SIZE,
FlinkConnectorOptions.SCAN_KV_SNAPSHOT_LEASE_ID,
Expand Down Expand Up @@ -356,6 +361,8 @@ private DynamicTableSource createChangelogTableSource(
context.getConfiguration().get(TableConfigOptions.LOCAL_TIME_ZONE));
final FlinkConnectorOptionsUtils.StartupOptions startupOptions =
FlinkConnectorOptionsUtils.getStartupOptions(tableOptions, timeZone);
final FlinkConnectorOptionsUtils.BoundedOptions boundedOptions =
FlinkConnectorOptionsUtils.getBoundedOptions(tableOptions, timeZone);

ResolvedCatalogTable resolvedCatalogTable = context.getCatalogTable();

Expand All @@ -379,6 +386,7 @@ private DynamicTableSource createChangelogTableSource(
partitionKeyIndexes,
isStreamingMode,
startupOptions,
boundedOptions,
partitionDiscoveryIntervalMs,
splitAssignmentBatchSize,
catalogTableOptions);
Expand Down Expand Up @@ -410,6 +418,8 @@ private DynamicTableSource createBinlogTableSource(
context.getConfiguration().get(TableConfigOptions.LOCAL_TIME_ZONE));
final FlinkConnectorOptionsUtils.StartupOptions startupOptions =
FlinkConnectorOptionsUtils.getStartupOptions(tableOptions, timeZone);
final FlinkConnectorOptionsUtils.BoundedOptions boundedOptions =
FlinkConnectorOptionsUtils.getBoundedOptions(tableOptions, timeZone);

// Check if the table is partitioned from the internal option
boolean isPartitioned =
Expand All @@ -429,6 +439,7 @@ private DynamicTableSource createBinlogTableSource(
isPartitioned,
isStreamingMode,
startupOptions,
boundedOptions,
partitionDiscoveryIntervalMs,
splitAssignmentBatchSize,
catalogTableOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class BinlogFlinkTableSource
private final boolean isPartitioned;
private final boolean streaming;
private final FlinkConnectorOptionsUtils.StartupOptions startupOptions;
private final FlinkConnectorOptionsUtils.BoundedOptions boundedOptions;
private final long scanPartitionDiscoveryIntervalMs;
private final int splitPerAssignmentBatchSize;
private final Map<String, String> tableOptions;
Expand Down Expand Up @@ -93,12 +94,37 @@ public BinlogFlinkTableSource(
long scanPartitionDiscoveryIntervalMs,
int splitPerAssignmentBatchSize,
Map<String, String> tableOptions) {
this(
tablePath,
flussConfig,
binlogOutputType,
isPartitioned,
streaming,
startupOptions,
FlinkConnectorOptionsUtils.BoundedOptions.unbounded(),
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
tableOptions);
}

public BinlogFlinkTableSource(
TablePath tablePath,
Configuration flussConfig,
org.apache.flink.table.types.logical.RowType binlogOutputType,
boolean isPartitioned,
boolean streaming,
FlinkConnectorOptionsUtils.StartupOptions startupOptions,
FlinkConnectorOptionsUtils.BoundedOptions boundedOptions,
long scanPartitionDiscoveryIntervalMs,
int splitPerAssignmentBatchSize,
Map<String, String> tableOptions) {
this.tablePath = tablePath;
this.flussConfig = flussConfig;
this.binlogOutputType = binlogOutputType;
this.isPartitioned = isPartitioned;
this.streaming = streaming;
this.startupOptions = startupOptions;
this.boundedOptions = boundedOptions;
this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
this.splitPerAssignmentBatchSize = splitPerAssignmentBatchSize;
this.tableOptions = tableOptions;
Expand Down Expand Up @@ -142,6 +168,8 @@ public ScanRuntimeProvider getScanRuntimeProvider(ScanContext scanContext) {
}

// Create the source with the binlog deserialization schema
OffsetsInitializer stoppingOffsetsInitializer =
FlinkConnectorOptionsUtils.toStoppingOffsetsInitializer(streaming, boundedOptions);
FlinkSource<RowData> source =
new FlinkSource<>(
flussConfig,
Expand All @@ -152,6 +180,8 @@ public ScanRuntimeProvider getScanRuntimeProvider(ScanContext scanContext) {
null,
null,
offsetsInitializer,
stoppingOffsetsInitializer,
FlinkConnectorOptionsUtils.toBoundedness(streaming, boundedOptions),
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
new BinlogDeserializationSchema(),
Expand All @@ -160,6 +190,7 @@ public ScanRuntimeProvider getScanRuntimeProvider(ScanContext scanContext) {
// $binlog data/partition columns are nested inside before/after ROWs, so no
// top-level partition filter is pushable; always scan without one.
null,
null,
LeaseContext.DEFAULT);

return SourceProvider.of(source);
Expand All @@ -175,6 +206,7 @@ public DynamicTableSource copy() {
isPartitioned,
streaming,
startupOptions,
boundedOptions,
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
tableOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public class ChangelogFlinkTableSource
private final int[] partitionKeyIndexes;
private final boolean streaming;
private final FlinkConnectorOptionsUtils.StartupOptions startupOptions;
private final FlinkConnectorOptionsUtils.BoundedOptions boundedOptions;
private final long scanPartitionDiscoveryIntervalMs;
private final int splitPerAssignmentBatchSize;
private final Map<String, String> tableOptions;
Expand Down Expand Up @@ -129,13 +130,38 @@ public ChangelogFlinkTableSource(
long scanPartitionDiscoveryIntervalMs,
int splitPerAssignmentBatchSize,
Map<String, String> tableOptions) {
this(
tablePath,
flussConfig,
changelogOutputType,
partitionKeyIndexes,
streaming,
startupOptions,
FlinkConnectorOptionsUtils.BoundedOptions.unbounded(),
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
tableOptions);
}

public ChangelogFlinkTableSource(
TablePath tablePath,
Configuration flussConfig,
org.apache.flink.table.types.logical.RowType changelogOutputType,
int[] partitionKeyIndexes,
boolean streaming,
FlinkConnectorOptionsUtils.StartupOptions startupOptions,
FlinkConnectorOptionsUtils.BoundedOptions boundedOptions,
long scanPartitionDiscoveryIntervalMs,
int splitPerAssignmentBatchSize,
Map<String, String> tableOptions) {
this.tablePath = tablePath;
this.flussConfig = flussConfig;
// The changelogOutputType already includes metadata columns from FlinkCatalog
this.changelogOutputType = changelogOutputType;
this.partitionKeyIndexes = partitionKeyIndexes;
this.streaming = streaming;
this.startupOptions = startupOptions;
this.boundedOptions = boundedOptions;
this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
this.splitPerAssignmentBatchSize = splitPerAssignmentBatchSize;
this.tableOptions = tableOptions;
Expand Down Expand Up @@ -198,6 +224,8 @@ public ScanRuntimeProvider getScanRuntimeProvider(ScanContext scanContext) {
}

// Create the source with the changelog deserialization schema
OffsetsInitializer stoppingOffsetsInitializer =
FlinkConnectorOptionsUtils.toStoppingOffsetsInitializer(streaming, boundedOptions);
FlinkSource<RowData> source =
new FlinkSource<>(
flussConfig,
Expand All @@ -212,12 +240,15 @@ public ScanRuntimeProvider getScanRuntimeProvider(ScanContext scanContext) {
dataProjection,
logRecordBatchFilter,
offsetsInitializer,
stoppingOffsetsInitializer,
FlinkConnectorOptionsUtils.toBoundedness(streaming, boundedOptions),
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
new ChangelogDeserializationSchema(),
FlinkConversions.toFlussRowType(producedDataType),
streaming,
partitionFilters,
null,
LeaseContext.DEFAULT); // Lake source not supported

return SourceProvider.of(source);
Expand All @@ -233,6 +264,7 @@ public DynamicTableSource copy() {
partitionKeyIndexes,
streaming,
startupOptions,
boundedOptions,
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
tableOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.fluss.flink.source;

import org.apache.fluss.client.initializer.NoStoppingOffsetsInitializer;
import org.apache.fluss.client.initializer.OffsetsInitializer;
import org.apache.fluss.config.Configuration;
import org.apache.fluss.flink.FlinkConnectorOptions;
Expand Down Expand Up @@ -75,9 +76,11 @@ public class FlinkSource<OUT>
@Nullable private final FlinkRecordEmitter.OutputProjection<OUT> outputProjection;
@Nullable private final int[] projectedFields;
protected final OffsetsInitializer offsetsInitializer;
protected final OffsetsInitializer stoppingOffsetsInitializer;
protected final long scanPartitionDiscoveryIntervalMs;
protected final int splitPerAssignmentBatchSize;
private final boolean streaming;
private final Boundedness boundedness;
private final FlussDeserializationSchema<OUT> deserializationSchema;
@Nullable private final Predicate partitionFilters;
@Nullable private final LakeSource<LakeSplit> lakeSource;
Expand Down Expand Up @@ -208,6 +211,8 @@ public FlinkSource(
projectedFields,
logRecordBatchFilter,
offsetsInitializer,
streaming ? new NoStoppingOffsetsInitializer() : OffsetsInitializer.latest(),
streaming ? Boundedness.CONTINUOUS_UNBOUNDED : Boundedness.BOUNDED,
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
deserializationSchema,
Expand Down Expand Up @@ -243,6 +248,8 @@ public FlinkSource(
projectedFields,
logRecordBatchFilter,
offsetsInitializer,
streaming ? new NoStoppingOffsetsInitializer() : OffsetsInitializer.latest(),
streaming ? Boundedness.CONTINUOUS_UNBOUNDED : Boundedness.BOUNDED,
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
deserializationSchema,
Expand All @@ -262,6 +269,8 @@ public FlinkSource(
@Nullable int[] projectedFields,
@Nullable Predicate logRecordBatchFilter,
OffsetsInitializer offsetsInitializer,
OffsetsInitializer stoppingOffsetsInitializer,
Boundedness boundedness,
long scanPartitionDiscoveryIntervalMs,
int splitPerAssignmentBatchSize,
FlussDeserializationSchema<OUT> deserializationSchema,
Expand All @@ -278,6 +287,8 @@ public FlinkSource(
this.projectedFields = projectedFields;
this.logRecordBatchFilter = logRecordBatchFilter;
this.offsetsInitializer = offsetsInitializer;
this.stoppingOffsetsInitializer = stoppingOffsetsInitializer;
this.boundedness = boundedness;
this.scanPartitionDiscoveryIntervalMs = scanPartitionDiscoveryIntervalMs;
this.splitPerAssignmentBatchSize = splitPerAssignmentBatchSize;
this.deserializationSchema = deserializationSchema;
Expand All @@ -291,7 +302,7 @@ public FlinkSource(

@Override
public Boundedness getBoundedness() {
return streaming ? Boundedness.CONTINUOUS_UNBOUNDED : Boundedness.BOUNDED;
return boundedness;
}

@Override
Expand All @@ -304,6 +315,8 @@ public SplitEnumerator<SourceSplitBase, SourceEnumeratorState> createEnumerator(
isPartitioned,
splitEnumeratorContext,
offsetsInitializer,
stoppingOffsetsInitializer,
boundedness,
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
streaming,
Expand Down Expand Up @@ -335,6 +348,8 @@ public SplitEnumerator<SourceSplitBase, SourceEnumeratorState> restoreEnumerator
sourceEnumeratorState.getAssignedPartitions(),
remainingHybridLakeFlussSplits,
offsetsInitializer,
stoppingOffsetsInitializer,
boundedness,
scanPartitionDiscoveryIntervalMs,
splitPerAssignmentBatchSize,
streaming,
Expand Down
Loading
Loading