You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@iceberg.apache.org by "rodmeneses (via GitHub)" <gi...@apache.org> on 2024/04/18 20:29:40 UTC
[PR] Iceberg sink a [iceberg]
rodmeneses opened a new pull request, #10179:
URL: https://github.com/apache/iceberg/pull/10179
(no comment)
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1708088961
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSinkWriter.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * org.apache.iceberg.flink.sink.IcebergSink} (SinkV2). Writes out the data to the final place, and
+ * emits a single {@link WriteResult} at every checkpoint for every data/delete file created by this
+ * writer.
+ */
+class IcebergSinkWriter implements CommittingSinkWriter<RowData, WriteResult> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSinkWriter.class);
+
+ private final String fullTableName;
+ private final TaskWriterFactory<RowData> taskWriterFactory;
+ private final IcebergStreamWriterMetrics metrics;
+ private TaskWriter<RowData> writer;
+ private final int subTaskId;
+ private final int attemptId;
+
+ IcebergSinkWriter(
+ String fullTableName,
+ TaskWriterFactory<RowData> taskWriterFactory,
+ IcebergStreamWriterMetrics metrics,
+ int subTaskId,
+ int attemptId) {
+ this.fullTableName = fullTableName;
+ this.taskWriterFactory = taskWriterFactory;
+ // Initialize the task writer factory.
+ taskWriterFactory.initialize(subTaskId, attemptId);
+ // Initialize the task writer.
+ this.writer = taskWriterFactory.create();
+ this.metrics = metrics;
+ this.subTaskId = subTaskId;
+ this.attemptId = attemptId;
+
+ LOG.debug(
+ "StreamWriter created for table {} subtask {} attemptId {}",
Review Comment:
nit: Iceberg style doesn't use interface or class name in the log message. targeting end users (not code developer). hence this could be like `Created stream writer for ....`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,761 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ this.table = checkAndGetTable(tableLoader(), table);
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds =
+ SinkUtil.checkAndGetEqualityFieldIds(table, equalityFieldColumns);
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ SerializableTable serializableTable = (SerializableTable) SerializableTable.copyOf(table);
Review Comment:
`table` is already a `SerializeableTable` from `checkAndGetTable`. we are doing double wrapping here?
as mentioned in another comment. if we let `checkAndGetTable` return `SerializableTable`, we can probably avoid this problem and improve readability.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
Review Comment:
nit: Flink checkpoint id starts from 1 in real env. please check it for all tests
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,347 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+ this.tableLoader = tableLoader;
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool(
+ "iceberg-committer-pool-" + table.name() + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
Review Comment:
nit: empty line after a control block like if or for/while loop
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
Review Comment:
this comment is wrong. it is `disordered` case
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
Review Comment:
why have `Mock` in the test name?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
Review Comment:
checkpointId should be `1`? incremented from last checkpoint of 0?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
Review Comment:
we only asserted the first element? do we also need to check the 2nd element?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
Review Comment:
how is this test different with the above? no `CommittableWithLineage`? what is the expected behavior in this case?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
Review Comment:
typo: should be `preJobSnapshotId`
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
Review Comment:
Iceberg style doesn't use `final` for local variables. please check it across the board.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
Review Comment:
what's the order of list? is `[0]` the oldest element?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ newOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), 2);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), -1);
+
+ rows.add(SimpleDataUtil.createRowData(2, "world"));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile("data-new-1", rows);
+
+ processElement(newJobId.toString(), 0, harness, 1, newOperatorId.toString(), dataFile);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleJobsWriteSameTable() throws Exception {
+ long timestamp = 0;
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID[] jobs = new JobID[] {new JobID(), new JobID(), new JobID()};
+ OperatorID[] operatorIds =
+ new OperatorID[] {new OperatorID(), new OperatorID(), new OperatorID()};
+ for (int i = 0; i < 20; i++) {
+ int jobIndex = i % 3;
+ int checkpointId = i / 3;
+ JobID jobID = jobs[jobIndex];
+ OperatorID operatorId = operatorIds[jobIndex];
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.setup();
+ harness.open();
+
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(
+ jobID.toString(), operatorId.toString(), checkpointId == 0 ? -1 : checkpointId - 1);
+
+ List<RowData> rows = Lists.newArrayList(SimpleDataUtil.createRowData(i, "word-" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+
+ processElement(jobID.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i + 1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpointId);
+ }
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleSinksRecoveryFromValidSnapshot() throws Exception {
+ long checkpointId = 0;
+ long timestamp = 0;
+ List<RowData> expectedRows = Lists.newArrayList();
+ OperatorSubtaskState snapshot1;
+ OperatorSubtaskState snapshot2;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId1 = new OperatorID();
+ OperatorID operatorId2 = new OperatorID();
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.open();
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.open();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), -1L);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-1-1", ImmutableList.of(row1));
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ snapshot1 = harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(1, "hello2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-1-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ snapshot2 = harness2.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // Only notify one of the committers
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(1);
+
+ // Only the first row is committed at this point
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1);
+ }
+
+ // Restore from the given snapshot
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.initializeState(snapshot1);
+ harness1.open();
+
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.initializeState(snapshot2);
+ harness2.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+
+ RowData row1 = SimpleDataUtil.createRowData(2, "world1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-2-1", ImmutableList.of(row1));
+
+ checkpointId++;
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-2-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ harness2.snapshot(checkpointId, ++timestamp);
+
+ assertFlinkManifests(2);
+
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ harness2.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testFlinkManifests() throws Exception {
+ long timestamp = 0;
+ final long checkpoint = 10;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ // harness.processElement(of(dataFile1), ++timestamp);
+ processElement(jobID.toString(), checkpoint, harness, 1, operatorId.toString(), dataFile1);
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ // 1. snapshotState for checkpoint#1
+ harness.snapshot(checkpoint, ++timestamp);
+ List<Path> manifestPaths = assertFlinkManifests(1);
+ Path manifestPath = manifestPaths.get(0);
+ assertThat(manifestPath.getFileName())
+ .asString()
+ .isEqualTo(
+ String.format("%s-%s-%05d-%d-%d-%05d.avro", jobID, operatorId, 0, 0, checkpoint, 1));
+ //
+ // 2. Read the data files from manifests and assert.
+ List<DataFile> dataFiles =
+ FlinkManifestUtil.readDataFiles(
+ createTestingManifestFile(manifestPath), table.io(), table.specs());
+ assertThat(dataFiles).hasSize(1);
+ TestHelpers.assertEquals(dataFile1, dataFiles.get(0));
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(checkpoint);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpoint);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testHandleEndInput() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ testHarness.endInput();
+
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // Future emission calls should change the output
+ testHarness.notifyOfCompletedCheckpoint(2);
+ testHarness.endInput();
+
+ assertThat(testHarness.getOutput()).hasSize(2);
+ }
+
+ @TestTemplate
+ public void testDeleteFiles() throws Exception {
+
+ assumeThat(formatVersion).as("Only support delete in format v2").isGreaterThanOrEqualTo(2);
+ FileAppenderFactory<RowData> appenderFactory = createDeletableAppenderFactory();
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ long checkpointId = 0;
+ RowData row1 = SimpleDataUtil.createInsert(1, "aaa");
+ DataFile dataFile1 = writeDataFile("data-file-1", ImmutableList.of(row1));
+ processElement(jobId, checkpointId, testHarness, 1, "flink-sink", dataFile1);
+
+ testHarness.snapshot(checkpointId, 0);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+
+ // The 2. commit
+ checkpointId = 1;
+ RowData row2 = SimpleDataUtil.createInsert(2, "bbb");
+ DataFile dataFile2 = writeDataFile("data-file-2", ImmutableList.of(row2));
+
+ RowData row3 = SimpleDataUtil.createInsert(3, "ccc");
+ DataFile dataFile3 = writeDataFile("data-file-3", ImmutableList.of(row3));
+ processElement(jobId, checkpointId, testHarness, 2, "flink-sink", dataFile2, dataFile3);
+
+ testHarness.snapshot(checkpointId, 1);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2, row3), branch);
+
+ final List<StreamElement> output2 = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output2).hasSize(2 + 2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(2)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // The 3. commit
+ checkpointId = 2;
+ RowData delete1 = SimpleDataUtil.createDelete(1, "aaa");
+ DeleteFile deleteFile1 =
+ writeEqDeleteFile(appenderFactory, "delete-file-1", ImmutableList.of(delete1));
+ RowData row4 = SimpleDataUtil.createInsert(4, "ddd");
+ DataFile dataFile4 = writeDataFile("data-file-4", ImmutableList.of(row4));
+
+ RowData row5 = SimpleDataUtil.createInsert(5, "eee");
+ DataFile dataFile5 = writeDataFile("data-file-5", ImmutableList.of(row5));
+ WriteResult withRecord4 =
+ WriteResult.builder()
+ .addDataFiles(dataFile4, dataFile5)
+ .addDeleteFiles(deleteFile1)
+ .build();
+ processElement(withRecord4, jobId, checkpointId, testHarness, 2, "flink-sink");
+
+ testHarness.snapshot(checkpointId, 3);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row2, row3, row4, row5), branch);
+
+ final List<StreamElement> output3 = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output3).hasSize(2 + 2 + 2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(4)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+ }
+
+ private ManifestFile createTestingManifestFile(Path manifestPath) {
+ return new GenericManifestFile(
+ manifestPath.toAbsolutePath().toString(),
+ manifestPath.toFile().length(),
+ 0,
+ ManifestContent.DATA,
+ 0,
+ 0,
+ 0L,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ null,
+ null);
+ }
+
+ private IcebergWriteAggregator buildIcebergWriteAggregator(String myJobId, String operatorId) {
+ IcebergWriteAggregator icebergWriteAggregator = spy(new IcebergWriteAggregator(tableLoader));
+ StreamTask ctx = mock(StreamTask.class);
+ Environment env = mock(Environment.class);
+ StreamingRuntimeContext streamingRuntimeContext = mock(StreamingRuntimeContext.class);
+ TaskInfo taskInfo = mock(TaskInfo.class);
+ JobID myJobID = mock(JobID.class);
+ OperatorID operatorID = mock(OperatorID.class);
+ doReturn(myJobId).when(myJobID).toString();
+ doReturn(myJobID).when(env).getJobID();
+ doReturn(env).when(ctx).getEnvironment();
+ doReturn(ctx).when(icebergWriteAggregator).getContainingTask();
+ doReturn(operatorId).when(operatorID).toString();
+ doReturn(operatorID).when(icebergWriteAggregator).getOperatorID();
+ doReturn(0).when(taskInfo).getAttemptNumber();
+ doReturn(taskInfo).when(streamingRuntimeContext).getTaskInfo();
+ doReturn(streamingRuntimeContext).when(icebergWriteAggregator).getRuntimeContext();
+ try {
+ icebergWriteAggregator.open();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return icebergWriteAggregator;
+ }
+
+ private CommittableSummary<IcebergCommittable> processElement(
+ WriteResult withRecord,
+ String myJobId,
+ long checkpointId,
+ OneInputStreamOperatorTestHarness testHarness,
+ int subTaskId,
+ String operatorId)
+ throws Exception {
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(myJobId, operatorId)
+ .writeToManifest(Lists.newArrayList(withRecord), checkpointId),
+ myJobId,
+ operatorId,
+ checkpointId);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(subTaskId, 1, checkpointId, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committable =
+ new CommittableWithLineage<>(commit, checkpointId, subTaskId);
+ testHarness.processElement(new StreamRecord<>(committable));
+ return committableSummary;
+ }
+
+ private CommittableSummary<IcebergCommittable> processElement(
+ String myJobID,
+ long checkpointId,
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness,
+ int subTaskId,
+ String operatorId,
+ DataFile... dataFile)
+ throws Exception {
+ WriteResult withRecord = WriteResult.builder().addDataFiles(dataFile).build();
+ return processElement(withRecord, myJobID, checkpointId, testHarness, subTaskId, operatorId);
+ }
+
+ private FileAppenderFactory<RowData> createDeletableAppenderFactory() {
+ int[] equalityFieldIds =
+ new int[] {
+ table.schema().findField("id").fieldId(), table.schema().findField("data").fieldId()
+ };
+ return new FlinkAppenderFactory(
+ table,
+ table.schema(),
+ FlinkSchemaUtil.convert(table.schema()),
+ table.properties(),
+ table.spec(),
+ equalityFieldIds,
+ table.schema(),
+ null);
+ }
+
+ private List<Path> assertFlinkManifests(int expectedCount) throws IOException {
+ List<Path> manifests =
+ Files.list(flinkManifestFolder.toPath())
+ .filter(p -> !p.toString().endsWith(".crc"))
+ .collect(Collectors.toList());
+ assertThat(manifests).hasSize(expectedCount);
+ return manifests;
+ }
+
+ private DataFile writeDataFile(String filename, List<RowData> rows) throws IOException {
+ return SimpleDataUtil.writeFile(
+ table,
+ table.schema(),
+ table.spec(),
+ new Configuration(),
+ table.location(),
+ FileFormat.PARQUET.addExtension(filename),
+ rows);
+ }
+
+ private DeleteFile writeEqDeleteFile(
+ FileAppenderFactory<RowData> appenderFactory, String filename, List<RowData> deletes)
+ throws IOException {
+ return SimpleDataUtil.writeEqDeleteFile(
+ table, FileFormat.PARQUET, filename, appenderFactory, deletes);
+ }
+
+ private OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ getTestHarness() throws Exception {
+ IcebergSink sink =
+ IcebergSink.forRowData(null).table(table).toBranch(branch).tableLoader(tableLoader).build();
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, isBatchMode, true));
+ testHarness.setup(committableMessageTypeSerializer);
+
+ return testHarness;
+ }
+
+ // ------------------------------- Mock Classes --------------------------------
+
+ private static class ForwardingCommitter extends IcebergCommitter {
Review Comment:
what does `Forwarding` mean in this case? commit request is not actually forwarded to the base class `IcebergCommitter`. it seems that simple `TestCommitter` is more accurate.
also does it need to extend from `IcebergCommitter`?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
Review Comment:
`OneInputStreamOperatorTestHarness` should be used with try-resource pattern so that they are closed. Please check this for all tests.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
Review Comment:
who emit the commit records in this case?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -41,6 +42,19 @@ class ManifestOutputFileFactory {
private final long attemptNumber;
private final AtomicInteger fileCount = new AtomicInteger(0);
+ public ManifestOutputFileFactory(
+ Supplier<Table> tableSupplier,
+ Map<String, String> props,
+ String flinkJobId,
+ String operatorId) {
+ this.tableSupplier = tableSupplier;
+ this.props = props;
+ this.flinkJobId = flinkJobId;
+ this.operatorUniqueId = operatorId;
+ this.subTaskId = 0;
Review Comment:
@rodmeneses why is this marked as resolved?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
Review Comment:
I don't know how we distinguish btw restoring from a previous job or a current job. jobId is not used during test harness creation.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
Review Comment:
fix formatting. also there is incomplete sentence here or extra words
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ newOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), 2);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), -1);
+
+ rows.add(SimpleDataUtil.createRowData(2, "world"));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile("data-new-1", rows);
+
+ processElement(newJobId.toString(), 0, harness, 1, newOperatorId.toString(), dataFile);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
Review Comment:
it seems that checkpointId is 0 here, which is different with the comment from line 790.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
Review Comment:
don't we need to call `harness.snapshot(...)` first?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
Review Comment:
how is `newJobId` set to the harness/committer so that it is used for Iceberg commit?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
why is this resolved without comment?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ newOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), 2);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), -1);
+
+ rows.add(SimpleDataUtil.createRowData(2, "world"));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile("data-new-1", rows);
+
+ processElement(newJobId.toString(), 0, harness, 1, newOperatorId.toString(), dataFile);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleJobsWriteSameTable() throws Exception {
+ long timestamp = 0;
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID[] jobs = new JobID[] {new JobID(), new JobID(), new JobID()};
+ OperatorID[] operatorIds =
+ new OperatorID[] {new OperatorID(), new OperatorID(), new OperatorID()};
+ for (int i = 0; i < 20; i++) {
+ int jobIndex = i % 3;
+ int checkpointId = i / 3;
+ JobID jobID = jobs[jobIndex];
+ OperatorID operatorId = operatorIds[jobIndex];
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.setup();
+ harness.open();
+
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(
+ jobID.toString(), operatorId.toString(), checkpointId == 0 ? -1 : checkpointId - 1);
+
+ List<RowData> rows = Lists.newArrayList(SimpleDataUtil.createRowData(i, "word-" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+
+ processElement(jobID.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i + 1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpointId);
+ }
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleSinksRecoveryFromValidSnapshot() throws Exception {
+ long checkpointId = 0;
+ long timestamp = 0;
+ List<RowData> expectedRows = Lists.newArrayList();
+ OperatorSubtaskState snapshot1;
+ OperatorSubtaskState snapshot2;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId1 = new OperatorID();
+ OperatorID operatorId2 = new OperatorID();
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.open();
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.open();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), -1L);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-1-1", ImmutableList.of(row1));
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ snapshot1 = harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(1, "hello2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-1-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ snapshot2 = harness2.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // Only notify one of the committers
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(1);
+
+ // Only the first row is committed at this point
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1);
+ }
+
+ // Restore from the given snapshot
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.initializeState(snapshot1);
+ harness1.open();
+
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.initializeState(snapshot2);
+ harness2.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+
+ RowData row1 = SimpleDataUtil.createRowData(2, "world1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-2-1", ImmutableList.of(row1));
+
+ checkpointId++;
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-2-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ harness2.snapshot(checkpointId, ++timestamp);
+
+ assertFlinkManifests(2);
+
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ harness2.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testFlinkManifests() throws Exception {
+ long timestamp = 0;
+ final long checkpoint = 10;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ // harness.processElement(of(dataFile1), ++timestamp);
+ processElement(jobID.toString(), checkpoint, harness, 1, operatorId.toString(), dataFile1);
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ // 1. snapshotState for checkpoint#1
+ harness.snapshot(checkpoint, ++timestamp);
+ List<Path> manifestPaths = assertFlinkManifests(1);
+ Path manifestPath = manifestPaths.get(0);
+ assertThat(manifestPath.getFileName())
+ .asString()
+ .isEqualTo(
+ String.format("%s-%s-%05d-%d-%d-%05d.avro", jobID, operatorId, 0, 0, checkpoint, 1));
+ //
+ // 2. Read the data files from manifests and assert.
+ List<DataFile> dataFiles =
+ FlinkManifestUtil.readDataFiles(
+ createTestingManifestFile(manifestPath), table.io(), table.specs());
+ assertThat(dataFiles).hasSize(1);
+ TestHelpers.assertEquals(dataFile1, dataFiles.get(0));
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(checkpoint);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpoint);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testHandleEndInput() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ testHarness.endInput();
+
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // Future emission calls should change the output
+ testHarness.notifyOfCompletedCheckpoint(2);
+ testHarness.endInput();
+
+ assertThat(testHarness.getOutput()).hasSize(2);
+ }
+
+ @TestTemplate
+ public void testDeleteFiles() throws Exception {
+
+ assumeThat(formatVersion).as("Only support delete in format v2").isGreaterThanOrEqualTo(2);
+ FileAppenderFactory<RowData> appenderFactory = createDeletableAppenderFactory();
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ long checkpointId = 0;
+ RowData row1 = SimpleDataUtil.createInsert(1, "aaa");
+ DataFile dataFile1 = writeDataFile("data-file-1", ImmutableList.of(row1));
+ processElement(jobId, checkpointId, testHarness, 1, "flink-sink", dataFile1);
+
+ testHarness.snapshot(checkpointId, 0);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+
+ // The 2. commit
+ checkpointId = 1;
+ RowData row2 = SimpleDataUtil.createInsert(2, "bbb");
+ DataFile dataFile2 = writeDataFile("data-file-2", ImmutableList.of(row2));
+
+ RowData row3 = SimpleDataUtil.createInsert(3, "ccc");
+ DataFile dataFile3 = writeDataFile("data-file-3", ImmutableList.of(row3));
+ processElement(jobId, checkpointId, testHarness, 2, "flink-sink", dataFile2, dataFile3);
+
+ testHarness.snapshot(checkpointId, 1);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2, row3), branch);
+
+ final List<StreamElement> output2 = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output2).hasSize(2 + 2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(2)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // The 3. commit
+ checkpointId = 2;
+ RowData delete1 = SimpleDataUtil.createDelete(1, "aaa");
+ DeleteFile deleteFile1 =
+ writeEqDeleteFile(appenderFactory, "delete-file-1", ImmutableList.of(delete1));
+ RowData row4 = SimpleDataUtil.createInsert(4, "ddd");
+ DataFile dataFile4 = writeDataFile("data-file-4", ImmutableList.of(row4));
+
+ RowData row5 = SimpleDataUtil.createInsert(5, "eee");
+ DataFile dataFile5 = writeDataFile("data-file-5", ImmutableList.of(row5));
+ WriteResult withRecord4 =
+ WriteResult.builder()
+ .addDataFiles(dataFile4, dataFile5)
+ .addDeleteFiles(deleteFile1)
+ .build();
+ processElement(withRecord4, jobId, checkpointId, testHarness, 2, "flink-sink");
+
+ testHarness.snapshot(checkpointId, 3);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row2, row3, row4, row5), branch);
+
+ final List<StreamElement> output3 = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output3).hasSize(2 + 2 + 2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(4)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+ }
+
+ private ManifestFile createTestingManifestFile(Path manifestPath) {
+ return new GenericManifestFile(
+ manifestPath.toAbsolutePath().toString(),
+ manifestPath.toFile().length(),
+ 0,
+ ManifestContent.DATA,
+ 0,
+ 0,
+ 0L,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ null,
+ null);
+ }
+
+ private IcebergWriteAggregator buildIcebergWriteAggregator(String myJobId, String operatorId) {
+ IcebergWriteAggregator icebergWriteAggregator = spy(new IcebergWriteAggregator(tableLoader));
+ StreamTask ctx = mock(StreamTask.class);
+ Environment env = mock(Environment.class);
+ StreamingRuntimeContext streamingRuntimeContext = mock(StreamingRuntimeContext.class);
+ TaskInfo taskInfo = mock(TaskInfo.class);
+ JobID myJobID = mock(JobID.class);
+ OperatorID operatorID = mock(OperatorID.class);
+ doReturn(myJobId).when(myJobID).toString();
+ doReturn(myJobID).when(env).getJobID();
+ doReturn(env).when(ctx).getEnvironment();
+ doReturn(ctx).when(icebergWriteAggregator).getContainingTask();
+ doReturn(operatorId).when(operatorID).toString();
+ doReturn(operatorID).when(icebergWriteAggregator).getOperatorID();
+ doReturn(0).when(taskInfo).getAttemptNumber();
+ doReturn(taskInfo).when(streamingRuntimeContext).getTaskInfo();
+ doReturn(streamingRuntimeContext).when(icebergWriteAggregator).getRuntimeContext();
+ try {
+ icebergWriteAggregator.open();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return icebergWriteAggregator;
+ }
+
+ private CommittableSummary<IcebergCommittable> processElement(
+ WriteResult withRecord,
+ String myJobId,
+ long checkpointId,
+ OneInputStreamOperatorTestHarness testHarness,
+ int subTaskId,
+ String operatorId)
+ throws Exception {
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(myJobId, operatorId)
+ .writeToManifest(Lists.newArrayList(withRecord), checkpointId),
+ myJobId,
+ operatorId,
+ checkpointId);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(subTaskId, 1, checkpointId, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committable =
+ new CommittableWithLineage<>(commit, checkpointId, subTaskId);
+ testHarness.processElement(new StreamRecord<>(committable));
+ return committableSummary;
+ }
+
+ private CommittableSummary<IcebergCommittable> processElement(
+ String myJobID,
+ long checkpointId,
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness,
+ int subTaskId,
+ String operatorId,
+ DataFile... dataFile)
+ throws Exception {
+ WriteResult withRecord = WriteResult.builder().addDataFiles(dataFile).build();
+ return processElement(withRecord, myJobID, checkpointId, testHarness, subTaskId, operatorId);
+ }
+
+ private FileAppenderFactory<RowData> createDeletableAppenderFactory() {
+ int[] equalityFieldIds =
+ new int[] {
+ table.schema().findField("id").fieldId(), table.schema().findField("data").fieldId()
+ };
+ return new FlinkAppenderFactory(
+ table,
+ table.schema(),
+ FlinkSchemaUtil.convert(table.schema()),
+ table.properties(),
+ table.spec(),
+ equalityFieldIds,
+ table.schema(),
+ null);
+ }
+
+ private List<Path> assertFlinkManifests(int expectedCount) throws IOException {
+ List<Path> manifests =
+ Files.list(flinkManifestFolder.toPath())
+ .filter(p -> !p.toString().endsWith(".crc"))
+ .collect(Collectors.toList());
+ assertThat(manifests).hasSize(expectedCount);
+ return manifests;
+ }
+
+ private DataFile writeDataFile(String filename, List<RowData> rows) throws IOException {
+ return SimpleDataUtil.writeFile(
+ table,
+ table.schema(),
+ table.spec(),
+ new Configuration(),
+ table.location(),
+ FileFormat.PARQUET.addExtension(filename),
+ rows);
+ }
+
+ private DeleteFile writeEqDeleteFile(
+ FileAppenderFactory<RowData> appenderFactory, String filename, List<RowData> deletes)
+ throws IOException {
+ return SimpleDataUtil.writeEqDeleteFile(
+ table, FileFormat.PARQUET, filename, appenderFactory, deletes);
+ }
+
+ private OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ getTestHarness() throws Exception {
+ IcebergSink sink =
+ IcebergSink.forRowData(null).table(table).toBranch(branch).tableLoader(tableLoader).build();
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, isBatchMode, true));
+ testHarness.setup(committableMessageTypeSerializer);
+
+ return testHarness;
+ }
+
+ // ------------------------------- Mock Classes --------------------------------
+
+ private static class ForwardingCommitter extends IcebergCommitter {
+ private int successfulCommits = 0;
+
+ ForwardingCommitter(TableLoader tableLoader) {
+ super(tableLoader, null, Maps.newHashMap(), false, 2, "test-sink-prefix", null);
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ commitRequests.forEach(
+ one -> LOG.info("Committed with numberOfRetries: {}", one.getNumberOfRetries()));
+ successfulCommits += commitRequests.size();
+ }
+
+ @Override
+ public void close() {}
+
+ public int getSuccessfulCommits() {
+ return successfulCommits;
+ }
+ }
+
+ // ------------------------------- Utility Methods --------------------------------
+
+ private IcebergCommitter getCommitter() {
+ IcebergFilesCommitterMetrics metric = mock(IcebergFilesCommitterMetrics.class);
+ return new IcebergCommitter(
+ tableLoader,
+ branch,
+ Collections.singletonMap("flink.test", TestIcebergCommitter.class.getName()),
+ false,
+ 10,
+ "sinkId",
+ metric);
+ }
+
+ private Committer.CommitRequest<IcebergCommittable> buildCommitRequestFor(
+ String myJobID, long checkpoint, Collection<WriteResult> writeResults) throws IOException {
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(myJobID, "flink-sink")
+ .writeToManifest(writeResults, checkpoint),
+ myJobID,
+ "flink-sink",
+ checkpoint);
+ CommittableWithLineage committableWithLineage =
+ new CommittableWithLineage(commit, checkpoint, 1);
+ Committer.CommitRequest<IcebergCommittable> commitRequest = mock(Committer.CommitRequest.class);
+ doReturn(committableWithLineage.getCommittable()).when(commitRequest).getCommittable();
+ return commitRequest;
+ }
+
+ private WriteResult of(DataFile dataFile) {
+ return WriteResult.builder().addDataFiles(dataFile).build();
+ }
+
+ private void assertMaxCommittedCheckpointId(String myJobID, String operatorId, long expectedId) {
+ table.refresh();
+ long actualId =
+ IcebergCommitter.getMaxCommittedCheckpointId(table, myJobID, operatorId, branch);
+ assertThat(actualId).isEqualTo(expectedId);
+ }
+
+ private void assertMaxCommittedCheckpointId(String myJobID, long expectedId) {
+ assertMaxCommittedCheckpointId(myJobID, "flink-sink", expectedId);
+ }
+
+ private void assertSnapshotSize(int expectedSnapshotSize) {
+ table.refresh();
+ assertThat(table.snapshots()).hasSize(expectedSnapshotSize);
+ }
+
+ private static ByteBuffer longToBuffer(long value) {
+ return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(0, value);
+ }
+
+ final TypeSerializer<CommittableMessage<IcebergCommittable>> committableMessageTypeSerializer =
Review Comment:
looks like this is a class member. for readability, maybe define a private static class here and move the class variable up near the beginning, which is typically class member variables are defined.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
Review Comment:
looks like it is `processElement` use the jobId for the aggregator operator.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ newOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), 2);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), -1);
+
+ rows.add(SimpleDataUtil.createRowData(2, "world"));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile("data-new-1", rows);
+
+ processElement(newJobId.toString(), 0, harness, 1, newOperatorId.toString(), dataFile);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleJobsWriteSameTable() throws Exception {
+ long timestamp = 0;
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID[] jobs = new JobID[] {new JobID(), new JobID(), new JobID()};
+ OperatorID[] operatorIds =
+ new OperatorID[] {new OperatorID(), new OperatorID(), new OperatorID()};
+ for (int i = 0; i < 20; i++) {
+ int jobIndex = i % 3;
+ int checkpointId = i / 3;
+ JobID jobID = jobs[jobIndex];
+ OperatorID operatorId = operatorIds[jobIndex];
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.setup();
+ harness.open();
+
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(
+ jobID.toString(), operatorId.toString(), checkpointId == 0 ? -1 : checkpointId - 1);
+
+ List<RowData> rows = Lists.newArrayList(SimpleDataUtil.createRowData(i, "word-" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+
+ processElement(jobID.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i + 1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpointId);
+ }
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleSinksRecoveryFromValidSnapshot() throws Exception {
+ long checkpointId = 0;
+ long timestamp = 0;
+ List<RowData> expectedRows = Lists.newArrayList();
+ OperatorSubtaskState snapshot1;
+ OperatorSubtaskState snapshot2;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId1 = new OperatorID();
+ OperatorID operatorId2 = new OperatorID();
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.open();
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.open();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), -1L);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-1-1", ImmutableList.of(row1));
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ snapshot1 = harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(1, "hello2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-1-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ snapshot2 = harness2.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // Only notify one of the committers
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(1);
+
+ // Only the first row is committed at this point
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1);
+ }
+
+ // Restore from the given snapshot
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.initializeState(snapshot1);
+ harness1.open();
+
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.initializeState(snapshot2);
+ harness2.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+
+ RowData row1 = SimpleDataUtil.createRowData(2, "world1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-2-1", ImmutableList.of(row1));
+
+ checkpointId++;
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-2-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ harness2.snapshot(checkpointId, ++timestamp);
+
+ assertFlinkManifests(2);
+
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ harness2.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testFlinkManifests() throws Exception {
+ long timestamp = 0;
+ final long checkpoint = 10;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ // harness.processElement(of(dataFile1), ++timestamp);
+ processElement(jobID.toString(), checkpoint, harness, 1, operatorId.toString(), dataFile1);
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ // 1. snapshotState for checkpoint#1
+ harness.snapshot(checkpoint, ++timestamp);
+ List<Path> manifestPaths = assertFlinkManifests(1);
+ Path manifestPath = manifestPaths.get(0);
+ assertThat(manifestPath.getFileName())
+ .asString()
+ .isEqualTo(
+ String.format("%s-%s-%05d-%d-%d-%05d.avro", jobID, operatorId, 0, 0, checkpoint, 1));
+ //
+ // 2. Read the data files from manifests and assert.
+ List<DataFile> dataFiles =
+ FlinkManifestUtil.readDataFiles(
+ createTestingManifestFile(manifestPath), table.io(), table.specs());
+ assertThat(dataFiles).hasSize(1);
+ TestHelpers.assertEquals(dataFile1, dataFiles.get(0));
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(checkpoint);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpoint);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testHandleEndInput() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ testHarness.endInput();
+
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // Future emission calls should change the output
+ testHarness.notifyOfCompletedCheckpoint(2);
+ testHarness.endInput();
+
+ assertThat(testHarness.getOutput()).hasSize(2);
Review Comment:
we should also `assertMaxCommittedCheckpointId` right on `Long.MAX_VALUE`?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
Review Comment:
I see `checkAndGetTable` internally constructs `SerializableTable.copyOf`. might be cleaner to do the type cast inside that method and return a `SerializableTable` from it. casting immediately after the construction is easy to see it is safe.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ newOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), 2);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), -1);
+
+ rows.add(SimpleDataUtil.createRowData(2, "world"));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile("data-new-1", rows);
+
+ processElement(newJobId.toString(), 0, harness, 1, newOperatorId.toString(), dataFile);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleJobsWriteSameTable() throws Exception {
+ long timestamp = 0;
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID[] jobs = new JobID[] {new JobID(), new JobID(), new JobID()};
+ OperatorID[] operatorIds =
+ new OperatorID[] {new OperatorID(), new OperatorID(), new OperatorID()};
+ for (int i = 0; i < 20; i++) {
+ int jobIndex = i % 3;
+ int checkpointId = i / 3;
+ JobID jobID = jobs[jobIndex];
+ OperatorID operatorId = operatorIds[jobIndex];
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.setup();
+ harness.open();
+
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(
+ jobID.toString(), operatorId.toString(), checkpointId == 0 ? -1 : checkpointId - 1);
+
+ List<RowData> rows = Lists.newArrayList(SimpleDataUtil.createRowData(i, "word-" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+
+ processElement(jobID.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i + 1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpointId);
+ }
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleSinksRecoveryFromValidSnapshot() throws Exception {
+ long checkpointId = 0;
+ long timestamp = 0;
+ List<RowData> expectedRows = Lists.newArrayList();
+ OperatorSubtaskState snapshot1;
+ OperatorSubtaskState snapshot2;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId1 = new OperatorID();
+ OperatorID operatorId2 = new OperatorID();
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.open();
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.open();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), -1L);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-1-1", ImmutableList.of(row1));
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ snapshot1 = harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(1, "hello2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-1-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ snapshot2 = harness2.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // Only notify one of the committers
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(1);
+
+ // Only the first row is committed at this point
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1);
+ }
+
+ // Restore from the given snapshot
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.initializeState(snapshot1);
+ harness1.open();
+
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.initializeState(snapshot2);
+ harness2.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+
+ RowData row1 = SimpleDataUtil.createRowData(2, "world1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-2-1", ImmutableList.of(row1));
+
+ checkpointId++;
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-2-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ harness2.snapshot(checkpointId, ++timestamp);
+
+ assertFlinkManifests(2);
+
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ harness2.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testFlinkManifests() throws Exception {
+ long timestamp = 0;
+ final long checkpoint = 10;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ // harness.processElement(of(dataFile1), ++timestamp);
+ processElement(jobID.toString(), checkpoint, harness, 1, operatorId.toString(), dataFile1);
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ // 1. snapshotState for checkpoint#1
+ harness.snapshot(checkpoint, ++timestamp);
+ List<Path> manifestPaths = assertFlinkManifests(1);
+ Path manifestPath = manifestPaths.get(0);
+ assertThat(manifestPath.getFileName())
+ .asString()
+ .isEqualTo(
+ String.format("%s-%s-%05d-%d-%d-%05d.avro", jobID, operatorId, 0, 0, checkpoint, 1));
+ //
+ // 2. Read the data files from manifests and assert.
+ List<DataFile> dataFiles =
+ FlinkManifestUtil.readDataFiles(
+ createTestingManifestFile(manifestPath), table.io(), table.specs());
+ assertThat(dataFiles).hasSize(1);
+ TestHelpers.assertEquals(dataFile1, dataFiles.get(0));
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(checkpoint);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpoint);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testHandleEndInput() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ testHarness.endInput();
+
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // Future emission calls should change the output
+ testHarness.notifyOfCompletedCheckpoint(2);
+ testHarness.endInput();
+
+ assertThat(testHarness.getOutput()).hasSize(2);
+ }
+
+ @TestTemplate
+ public void testDeleteFiles() throws Exception {
+
+ assumeThat(formatVersion).as("Only support delete in format v2").isGreaterThanOrEqualTo(2);
+ FileAppenderFactory<RowData> appenderFactory = createDeletableAppenderFactory();
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ long checkpointId = 0;
+ RowData row1 = SimpleDataUtil.createInsert(1, "aaa");
+ DataFile dataFile1 = writeDataFile("data-file-1", ImmutableList.of(row1));
+ processElement(jobId, checkpointId, testHarness, 1, "flink-sink", dataFile1);
+
+ testHarness.snapshot(checkpointId, 0);
Review Comment:
again, checkpointId starts from 1
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
Review Comment:
here the new job/operator started without state. shouldn't this test the scenario where new job restore from checkpoint 1 from the previous job? I thought that is a more interesting scenario.
if this test is not intended for that scenario, maybe we can another test for it.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1698256343
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -41,6 +42,19 @@ class ManifestOutputFileFactory {
private final long attemptNumber;
private final AtomicInteger fileCount = new AtomicInteger(0);
+ public ManifestOutputFileFactory(
+ Supplier<Table> tableSupplier,
+ Map<String, String> props,
+ String flinkJobId,
+ String operatorId) {
+ this.tableSupplier = tableSupplier;
+ this.props = props;
+ this.flinkJobId = flinkJobId;
+ this.operatorUniqueId = operatorId;
+ this.subTaskId = 0;
Review Comment:
I think we can use `getRuntimeContext().getAttemptNumber()` from the `IcebergWriteAggregator.open` to get the required parameters, and then we don't need to use hardcoded values.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718753263
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2Base.java:
##########
@@ -373,11 +373,11 @@ private List<Snapshot> findValidSnapshots() {
return validSnapshots;
}
- private StructLikeSet expectedRowSet(Record... records) {
+ protected StructLikeSet expectedRowSet(Record... records) {
Review Comment:
I made the whole thing package private
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719099167
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
Review Comment:
also with `build()` method exposed, `writeParallelism` can be applied. if users have to call the `DataStream#sinkTo(icebergSink)` themselves. The more I think about it, the more I prefer only expose the `append()` method.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "gyfora (via GitHub)" <gi...@apache.org>.
gyfora commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718585951
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
if max parallelism is 1 then we don't need global
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718102942
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
Review Comment:
Maybe something like this (fix my English if applicable):
```
* <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
* generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718257297
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
Review Comment:
nit: Can we have this as the first attribute for the `IcebergSink`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719095758
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
thanks for confirming. Please add a code comment to explain the behavior.
Just to clarify, Flink v2 sink framework propagate the parallelism set in this step to other framework operators (like writers, committers). this may need to be revisited if Flink can support setting committer parallelism (to 1 for Iceberg sink).
```
rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "nastra (via GitHub)" <gi...@apache.org>.
nastra commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1571832123
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkV2Committer.java:
##########
@@ -0,0 +1,808 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.writer.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RunWith(Parameterized.class)
+public class TestSinkV2Committer extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkV2Committer.class);
+ private static final Configuration CONF = new Configuration();
+
+ @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder();
Review Comment:
```suggestion
@TempDir private Path temp;
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1621389662
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
Review Comment:
yeah. `CheckpointCommittableManagerImpl` marks those requests as committed if no retryLater or failure when the commit doesn't throw an exception.
```
@Override
public Collection<CommittableWithLineage<CommT>> commit(
boolean fullyReceived, Committer<CommT> committer)
throws IOException, InterruptedException {
Collection<CommitRequestImpl<CommT>> requests = getPendingRequests(fullyReceived);
requests.forEach(CommitRequestImpl::setSelected);
committer.commit(new ArrayList<>(requests));
requests.forEach(CommitRequestImpl::setCommittedIfNoError);
Collection<CommittableWithLineage<CommT>> committed = drainFinished();
metricGroup.setCurrentPendingCommittablesGauge(() -> getPendingRequests(false).size());
return committed;
}
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1612194617
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
Review Comment:
This restriction would be a regression compared to the old sink implementation. I have seen users having multiple sinks to the same table.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591968579
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
Review Comment:
Please check the comments
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592008844
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.committer.IcebergManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeDataFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readDataFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
+ private static final Configuration CONF = new Configuration();
+
+ // @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
Review Comment:
nit: remove
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591980017
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ // end new
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+ // init
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+ sink.initTable = sink.tableSupplier.get();
+ this.uidPrefix = uidPrefix == null ? "" : uidPrefix;
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ public static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = builder.table.schema();
+ PartitionSpec spec = builder.table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (builder.table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ builder.equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ // end new stuff
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ /**
+ * Sets the currently used committer. Should be used for testing purposes only. Needs to be public
+ * as {@link IcebergSink} is used to test the {@link SinkCommitter} functionality.
+ *
+ * @param newCommitter The committer used for testing
+ */
+ public void setCommitter(Committer<SinkCommittable> newCommitter) {
+ this.sinkV2Committer = newCommitter;
+ }
+
+ // @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer_1() {
Review Comment:
What is this `_1` in the name?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ // end new
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+ // init
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+ sink.initTable = sink.tableSupplier.get();
+ this.uidPrefix = uidPrefix == null ? "" : uidPrefix;
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ public static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = builder.table.schema();
+ PartitionSpec spec = builder.table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (builder.table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ builder.equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ // end new stuff
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ /**
+ * Sets the currently used committer. Should be used for testing purposes only. Needs to be public
+ * as {@link IcebergSink} is used to test the {@link SinkCommitter} functionality.
+ *
+ * @param newCommitter The committer used for testing
+ */
+ public void setCommitter(Committer<SinkCommittable> newCommitter) {
+ this.sinkV2Committer = newCommitter;
+ }
+
+ // @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer_1() {
+ return new SinkCommittableSerializer();
+ }
+
+ // @Override
+ public void addPostCommitTopology_1(
Review Comment:
What is this `_1` in the name?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591993745
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergManifestOutputFileFactory.java:
##########
@@ -0,0 +1,80 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.HasTableOperations;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableOperations;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
+
+public class IcebergManifestOutputFileFactory {
Review Comment:
The `create()` means, that we do not really have the `checkpointId` in the filename anymore.
Could we find a way to get this back?
Also do we have the parameters at hand, to use the original `ManifestOutputFileFactory`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599438514
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergSinkWriter.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.sink.TaskWriterFactory;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * IcebergSink} (SinkV2). Writes out the data to the final place, and emits a single {@link
+ * WriteResult} at every checkpoint for every data/delete file created by this writer.
+ */
+public class IcebergSinkWriter implements CommittingSinkWriter<RowData, WriteResult> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSinkWriter.class);
+
+ private final String fullTableName;
+ private final TaskWriterFactory<RowData> taskWriterFactory;
+ private final IcebergStreamWriterMetrics metrics;
+ private TaskWriter<RowData> writer;
+ private final int subTaskId;
+ private final int attemptId;
+
+ public IcebergSinkWriter(
+ SerializableSupplier<Table> tableSupplier,
+ TaskWriterFactory<RowData> taskWriterFactory,
+ IcebergStreamWriterMetrics metrics,
+ int subTaskId,
+ int attemptId) {
+ this.fullTableName = tableSupplier.get().name();
+ this.taskWriterFactory = taskWriterFactory;
+ // Initialize the task writer factory.
+ taskWriterFactory.initialize(subTaskId, attemptId);
+ // Initialize the task writer.
+ this.writer = taskWriterFactory.create();
+ this.metrics = metrics;
+ this.subTaskId = subTaskId;
+ this.attemptId = attemptId;
+
+ LOG.debug(
+ "StreamWriter created for table {} subtask {} parallelSubtasks {}",
+ fullTableName,
+ subTaskId,
+ attemptId);
+ }
+
+ @Override
+ public void write(RowData element, Context context) throws IOException, InterruptedException {
+ writer.write(element);
+ }
+
+ /** {@link #prepareCommit()} is used to handle flush/endOfInput, so no action is taken here. */
Review Comment:
nit: Could we put this comment to the message body, so we don't duplicate the comment?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1595258499
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,129 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
+ * only {@link WriteResult} objects) to a single {@link SinkCommittable} per checkpoint (storing the
+ * serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<SinkCommittable>, CommittableMessage<SinkCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(SinkAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
Review Comment:
Discussed it with @gyfora, and the solution is correct.
In the `prepareSnapshotPreBarrier` we should emit the aggregated records and then we do not need to have a state
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1698265918
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
It is better to set this to `global` so autoscaler will not try to scale up this operator.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2374304971
> @rodmeneses: I would follow the steps taken by the IcebergTableSource, but instead of `createFLIP27Stream` and `table.exec.iceberg.use-flip27-source` we should find a better name. I remember that we discussed that the choice of the parameters were not the best one. Maybe even V2 could be better in this case. WDYT @stevenzwu ?
Thanks @pvary . That sounds reasonable.
Hi @arkadius. When you said that "everything works", what did you mean? Did you mean that all tests passed? I might be missing something, but I cannot find unit tests covering `IcebergTableSink`. So maybe now it's a good opportunity to also write unit test to cover all these code...
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2374547250
Hi @arkadius . Are you interested to take this work? We would also requires to add unit test to have coverage in the Dynamic Tables flink codebase...
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2371596204
no code has been implemented for this. I see at least 2 options:
1. Implement a brand new `IcebergTableSink` that uses the new `IcebergSink`.
2. Control what underlying sink to use with some config.
thoughts? @pvary
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592006641
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSink.java:
##########
@@ -0,0 +1,399 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+// Like TestFlinkIcebergSink but only using the new IcebergSink
+@RunWith(Parameterized.class)
+public class TestIcebergSink extends TestFlinkIcebergSinkBase {
+
+ @ClassRule
+ public static final MiniClusterWithClientResource MINI_CLUSTER_RESOURCE =
+ MiniClusterResource.createWithClassloaderCheckDisabled();
+
+ @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder();
+
+ @Rule
+ public final HadoopCatalogResource catalogResource =
+ new HadoopCatalogResource(TEMPORARY_FOLDER, TestFixtures.DATABASE, TestFixtures.TABLE);
+
+ private TableLoader tableLoader;
+
+ private final FileFormat format;
+ private final int parallelism;
+ private final boolean partitioned;
+
+ @Parameterized.Parameters(name = "format={0}, parallelism = {1}, partitioned = {2}")
+ public static Object[][] parameters() {
+ return new Object[][] {
+ {"avro", 1, true},
+ {"avro", 1, false},
+ {"avro", 2, true},
+ {"avro", 2, false},
+ {"orc", 1, true},
+ {"orc", 1, false},
+ {"orc", 2, true},
+ {"orc", 2, false},
+ {"parquet", 1, true},
+ {"parquet", 1, false},
+ {"parquet", 2, true},
+ {"parquet", 2, false}
+ };
+ }
+
+ public TestIcebergSink(String format, int parallelism, boolean partitioned) {
+ this.format = FileFormat.fromString(format);
+ this.parallelism = parallelism;
+ this.partitioned = partitioned;
+ }
+
+ @Before
+ public void before() throws IOException {
+ table =
+ catalogResource
+ .catalog()
+ .createTable(
+ TestFixtures.TABLE_IDENTIFIER,
+ SimpleDataUtil.SCHEMA,
+ partitioned
+ ? PartitionSpec.builderFor(SimpleDataUtil.SCHEMA).identity("data").build()
+ : PartitionSpec.unpartitioned(),
+ ImmutableMap.of(TableProperties.DEFAULT_FILE_FORMAT, format.name()));
+
+ env =
+ StreamExecutionEnvironment.getExecutionEnvironment(
+ MiniClusterResource.DISABLE_CLASSLOADER_CHECK_CONFIG)
+ .enableCheckpointing(100)
+ .setParallelism(parallelism)
+ .setMaxParallelism(parallelism);
+
+ tableLoader = catalogResource.tableLoader();
+ }
+
+ @Test
+ public void testWriteRowData() throws Exception {
+ List<Row> rows = Lists.newArrayList(Row.of(1, "hello"), Row.of(2, "world"), Row.of(3, "foo"));
+ DataStream<RowData> dataStream =
+ env.addSource(createBoundedSource(rows), ROW_TYPE_INFO)
+ .map(CONVERTER::toInternal, FlinkCompatibilityUtil.toTypeInfo(SimpleDataUtil.ROW_TYPE));
+
+ IcebergSink.forRowData(dataStream)
+ .table(table)
+ .tableLoader(tableLoader)
+ .writeParallelism(parallelism)
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream");
+
+ // Assert the iceberg table's records.
+ SimpleDataUtil.assertTableRows(table, convertToRowData(rows));
+ }
+
+ private void testWriteRow(TableSchema tableSchema, DistributionMode distributionMode)
+ throws Exception {
+ List<Row> rows = createRows("");
+ DataStream<Row> dataStream = env.addSource(createBoundedSource(rows), ROW_TYPE_INFO);
+
+ IcebergSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(table)
+ .tableLoader(tableLoader)
+ .tableSchema(tableSchema)
+ .writeParallelism(parallelism)
+ .distributionMode(distributionMode)
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream.");
+
+ SimpleDataUtil.assertTableRows(table, convertToRowData(rows));
+ }
+
+ private int partitionFiles(String partition) throws IOException {
+ return SimpleDataUtil.partitionDataFiles(table, ImmutableMap.of("data", partition)).size();
+ }
+
+ @Test
+ public void testWriteRow() throws Exception {
+ testWriteRow(null, DistributionMode.NONE);
+ }
+
+ @Test
+ public void testWriteRowWithTableSchema() throws Exception {
+ testWriteRow(SimpleDataUtil.FLINK_SCHEMA, DistributionMode.NONE);
+ }
+
+ @Test
+ public void testJobNoneDistributeMode() throws Exception {
+ table
+ .updateProperties()
+ .set(TableProperties.WRITE_DISTRIBUTION_MODE, DistributionMode.HASH.modeName())
+ .commit();
+
+ testWriteRow(null, DistributionMode.NONE);
+
+ if (parallelism > 1) {
+ if (partitioned) {
+ int files = partitionFiles("aaa") + partitionFiles("bbb") + partitionFiles("ccc");
+ Assert.assertTrue("Should have more than 3 files in iceberg table.", files > 3);
+ }
+ }
+ }
+
+ @Test
+ public void testJobHashDistributionMode() {
+ table
+ .updateProperties()
+ .set(TableProperties.WRITE_DISTRIBUTION_MODE, DistributionMode.HASH.modeName())
+ .commit();
+
+ Assertions.assertThatThrownBy(() -> testWriteRow(null, DistributionMode.RANGE))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Flink does not support 'range' write distribution mode now.");
+ }
+
+ @Test
+ public void testJobNullDistributionMode() throws Exception {
+ table
+ .updateProperties()
+ .set(TableProperties.WRITE_DISTRIBUTION_MODE, DistributionMode.HASH.modeName())
+ .commit();
+
+ testWriteRow(null, null);
+
+ if (partitioned) {
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'aaa'", 1, partitionFiles("aaa"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'bbb'", 1, partitionFiles("bbb"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'ccc'", 1, partitionFiles("ccc"));
+ }
+ }
+
+ @Test
+ public void testPartitionWriteMode() throws Exception {
+ testWriteRow(null, DistributionMode.HASH);
+ if (partitioned) {
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'aaa'", 1, partitionFiles("aaa"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'bbb'", 1, partitionFiles("bbb"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'ccc'", 1, partitionFiles("ccc"));
+ }
+ }
+
+ @Test
+ public void testShuffleByPartitionWithSchema() throws Exception {
+ testWriteRow(SimpleDataUtil.FLINK_SCHEMA, DistributionMode.HASH);
+ if (partitioned) {
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'aaa'", 1, partitionFiles("aaa"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'bbb'", 1, partitionFiles("bbb"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'ccc'", 1, partitionFiles("ccc"));
+ }
+ }
+
+ @Test
+ public void testTwoSinksInDisjointedDAG() throws Exception {
+ Map<String, String> props = ImmutableMap.of(TableProperties.DEFAULT_FILE_FORMAT, format.name());
+
+ Table leftTable =
+ catalogResource
+ .catalog()
+ .createTable(
+ TableIdentifier.of("left"),
+ SimpleDataUtil.SCHEMA,
+ partitioned
+ ? PartitionSpec.builderFor(SimpleDataUtil.SCHEMA).identity("data").build()
+ : PartitionSpec.unpartitioned(),
+ props);
+ TableLoader leftTableLoader =
+ TableLoader.fromCatalog(catalogResource.catalogLoader(), TableIdentifier.of("left"));
+
+ Table rightTable =
+ catalogResource
+ .catalog()
+ .createTable(
+ TableIdentifier.of("right"),
+ SimpleDataUtil.SCHEMA,
+ partitioned
+ ? PartitionSpec.builderFor(SimpleDataUtil.SCHEMA).identity("data").build()
+ : PartitionSpec.unpartitioned(),
+ props);
+ TableLoader rightTableLoader =
+ TableLoader.fromCatalog(catalogResource.catalogLoader(), TableIdentifier.of("right"));
+
+ env =
+ StreamExecutionEnvironment.getExecutionEnvironment(
+ MiniClusterResource.DISABLE_CLASSLOADER_CHECK_CONFIG)
+ .enableCheckpointing(100)
+ .setParallelism(parallelism)
+ .setMaxParallelism(parallelism);
+ env.getConfig().disableAutoGeneratedUIDs();
+
+ List<Row> leftRows = createRows("left-");
+ DataStream<Row> leftStream =
+ env.fromCollection(leftRows, ROW_TYPE_INFO)
+ .name("leftCustomSource")
+ .uid("leftCustomSource");
+ IcebergSink.forRow(leftStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(leftTable)
+ .tableLoader(leftTableLoader)
+ .tableSchema(SimpleDataUtil.FLINK_SCHEMA)
+ .distributionMode(DistributionMode.NONE)
+ .uidPrefix("leftIcebergSink")
+ .append();
+
+ List<Row> rightRows = createRows("right-");
+ DataStream<Row> rightStream =
+ env.fromCollection(rightRows, ROW_TYPE_INFO)
+ .name("rightCustomSource")
+ .uid("rightCustomSource");
+ IcebergSink.forRow(rightStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(rightTable)
+ .tableLoader(rightTableLoader)
+ .tableSchema(SimpleDataUtil.FLINK_SCHEMA)
+ .writeParallelism(parallelism)
+ .distributionMode(DistributionMode.HASH)
+ .uidPrefix("rightIcebergSink")
+ .setSnapshotProperty("flink.test", TestIcebergSink.class.getName())
+ .setSnapshotProperties(Collections.singletonMap("direction", "rightTable"))
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream.");
+
+ SimpleDataUtil.assertTableRows(leftTable, convertToRowData(leftRows));
+ SimpleDataUtil.assertTableRows(rightTable, convertToRowData(rightRows));
+
+ leftTable.refresh();
+ Assert.assertNull(leftTable.currentSnapshot().summary().get("flink.test"));
+ Assert.assertNull(leftTable.currentSnapshot().summary().get("direction"));
+ rightTable.refresh();
+ Assert.assertEquals(
+ TestIcebergSink.class.getName(), rightTable.currentSnapshot().summary().get("flink.test"));
+ Assert.assertEquals("rightTable", rightTable.currentSnapshot().summary().get("direction"));
+ }
+
+ @Ignore
Review Comment:
Why do we have tests with `@Ignore`?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSink.java:
##########
@@ -0,0 +1,399 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+// Like TestFlinkIcebergSink but only using the new IcebergSink
+@RunWith(Parameterized.class)
+public class TestIcebergSink extends TestFlinkIcebergSinkBase {
+
+ @ClassRule
+ public static final MiniClusterWithClientResource MINI_CLUSTER_RESOURCE =
+ MiniClusterResource.createWithClassloaderCheckDisabled();
+
+ @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder();
+
+ @Rule
+ public final HadoopCatalogResource catalogResource =
+ new HadoopCatalogResource(TEMPORARY_FOLDER, TestFixtures.DATABASE, TestFixtures.TABLE);
+
+ private TableLoader tableLoader;
+
+ private final FileFormat format;
+ private final int parallelism;
+ private final boolean partitioned;
+
+ @Parameterized.Parameters(name = "format={0}, parallelism = {1}, partitioned = {2}")
+ public static Object[][] parameters() {
+ return new Object[][] {
+ {"avro", 1, true},
+ {"avro", 1, false},
+ {"avro", 2, true},
+ {"avro", 2, false},
+ {"orc", 1, true},
+ {"orc", 1, false},
+ {"orc", 2, true},
+ {"orc", 2, false},
+ {"parquet", 1, true},
+ {"parquet", 1, false},
+ {"parquet", 2, true},
+ {"parquet", 2, false}
+ };
+ }
+
+ public TestIcebergSink(String format, int parallelism, boolean partitioned) {
+ this.format = FileFormat.fromString(format);
+ this.parallelism = parallelism;
+ this.partitioned = partitioned;
+ }
+
+ @Before
+ public void before() throws IOException {
+ table =
+ catalogResource
+ .catalog()
+ .createTable(
+ TestFixtures.TABLE_IDENTIFIER,
+ SimpleDataUtil.SCHEMA,
+ partitioned
+ ? PartitionSpec.builderFor(SimpleDataUtil.SCHEMA).identity("data").build()
+ : PartitionSpec.unpartitioned(),
+ ImmutableMap.of(TableProperties.DEFAULT_FILE_FORMAT, format.name()));
+
+ env =
+ StreamExecutionEnvironment.getExecutionEnvironment(
+ MiniClusterResource.DISABLE_CLASSLOADER_CHECK_CONFIG)
+ .enableCheckpointing(100)
+ .setParallelism(parallelism)
+ .setMaxParallelism(parallelism);
+
+ tableLoader = catalogResource.tableLoader();
+ }
+
+ @Test
+ public void testWriteRowData() throws Exception {
+ List<Row> rows = Lists.newArrayList(Row.of(1, "hello"), Row.of(2, "world"), Row.of(3, "foo"));
+ DataStream<RowData> dataStream =
+ env.addSource(createBoundedSource(rows), ROW_TYPE_INFO)
+ .map(CONVERTER::toInternal, FlinkCompatibilityUtil.toTypeInfo(SimpleDataUtil.ROW_TYPE));
+
+ IcebergSink.forRowData(dataStream)
+ .table(table)
+ .tableLoader(tableLoader)
+ .writeParallelism(parallelism)
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream");
+
+ // Assert the iceberg table's records.
+ SimpleDataUtil.assertTableRows(table, convertToRowData(rows));
+ }
+
+ private void testWriteRow(TableSchema tableSchema, DistributionMode distributionMode)
+ throws Exception {
+ List<Row> rows = createRows("");
+ DataStream<Row> dataStream = env.addSource(createBoundedSource(rows), ROW_TYPE_INFO);
+
+ IcebergSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(table)
+ .tableLoader(tableLoader)
+ .tableSchema(tableSchema)
+ .writeParallelism(parallelism)
+ .distributionMode(distributionMode)
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream.");
+
+ SimpleDataUtil.assertTableRows(table, convertToRowData(rows));
+ }
+
+ private int partitionFiles(String partition) throws IOException {
+ return SimpleDataUtil.partitionDataFiles(table, ImmutableMap.of("data", partition)).size();
+ }
+
+ @Test
+ public void testWriteRow() throws Exception {
+ testWriteRow(null, DistributionMode.NONE);
+ }
+
+ @Test
+ public void testWriteRowWithTableSchema() throws Exception {
+ testWriteRow(SimpleDataUtil.FLINK_SCHEMA, DistributionMode.NONE);
+ }
+
+ @Test
+ public void testJobNoneDistributeMode() throws Exception {
+ table
+ .updateProperties()
+ .set(TableProperties.WRITE_DISTRIBUTION_MODE, DistributionMode.HASH.modeName())
+ .commit();
+
+ testWriteRow(null, DistributionMode.NONE);
+
+ if (parallelism > 1) {
+ if (partitioned) {
+ int files = partitionFiles("aaa") + partitionFiles("bbb") + partitionFiles("ccc");
+ Assert.assertTrue("Should have more than 3 files in iceberg table.", files > 3);
+ }
+ }
+ }
+
+ @Test
+ public void testJobHashDistributionMode() {
+ table
+ .updateProperties()
+ .set(TableProperties.WRITE_DISTRIBUTION_MODE, DistributionMode.HASH.modeName())
+ .commit();
+
+ Assertions.assertThatThrownBy(() -> testWriteRow(null, DistributionMode.RANGE))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Flink does not support 'range' write distribution mode now.");
+ }
+
+ @Test
+ public void testJobNullDistributionMode() throws Exception {
+ table
+ .updateProperties()
+ .set(TableProperties.WRITE_DISTRIBUTION_MODE, DistributionMode.HASH.modeName())
+ .commit();
+
+ testWriteRow(null, null);
+
+ if (partitioned) {
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'aaa'", 1, partitionFiles("aaa"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'bbb'", 1, partitionFiles("bbb"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'ccc'", 1, partitionFiles("ccc"));
+ }
+ }
+
+ @Test
+ public void testPartitionWriteMode() throws Exception {
+ testWriteRow(null, DistributionMode.HASH);
+ if (partitioned) {
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'aaa'", 1, partitionFiles("aaa"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'bbb'", 1, partitionFiles("bbb"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'ccc'", 1, partitionFiles("ccc"));
+ }
+ }
+
+ @Test
+ public void testShuffleByPartitionWithSchema() throws Exception {
+ testWriteRow(SimpleDataUtil.FLINK_SCHEMA, DistributionMode.HASH);
+ if (partitioned) {
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'aaa'", 1, partitionFiles("aaa"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'bbb'", 1, partitionFiles("bbb"));
+ Assert.assertEquals(
+ "There should be only 1 data file in partition 'ccc'", 1, partitionFiles("ccc"));
+ }
+ }
+
+ @Test
+ public void testTwoSinksInDisjointedDAG() throws Exception {
+ Map<String, String> props = ImmutableMap.of(TableProperties.DEFAULT_FILE_FORMAT, format.name());
+
+ Table leftTable =
+ catalogResource
+ .catalog()
+ .createTable(
+ TableIdentifier.of("left"),
+ SimpleDataUtil.SCHEMA,
+ partitioned
+ ? PartitionSpec.builderFor(SimpleDataUtil.SCHEMA).identity("data").build()
+ : PartitionSpec.unpartitioned(),
+ props);
+ TableLoader leftTableLoader =
+ TableLoader.fromCatalog(catalogResource.catalogLoader(), TableIdentifier.of("left"));
+
+ Table rightTable =
+ catalogResource
+ .catalog()
+ .createTable(
+ TableIdentifier.of("right"),
+ SimpleDataUtil.SCHEMA,
+ partitioned
+ ? PartitionSpec.builderFor(SimpleDataUtil.SCHEMA).identity("data").build()
+ : PartitionSpec.unpartitioned(),
+ props);
+ TableLoader rightTableLoader =
+ TableLoader.fromCatalog(catalogResource.catalogLoader(), TableIdentifier.of("right"));
+
+ env =
+ StreamExecutionEnvironment.getExecutionEnvironment(
+ MiniClusterResource.DISABLE_CLASSLOADER_CHECK_CONFIG)
+ .enableCheckpointing(100)
+ .setParallelism(parallelism)
+ .setMaxParallelism(parallelism);
+ env.getConfig().disableAutoGeneratedUIDs();
+
+ List<Row> leftRows = createRows("left-");
+ DataStream<Row> leftStream =
+ env.fromCollection(leftRows, ROW_TYPE_INFO)
+ .name("leftCustomSource")
+ .uid("leftCustomSource");
+ IcebergSink.forRow(leftStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(leftTable)
+ .tableLoader(leftTableLoader)
+ .tableSchema(SimpleDataUtil.FLINK_SCHEMA)
+ .distributionMode(DistributionMode.NONE)
+ .uidPrefix("leftIcebergSink")
+ .append();
+
+ List<Row> rightRows = createRows("right-");
+ DataStream<Row> rightStream =
+ env.fromCollection(rightRows, ROW_TYPE_INFO)
+ .name("rightCustomSource")
+ .uid("rightCustomSource");
+ IcebergSink.forRow(rightStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(rightTable)
+ .tableLoader(rightTableLoader)
+ .tableSchema(SimpleDataUtil.FLINK_SCHEMA)
+ .writeParallelism(parallelism)
+ .distributionMode(DistributionMode.HASH)
+ .uidPrefix("rightIcebergSink")
+ .setSnapshotProperty("flink.test", TestIcebergSink.class.getName())
+ .setSnapshotProperties(Collections.singletonMap("direction", "rightTable"))
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream.");
+
+ SimpleDataUtil.assertTableRows(leftTable, convertToRowData(leftRows));
+ SimpleDataUtil.assertTableRows(rightTable, convertToRowData(rightRows));
+
+ leftTable.refresh();
+ Assert.assertNull(leftTable.currentSnapshot().summary().get("flink.test"));
+ Assert.assertNull(leftTable.currentSnapshot().summary().get("direction"));
+ rightTable.refresh();
+ Assert.assertEquals(
+ TestIcebergSink.class.getName(), rightTable.currentSnapshot().summary().get("flink.test"));
+ Assert.assertEquals("rightTable", rightTable.currentSnapshot().summary().get("direction"));
+ }
+
+ @Ignore
+ @Test
+ public void testOverrideWriteConfigWithUnknownDistributionMode() {
+ Map<String, String> newProps = Maps.newHashMap();
+ newProps.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), "UNRECOGNIZED");
+
+ List<Row> rows = createRows("");
+ DataStream<Row> dataStream = env.addSource(createBoundedSource(rows), ROW_TYPE_INFO);
+
+ IcebergSink.Builder builder =
+ IcebergSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(table)
+ .tableLoader(tableLoader)
+ .writeParallelism(parallelism)
+ .setAll(newProps);
+
+ Assertions.assertThatThrownBy(builder::append)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid distribution mode: UNRECOGNIZED");
+ }
+
+ @Ignore
Review Comment:
Why do we have tests with `@Ignore`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599436116
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,439 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ private final TableLoader tableLoader;
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.tableLoader = tableLoader;
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ LOG.info("Noting to commit");
Review Comment:
Or just remove this, and leave it to the flow to handle this directly?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1600206083
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2.java:
##########
@@ -140,8 +140,8 @@ public void testCheckAndGetEqualityFieldIds() {
DataStream<Row> dataStream =
env.addSource(new BoundedTestSource<>(ImmutableList.of()), ROW_TYPE_INFO);
- FlinkSink.Builder builder =
- FlinkSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
+ IcebergSink.Builder builder =
Review Comment:
There was already a unit test file called `TestFlinkIcebergSink`. So I cloned its changed it to use `IcebergSink` and rename the file accordingly. This same applies to `TestFlinkIcebergSinkV2Base`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599432484
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommittableSerializer.java:
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+
+/**
+ * This serializer is used for serializing the {@link SinkCommittable} objects between the Writer
+ * and the Aggregator operator and between the Aggregator and the Committer as well.
+ *
+ * <p>In both cases only the respective part is serialized.
+ */
+public class SinkCommittableSerializer implements SimpleVersionedSerializer<SinkCommittable> {
+ private static final int VERSION_1 = 1;
+
+ @Override
+ public int getVersion() {
+ return VERSION_1;
+ }
+
+ @Override
+ public byte[] serialize(SinkCommittable committable) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+ view.writeUTF(committable.jobId());
Review Comment:
Add the version to the serialized byte array too, and read it back
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591405651
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/CachingTableSupplier.java:
##########
@@ -33,7 +34,8 @@
* table loader should be used carefully when used with writer tasks. It could result in heavy load
* on a catalog for jobs with many writers.
*/
-class CachingTableSupplier implements SerializableSupplier<Table> {
+@Internal
+public class CachingTableSupplier implements SerializableSupplier<Table> {
Review Comment:
My mistake
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/RowDataTaskWriterFactory.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.iceberg.flink.sink.writer;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionKey;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.RowDataWrapper;
+import org.apache.iceberg.flink.sink.CachingTableSupplier;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.OutputFileFactory;
+import org.apache.iceberg.io.PartitionedFanoutWriter;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.UnpartitionedWriter;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.ArrayUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+
+public class RowDataTaskWriterFactory implements TaskWriterFactory<RowData> {
+ private final Supplier<Table> tableSupplier;
+ private final Schema schema;
+ private final RowType flinkSchema;
+ private final PartitionSpec spec;
+ private final long targetFileSizeBytes;
+ private final FileFormat format;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsert;
+ private final FileAppenderFactory<RowData> appenderFactory;
+
+ private transient OutputFileFactory outputFileFactory;
+
+ public RowDataTaskWriterFactory(
+ Table table,
+ RowType flinkSchema,
+ long targetFileSizeBytes,
+ FileFormat format,
+ Map<String, String> writeProperties,
+ List<Integer> equalityFieldIds,
+ boolean upsert) {
+ this(
+ () -> table,
+ flinkSchema,
+ targetFileSizeBytes,
+ format,
+ writeProperties,
+ equalityFieldIds,
+ upsert);
+ }
+
+ public RowDataTaskWriterFactory(
+ SerializableSupplier<Table> tableSupplier,
+ RowType flinkSchema,
+ long targetFileSizeBytes,
+ FileFormat format,
+ Map<String, String> writeProperties,
+ List<Integer> equalityFieldIds,
+ boolean upsert) {
+ this.tableSupplier = tableSupplier;
+
+ Table table;
+ if (tableSupplier instanceof CachingTableSupplier) {
Review Comment:
My mistake
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626400067
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
Review Comment:
operator ID not available in this class constructor. The caller of this constructor ir passing in `sinkId` in the prefix arg. So I will rename `prefix` to be `sinkId` and remove `prefix` as local variable which will not needed anymore. I will also remove local var `workerPoolSize` which is also only used in `IcebergCommitter` constructor
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587797538
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/CachingTableSupplier.java:
##########
@@ -33,7 +34,8 @@
* table loader should be used carefully when used with writer tasks. It could result in heavy load
* on a catalog for jobs with many writers.
*/
-class CachingTableSupplier implements SerializableSupplier<Table> {
+@Internal
+public class CachingTableSupplier implements SerializableSupplier<Table> {
Review Comment:
I would like to avoid this. Seems like a bad idea
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1705625534
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSink.java:
##########
@@ -0,0 +1,395 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.sink.IcebergSink.Builder;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
Review Comment:
The refactor needed is not worth the effort IMO, so let's proceed with what we have. Thanks!!
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1707839978
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
Review Comment:
how do we set uid for the operators created by the v2 sink framework like writer and committer operators?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718123448
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -21,18 +21,19 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
+import org.apache.flink.annotation.Internal;
import org.apache.iceberg.FileFormat;
import org.apache.iceberg.HasTableOperations;
import org.apache.iceberg.Table;
import org.apache.iceberg.TableOperations;
import org.apache.iceberg.io.OutputFile;
import org.apache.iceberg.relocated.com.google.common.base.Strings;
-class ManifestOutputFileFactory {
+@Internal
+public class ManifestOutputFileFactory {
// Users could define their own flink manifests directory by setting this value in table
// properties.
- static final String FLINK_MANIFEST_LOCATION = "flink.manifests.location";
-
+ public static final String FLINK_MANIFEST_LOCATION = "flink.manifests.location";
Review Comment:
Why is this public?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718097213
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/FlinkManifestUtil.java:
##########
@@ -33,15 +33,19 @@
import org.apache.iceberg.io.FileIO;
import org.apache.iceberg.io.OutputFile;
import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-class FlinkManifestUtil {
+public class FlinkManifestUtil {
Review Comment:
nit: Do we want to add `@Internal` here as well?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714415965
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
Review Comment:
TBH, I'm not sure if this comment actually makes sense or not. all we're doing in this test, is asserting that data committed in the previous job, can be read back in a new job. I changed it
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719087855
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
Review Comment:
I did a debugging session, where I could verify that the framework created operators were prefixing a value to the iceberg sink uid. In this example, the sink uid was "meneses" and the below picture was for the `Sink Committer` operator

In this way, the Sink UID is actually being propagated to all of these operators, which is what we wanted.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719099167
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
Review Comment:
also with `build()` method exposed, `writeParallelism` and 'uidsuffix` can't be applied, if users have to call the `DataStream#sinkTo(icebergSink)` themselves. The more I think about it, the more I prefer only expose the `append()` method.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1723555383
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
Review Comment:
I will have to revisit and may be change this method, as we are wanting now to prefix `"Sink pre-commit aggregator: "`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu merged PR #10179:
URL: https://github.com/apache/iceberg/pull/10179
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "mamineturki (via GitHub)" <gi...@apache.org>.
mamineturki commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2317263860
Hi, when will this change be released please?
Thanks,
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "jtchen-study (via GitHub)" <gi...@apache.org>.
jtchen-study commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1607483056
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
Review Comment:
how to solve small file issue ,if this part is not completed
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1594360116
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergFlinkManifestUtil.java:
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+
+public class IcebergFlinkManifestUtil {
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ private IcebergFlinkManifestUtil() {}
+
+ static ManifestFile writeDataFiles(
Review Comment:
refactored and only left `createOutputFileFactory` which is used from different places
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1595721315
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkV2Committer.java:
##########
@@ -0,0 +1,808 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.writer.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RunWith(Parameterized.class)
Review Comment:
HI, @nastra:
I will have all _new_ unit tests using JUnit 5.
All the others we can update in another PR, once this is merged.
What do you think?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1593408257
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
Review Comment:
Missed this on
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
Review Comment:
Missed this one
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2109381711
Looks promising to me. @stevenzwu, could you please review too?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591981306
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ // end new
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+ // init
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+ sink.initTable = sink.tableSupplier.get();
+ this.uidPrefix = uidPrefix == null ? "" : uidPrefix;
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ public static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = builder.table.schema();
+ PartitionSpec spec = builder.table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (builder.table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ builder.equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ // end new stuff
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ /**
+ * Sets the currently used committer. Should be used for testing purposes only. Needs to be public
+ * as {@link IcebergSink} is used to test the {@link SinkCommitter} functionality.
+ *
+ * @param newCommitter The committer used for testing
+ */
+ public void setCommitter(Committer<SinkCommittable> newCommitter) {
+ this.sinkV2Committer = newCommitter;
+ }
+
+ // @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer_1() {
+ return new SinkCommittableSerializer();
+ }
+
+ // @Override
+ public void addPostCommitTopology_1(
+ DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from generic input data stream into iceberg
+ * table. We use {@link RowData} inside the sink connector, so users need to provide a mapper
+ * function and a {@link TypeInformation} to convert those generic records to a RowData
+ * DataStream.
+ *
+ * @param input the generic source input data stream.
+ * @param mapper function to convert the generic data to {@link RowData}
+ * @param outputType to define the {@link TypeInformation} for the input data.
+ * @param <T> the data type of records.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static <T> IcebergSink.Builder builderFor(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ return new Builder().forMapperOutputType(input, mapper, outputType);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link Row}s into
+ * iceberg table. We use {@link RowData} inside the sink connector, so users need to provide a
+ * {@link TableSchema} for builder to convert those {@link Row}s to a {@link RowData} DataStream.
+ *
+ * @param input the source input data stream with {@link Row}s.
+ * @param tableSchema defines the {@link TypeInformation} for input data.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static IcebergSink.Builder forRow(DataStream<Row> input, TableSchema tableSchema) {
+ return new Builder().forRow(input, tableSchema);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link RowData}s
+ * into iceberg table.
+ *
+ * @param input the source input data stream with {@link RowData}s.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static IcebergSink.Builder forRowData(DataStream<RowData> input) {
+ return new Builder().forRowData(input);
+ }
+
+ static String prefixIfNotNull(String uidPrefix, String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ /**
+ * A table loader that will only reload a table after a certain interval has passed. WARNING: This
+ * table loader should be used carefully when used with writer tasks. It could result in heavy
+ * load on a catalog for jobs with many writers.
+ */
+ static class SimpleTableSupplier implements SerializableSupplier<Table> {
Review Comment:
Why is this not private?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1621395230
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
Review Comment:
I understand this is needed. it is unfortunate that the new sink V2 API enforce unnecessary check/overhead to Iceberg sink. we don't have to resolve it in this PR. but it is sth worth follow up to see if we can avoid it.
New sink V2 tried to hide a lot things from connectors, which also result in this side effect that connectors lose control.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1624900416
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -41,6 +42,16 @@ class ManifestOutputFileFactory {
private final long attemptNumber;
private final AtomicInteger fileCount = new AtomicInteger(0);
+ public ManifestOutputFileFactory(
+ Supplier<Table> tableSupplier, Map<String, String> props, String prefix) {
Review Comment:
is `prefix` used here?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<IcebergCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
Review Comment:
do we need to remember `builder` like here?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommittableSerializer.java:
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+
+/**
+ * This serializer is used for serializing the {@link IcebergCommittable} objects between the Writer
+ * and the Aggregator operator and between the Aggregator and the Committer as well.
+ *
+ * <p>In both cases only the respective part is serialized.
+ */
+public class IcebergCommittableSerializer implements SimpleVersionedSerializer<IcebergCommittable> {
+ private static final int VERSION = 1;
+
+ @Override
+ public int getVersion() {
+ return VERSION;
+ }
+
+ @Override
+ public byte[] serialize(IcebergCommittable committable) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+ view.writeUTF(committable.jobId());
+ view.writeUTF(committable.operatorId());
+ view.writeLong(committable.checkpointId());
+ view.writeInt(committable.manifest().length);
+ view.write(committable.manifest());
+ return out.toByteArray();
+ }
+
+ @Override
+ public IcebergCommittable deserialize(int version, byte[] serialized) throws IOException {
+ if (version == VERSION) {
Review Comment:
nit: typically use the constant `1` here, as VERSION is just the latest/default version. see example from Flink's `FileSourceSplitSerializer`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+
+ this.table = tableLoader.loadTable();
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(() -> table, table.properties(), prefix);
+ }
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<IcebergCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
+ results.clear();
+ }
+
+ /**
+ * Write all the completed data files to a newly created manifest file and return the manifest's
+ * avro serialized bytes.
+ */
+ public byte[] writeToManifest(Collection<WriteResult> writeResults, long checkpointId)
+ throws IOException {
+ if (writeResults.isEmpty()) {
+ return EMPTY_MANIFEST_DATA;
+ }
+
+ WriteResult result = WriteResult.builder().addAll(writeResults).build();
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ result, () -> icebergManifestOutputFileFactory.create(checkpointId), table.spec());
+
+ return SimpleVersionedSerialization.writeVersionAndSerialize(
+ DeltaManifestsSerializer.INSTANCE, deltaManifests);
+ }
+
+ /**
+ * Write the {@link WriteResult} to temporary manifest files.
+ *
+ * @param result all those DataFiles/DeleteFiles in this WriteResult should be written with same
+ * partition spec
+ */
+ public static DeltaManifests writeCompletedFiles(
Review Comment:
this static util method already exists in `FlinkManifestUtil`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
Review Comment:
this.prefix doesn't seem to be used later on. it seems `prefix` is only used as worker pool size. technically, it is not used as `prefix`.
old code use operator ID in the worker pool name. I guess it is not obtainable in the v2 sink?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
Review Comment:
let's mark it as `@Experimental` to star with
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link IcebergCommittable} (jobId/operatorId/checkpointId). Only used by
+ * {@link IcebergCommittable} (SinkV2).
+ *
+ * @param requests The {@link IcebergCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> requests) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ String snapshotCheckpointId = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (snapshotCheckpointId != null) {
+ long checkpointId = Long.parseLong(snapshotCheckpointId);
+ CommitRequest<IcebergCommittable> request = requests.get(checkpointId);
+ if (request != null
+ && request.getCommittable().jobId().equals(snapshotFlinkJobId)
+ && request.getCommittable().operatorId().equals(snapshotOperatorId)) {
+ lastCommittedCheckpointId = checkpointId;
+ break;
+ }
+ }
+
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param deltaManifestsMap The checkpointId to {@link DeltaManifests} map of the changes to
+ * commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param checkpointId The checkpointId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ public void commitUpToCheckpoint(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ NavigableMap<Long, byte[]> deltaManifestsMap,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId)
+ throws IOException {
+ NavigableMap<Long, byte[]> pendingMap = deltaManifestsMap.headMap(checkpointId, true);
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, byte[]> e : pendingMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue())) {
+ // Skip the empty flink manifest.
+ continue;
+ }
+
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue());
+ pendingResults.put(
+ e.getKey(),
+ FlinkManifestUtil.readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(
+ commitRequestMap, pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+ pendingMap.clear();
+ deleteCommittedManifests(manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(
+ commitRequestMap, pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ } else {
+ commitDeltaTxn(
+ commitRequestMap, pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ commitRequestMap,
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ // fail all commits as really its only one
+ commitOperation(
+ commitRequestMap,
+ appendFiles,
+ summary,
+ "append",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(
+ commitRequestMap, rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
+ }
+ }
+ }
+
+ private void commitOperation(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ SnapshotUpdate<?> operation,
+ CommitSummary summary,
+ String description,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ try {
+ LOG.info(
+ "Committing {} for checkpoint {} to table {} branch {} with summary: {}",
+ description,
+ checkpointId,
+ table.name(),
+ branch,
+ summary);
+ snapshotProperties.forEach(operation::set);
+ // custom snapshot metadata properties will be overridden if they conflict with internal ones
+ // used by the sink.
+ operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
+ operation.set(FLINK_JOB_ID, newFlinkJobId);
+ operation.set(OPERATOR_ID, operatorId);
+ operation.toBranch(branch);
+
+ long startNano = System.nanoTime();
+ operation.commit(); // abort is automatically called if this fails.
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano);
+ LOG.info(
+ "Committed {} to table: {}, branch: {}, checkpointId {} in {} ms",
+ description,
+ table.name(),
+ branch,
+ checkpointId,
+ durationMs);
+ if (committerMetrics != null) {
+ committerMetrics.commitDuration(durationMs);
+ }
+ } catch (Exception e) {
+ commitRequestMap.tailMap(checkpointId, true).values().forEach(CommitRequest::retryLater);
+ }
+ }
+
+ private void deleteCommittedManifests(
Review Comment:
nit: this can be made a static util method for reuse if table io is passed in
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -41,6 +42,16 @@ class ManifestOutputFileFactory {
private final long attemptNumber;
private final AtomicInteger fileCount = new AtomicInteger(0);
+ public ManifestOutputFileFactory(
+ Supplier<Table> tableSupplier, Map<String, String> props, String prefix) {
+ this.tableSupplier = tableSupplier;
+ this.props = props;
+ this.flinkJobId = null;
Review Comment:
this can be problematic. see the `generatePath` method.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
Review Comment:
nit: mark as `@Internal`? same for other classes in this sub package?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link IcebergCommittable} (jobId/operatorId/checkpointId). Only used by
+ * {@link IcebergCommittable} (SinkV2).
+ *
+ * @param requests The {@link IcebergCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
Review Comment:
this was a static method from the old code. can we reuse it here?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/WriteResultSerializer.java:
##########
@@ -0,0 +1,63 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+import org.apache.flink.util.InstantiationUtil;
+import org.apache.iceberg.io.WriteResult;
+
+public class WriteResultSerializer implements SimpleVersionedSerializer<WriteResult> {
+ private static final int VERSION_0 = 0;
Review Comment:
for consistency, `VERSION` marks the current/latest version as returned by `getVersion`. deserialize use constants directly for all supported versions.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<IcebergCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
Review Comment:
it is probably better to keep it next to the `.global()` than method Javadoc
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<IcebergCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
Review Comment:
curious why some class members are final and set in the constructor here while others are final and set in the builder?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
Review Comment:
let's also consider another scenario where the commit requests may accumulated over multiple jobs (after checkpoint and restart). what should be the expected behavior?
also noticed that in v1 case multiple commitable can be collapsed to a single commit to Iceberg, while in v2 table they are committed separately and sequentially.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
Review Comment:
why do we add an empty commit request in this case? can the method simply return here?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<IcebergCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
Review Comment:
is this style common? builder modify the target object state directly? should those be done in the sink constructor (instead of Builder.build method)? This way, the class members can be final.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/WriteResultSerializer.java:
##########
@@ -0,0 +1,63 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+import org.apache.flink.util.InstantiationUtil;
+import org.apache.iceberg.io.WriteResult;
+
+public class WriteResultSerializer implements SimpleVersionedSerializer<WriteResult> {
+ private static final int VERSION_0 = 0;
+
+ @Override
+ public int getVersion() {
+ return VERSION_0;
+ }
+
+ @Override
+ public byte[] serialize(WriteResult writeResult) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+ byte[] result = InstantiationUtil.serializeObject(writeResult);
Review Comment:
why is this resolved without response?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+
+ this.table = tableLoader.loadTable();
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(() -> table, table.properties(), prefix);
+ }
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<IcebergCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
+ results.clear();
+ }
+
+ /**
+ * Write all the completed data files to a newly created manifest file and return the manifest's
+ * avro serialized bytes.
+ */
+ public byte[] writeToManifest(Collection<WriteResult> writeResults, long checkpointId)
+ throws IOException {
+ if (writeResults.isEmpty()) {
+ return EMPTY_MANIFEST_DATA;
+ }
+
+ WriteResult result = WriteResult.builder().addAll(writeResults).build();
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ result, () -> icebergManifestOutputFileFactory.create(checkpointId), table.spec());
+
+ return SimpleVersionedSerialization.writeVersionAndSerialize(
+ DeltaManifestsSerializer.INSTANCE, deltaManifests);
+ }
+
+ /**
+ * Write the {@link WriteResult} to temporary manifest files.
+ *
+ * @param result all those DataFiles/DeleteFiles in this WriteResult should be written with same
+ * partition spec
+ */
+ public static DeltaManifests writeCompletedFiles(
+ WriteResult result, Supplier<OutputFile> outputFileSupplier, PartitionSpec spec)
+ throws IOException {
+
+ ManifestFile dataManifest = null;
+ ManifestFile deleteManifest = null;
+
+ // Write the completed data files into a newly created data manifest file.
+ if (result.dataFiles() != null && result.dataFiles().length > 0) {
+ dataManifest =
+ FlinkManifestUtil.writeDataFiles(
+ outputFileSupplier.get(), spec, Lists.newArrayList(result.dataFiles()));
+ }
+
+ // Write the completed delete files into a newly created delete manifest file.
+ if (result.deleteFiles() != null && result.deleteFiles().length > 0) {
+ OutputFile deleteManifestFile = outputFileSupplier.get();
+
+ ManifestWriter<DeleteFile> deleteManifestWriter =
+ ManifestFiles.writeDeleteManifest(FORMAT_V2, spec, deleteManifestFile, DUMMY_SNAPSHOT_ID);
+ try (ManifestWriter<DeleteFile> writer = deleteManifestWriter) {
+ for (DeleteFile deleteFile : result.deleteFiles()) {
+ writer.add(deleteFile);
+ }
+ }
+
+ deleteManifest = deleteManifestWriter.toManifestFile();
+ }
+
+ return new DeltaManifests(dataManifest, deleteManifest, result.referencedDataFiles());
+ }
+
+ @Override
+ public void processElement(StreamRecord<CommittableMessage<WriteResult>> element)
+ throws Exception {
+ if (element.isRecord() && element.getValue() instanceof CommittableWithLineage) {
Review Comment:
I think this check `element.getValue() instanceof CommittableWithLineage` probably should be moved inside the if section with a `Preconditions` check. otherwise, if upstream changed, we can fail silently here.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
Review Comment:
should we use Flink's `CheckpointStoreUtil#INVALID_CHECKPOINT_ID` instead?
`INITIAL` is also not accurate here. technically, initial checkpoint id is `1` as defined in Flink code
<img width="476" alt="image" src="https://github.com/apache/iceberg/assets/1545663/bd5037ec-1138-415f-bd65-a4e4be0159d0">
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
Review Comment:
I am wondering if it is correct to use the `jobId` and `operatorId` from the committable or it should be the current running job? Imagine the scenario where the commitable was restored from a checkpoint/savepoint from a previous job.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
Review Comment:
thinking about it again, current behavior seems correct with the way `getMaxCommittedCheckpointId` is implemented
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1690515381
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSinkWriter.java:
##########
@@ -0,0 +1,116 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * org.apache.iceberg.flink.sink.IcebergSink} (SinkV2). Writes out the data to the final place, and
+ * emits a single {@link WriteResult} at every checkpoint for every data/delete file created by this
+ * writer.
+ */
+class IcebergSinkWriter implements CommittingSinkWriter<RowData, WriteResult> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSinkWriter.class);
+
+ private final String fullTableName;
+ private final TaskWriterFactory<RowData> taskWriterFactory;
+ private final IcebergStreamWriterMetrics metrics;
+ private TaskWriter<RowData> writer;
+ private final int subTaskId;
+ private final int attemptId;
+
+ IcebergSinkWriter(
+ SerializableSupplier<Table> tableSupplier,
+ TaskWriterFactory<RowData> taskWriterFactory,
+ IcebergStreamWriterMetrics metrics,
+ int subTaskId,
+ int attemptId) {
+ this.fullTableName = tableSupplier.get().name();
+ this.taskWriterFactory = taskWriterFactory;
+ // Initialize the task writer factory.
+ taskWriterFactory.initialize(subTaskId, attemptId);
+ // Initialize the task writer.
+ this.writer = taskWriterFactory.create();
+ this.metrics = metrics;
+ this.subTaskId = subTaskId;
+ this.attemptId = attemptId;
+
+ LOG.debug(
+ "StreamWriter created for table {} subtask {} parallelSubtasks {}",
Review Comment:
`parallelSubtasks` is not correct here.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,350 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-committer-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
Review Comment:
nit: unnecessary empty line. applicable to line 140 and line 146 below too
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,350 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-committer-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
Review Comment:
nit: unnecessary empty line.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+@Internal
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+ String flinkJobId = getContainingTask().getEnvironment().getJobID().toString();
+ String operatorId = getOperatorID().toString();
+ this.table = tableLoader.loadTable();
+
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(
+ () -> table, table.properties(), flinkJobId, operatorId);
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<IcebergCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
Review Comment:
`message` doesn't implement `toString`
nit: `Emitted commit message to downstream committer operator`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,350 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-committer-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ long maxCommittedCheckpointId =
+ getMaxCommittedCheckpointId(table, last.jobId(), last.operatorId(), branch);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> uncommitted =
+ commitRequestMap.tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitPendingRequests(uncommitted, last.jobId(), last.operatorId());
+ }
+ }
+
+ public static long getMaxCommittedCheckpointId(
+ Table table, String flinkJobId, String operatorId, String branch) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ if (flinkJobId.equals(snapshotFlinkJobId)
+ && (snapshotOperatorId == null || snapshotOperatorId.equals(operatorId))) {
+ String value = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (value != null) {
+ lastCommittedCheckpointId = Long.parseLong(value);
+ break;
+ }
+ }
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param commitRequestMap The checkpointId to {@link CommitRequest} map of the changes to commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ private void commitPendingRequests(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ String newFlinkJobId,
+ String operatorId)
+ throws IOException {
+ long checkpointId = commitRequestMap.lastKey();
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, CommitRequest<IcebergCommittable>> e : commitRequestMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue().getCommittable().manifest())) {
+ pendingResults.put(e.getKey(), EMPTY_WRITE_RESULT);
+ } else {
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue().getCommittable().manifest());
+ pendingResults.put(
+ e.getKey(),
+ FlinkManifestUtil.readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+
+ FlinkManifestUtil.deleteCommittedManifests(table, manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ long checkpointId = pendingResults.lastKey();
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ // fail all commits as really its only one
+ commitOperation(appendFiles, summary, "append", newFlinkJobId, operatorId, checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
+ }
+ }
+ }
+
+ private void commitOperation(
+ SnapshotUpdate<?> operation,
+ CommitSummary summary,
+ String description,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+
+ LOG.info(
+ "Committing {} for checkpoint {} to table {} branch {} with summary: {}",
+ description,
+ checkpointId,
+ table.name(),
+ branch,
+ summary);
+ snapshotProperties.forEach(operation::set);
+ // custom snapshot metadata properties will be overridden if they conflict with internal ones
+ // used by the sink.
+ operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
+ operation.set(FLINK_JOB_ID, newFlinkJobId);
+ operation.set(OPERATOR_ID, operatorId);
+ operation.toBranch(branch);
+
+ long startNano = System.nanoTime();
+ operation.commit(); // abort is automatically called if this fails.
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano);
+ LOG.info(
+ "Committed {} to table: {}, branch: {}, checkpointId {} in {} ms",
+ description,
+ table.name(),
+ branch,
+ checkpointId,
+ durationMs);
+ if (committerMetrics != null) {
+ committerMetrics.commitDuration(durationMs);
+ }
+ }
+
+ @Override
+ public void close() {
+ // Nothing to do
Review Comment:
tableLoader is not closed after usage. we can either close it here or right after the usage.
also noticed that table was reloaded after each commit in PR #8555 in `IcebergFilesCommitter`. Don't know if that is really needed. @bryanck any comment?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+@Internal
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+ String flinkJobId = getContainingTask().getEnvironment().getJobID().toString();
+ String operatorId = getOperatorID().toString();
+ this.table = tableLoader.loadTable();
+
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(
+ () -> table, table.properties(), flinkJobId, operatorId);
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
Review Comment:
nit: move this after line 97 (closer to where it is used)
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSinkWriter.java:
##########
@@ -0,0 +1,116 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * org.apache.iceberg.flink.sink.IcebergSink} (SinkV2). Writes out the data to the final place, and
+ * emits a single {@link WriteResult} at every checkpoint for every data/delete file created by this
+ * writer.
+ */
+class IcebergSinkWriter implements CommittingSinkWriter<RowData, WriteResult> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSinkWriter.class);
+
+ private final String fullTableName;
+ private final TaskWriterFactory<RowData> taskWriterFactory;
+ private final IcebergStreamWriterMetrics metrics;
+ private TaskWriter<RowData> writer;
+ private final int subTaskId;
+ private final int attemptId;
+
+ IcebergSinkWriter(
+ SerializableSupplier<Table> tableSupplier,
+ TaskWriterFactory<RowData> taskWriterFactory,
+ IcebergStreamWriterMetrics metrics,
+ int subTaskId,
+ int attemptId) {
+ this.fullTableName = tableSupplier.get().name();
Review Comment:
it seems that `tableSupplier` is only used to get full table name. since the sink object already loaded the table, why don't we pass the full table name string directly?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -41,6 +42,19 @@ class ManifestOutputFileFactory {
private final long attemptNumber;
private final AtomicInteger fileCount = new AtomicInteger(0);
+ public ManifestOutputFileFactory(
+ Supplier<Table> tableSupplier,
+ Map<String, String> props,
+ String flinkJobId,
+ String operatorId) {
+ this.tableSupplier = tableSupplier;
+ this.props = props;
+ this.flinkJobId = flinkJobId;
+ this.operatorUniqueId = operatorId;
+ this.subTaskId = 0;
Review Comment:
does this constructor make sense? is this correct? note that subTaskId and attemptNumber were used in file name patterns in this class.
```
private String generatePath(long checkpointId) {
return FileFormat.AVRO.addExtension(
String.format(
"%s-%s-%05d-%d-%d-%05d",
flinkJobId,
operatorUniqueId,
subTaskId,
attemptNumber,
checkpointId,
fileCount.incrementAndGet()));
```
On paper, setting them to 0 can cause collisions and overrides. Granted that commit always happen on subtask 0 with the `global` shuffle. Hence subTaskId may not matter much. But `attemptId` could still cause override behavior.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+@Internal
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader) {
Review Comment:
this operator doesn't commit. it probably only needs a `SerializableTable` for read only.
since we don't refresh the table state, ` table.spec()` would also always point to the spec at the time of loading. any spec/schema change would require job restart, which is still the expectation today.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,350 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-committer-pool-" + table + "-" + sinkId, workerPoolSize);
Review Comment:
should be table name, not `table` object here
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718105017
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
Review Comment:
Maybe something like this (fix my English if applicable):
```
* <li>{@link IcebergCommitter} which committs the incoming{@link IcebergCommittable}s to
* the Iceberg table
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718169486
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2Base.java:
##########
@@ -373,11 +373,11 @@ private List<Snapshot> findValidSnapshots() {
return validSnapshots;
}
- private StructLikeSet expectedRowSet(Record... records) {
+ protected StructLikeSet expectedRowSet(Record... records) {
return SimpleDataUtil.expectedRowSet(table, records);
}
- private StructLikeSet actualRowSet(long snapshotId, String... columns) throws IOException {
+ protected StructLikeSet actualRowSet(long snapshotId, String... columns) throws IOException {
Review Comment:
nit: could this be package private?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718170032
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2Branch.java:
##########
@@ -43,7 +43,7 @@
@ExtendWith(ParameterizedTestExtension.class)
public class TestFlinkIcebergSinkV2Branch extends TestFlinkIcebergSinkV2Base {
@RegisterExtension
- private static final HadoopCatalogExtension CATALOG_EXTENSION =
+ protected static final HadoopCatalogExtension CATALOG_EXTENSION =
Review Comment:
nit: could this be package private?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596970595
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,774 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link SinkCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ sink.initTable = sink.tableSupplier.get();
+ sink.uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ private static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
Review Comment:
nit: formatting
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596959383
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,774 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
Review Comment:
nit: formatting
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1619495787
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
Review Comment:
This is needed, so that this class can keep up with the design contract of `Comitter`:
```
A commit must be idempotent: If some failure occurs in Flink during commit phase, Flink will restart from previous checkpoint and re-attempt to commit all committables.
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592009893
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
+ private static final Configuration CONF = new Configuration();
Review Comment:
Is this needed?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1595263210
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,200 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
+ * only {@link WriteResult} objects) to a single {@link SinkCommittable} per checkpoint (storing the
+ * serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
Review Comment:
Since FLIP-372, we can separate the input, and the output type.
The `IcebergSinkWriter` could emit `WriteResult` objects, and we can emit `SinkCommittable` which is basically just manifest file, and a checkpoint id.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596972921
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,774 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link SinkCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ sink.initTable = sink.tableSupplier.get();
+ sink.uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ private static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ private static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = builder.table.schema();
+ PartitionSpec spec = builder.table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (builder.table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ builder.equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ /**
+ * Sets the currently used committer. Should be used for testing purposes only. Needs to be public
+ * as {@link IcebergSink} is used to test the {@link SinkCommitter} functionality.
+ *
+ * @param newCommitter The committer used for testing
+ */
+ public void setCommitter(Committer<SinkCommittable> newCommitter) {
Review Comment:
Not too elegant - could we find some workaround for setting the committer for tests, or inheriting from IcebergSink for testing, or something else?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599428479
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
+ * only {@link WriteResult} objects) to a single {@link SinkCommittable} per checkpoint (storing the
+ * serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<SinkCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(SinkAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+
+ private transient boolean initialized = false;
+
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public SinkAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ super.open();
Review Comment:
nit: this is not needed
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591350292
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergManifestOutputFileFactory.java:
##########
@@ -0,0 +1,80 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.HasTableOperations;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableOperations;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
+
+public class IcebergManifestOutputFileFactory {
Review Comment:
1. method `generatePath(long checkpointId)` is different.
2. missing method ` public OutputFile create()` in `ManifestOutputFileFactory` implementation
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592010621
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
Review Comment:
Is this used?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592004404
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergStreamWriterMetrics.java:
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import com.codahale.metrics.SlidingWindowReservoir;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.dropwizard.metrics.DropwizardHistogramWrapper;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.metrics.Histogram;
+import org.apache.flink.metrics.MetricGroup;
+import org.apache.iceberg.io.WriteResult;
+
+@Internal
+public class IcebergStreamWriterMetrics {
Review Comment:
Is this different from the `org.apache.iceberg.flink.sink.IcebergStreamWriterMetrics`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591977341
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ // end new
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
Review Comment:
nit: newline
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591966621
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergFilesCommitter.java:
##########
@@ -62,7 +62,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-class IcebergFilesCommitter extends AbstractStreamOperator<Void>
+public class IcebergFilesCommitter extends AbstractStreamOperator<Void>
Review Comment:
We do not need this to be public
Please go through all of the files to check if we need them to make public
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1660599765
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/FlinkManifestUtil.java:
##########
@@ -129,4 +138,25 @@ static WriteResult readCompletedFiles(
return builder.addReferencedDataFiles(deltaManifests.referencedDataFiles()).build();
}
+
+ public static void deleteCommittedManifests(
Review Comment:
we can remove the code from `IcebergFilesCommitter`?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
Review Comment:
is table always a `SerializableTable`?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
Review Comment:
we should move this comment next to the 2nd `.global` where it is used.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
Review Comment:
this can be extracted as a util method and shared btw two classes? `equalityFieldColumns` and `table` can be passed in as args
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
this is not needed, as the `IcebergWriteAggregator` is always set with parallelism of 1.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
Review Comment:
why do we have both initTable and table?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
This is setting the parallelism of `DataStreamSink` (not the writer tasks). We still need to figure out in v2 sink how is the writer parallelism set.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
Review Comment:
nit: avoid those local variables and directly call them in the `new IcebergSink` below?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
Review Comment:
maybe be more readable to explicitly assign. it is uncommon to call a builder method in side the final `build()` step
```
this.table = checkAndGetTable(tableLoader(), table);
```
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
Review Comment:
previously, we need this mainly for a stable UID for the stateful committer operator. In v2 sink, should users handle that themselves via this `DataStream` API?
```
public DataStreamSink<T> sinkTo(
org.apache.flink.api.connector.sink.Sink<T, ?, ?, ?> sink,
CustomSinkOperatorUidHashes customSinkOperatorUidHashes)
```
Previously, all operators are created by the `FlinkSink`. how is uidPrefix used in the v2 sink? some operators are created by the framework. some operators are created by the connector. e.g.
```
.transform(
prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
typeInformation,
new IcebergWriteAggregator(tableLoader))
.uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
```
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
nit: like the simpler name `tableSupplier` from `FlinkSink`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
Review Comment:
`workerPoolSize` is not used until way down in the method. just call the `flinkWriteConf.workerPoolSize()` where it is needed similar to another comment on avoiding local variables
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ private static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ private static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = table.schema();
+ PartitionSpec spec = table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from generic input data stream into iceberg
+ * table. We use {@link RowData} inside the sink connector, so users need to provide a mapper
+ * function and a {@link TypeInformation} to convert those generic records to a RowData
+ * DataStream.
+ *
+ * @param input the generic source input data stream.
+ * @param mapper function to convert the generic data to {@link RowData}
+ * @param outputType to define the {@link TypeInformation} for the input data.
+ * @param <T> the data type of records.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static <T> Builder builderFor(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ return new Builder().forMapperOutputType(input, mapper, outputType);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link Row}s into
+ * iceberg table. We use {@link RowData} inside the sink connector, so users need to provide a
+ * {@link TableSchema} for builder to convert those {@link Row}s to a {@link RowData} DataStream.
+ *
+ * @param input the source input data stream with {@link Row}s.
+ * @param tableSchema defines the {@link TypeInformation} for input data.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static Builder forRow(DataStream<Row> input, TableSchema tableSchema) {
+ return new Builder().forRow(input, tableSchema);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link RowData}s
+ * into iceberg table.
+ *
+ * @param input the source input data stream with {@link RowData}s.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static Builder forRowData(DataStream<RowData> input) {
+ return new Builder().forRowData(input);
+ }
+
+ private static String prefixIfNotNull(String uidPrefix, String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ /**
+ * A table loader that will only reload a table after a certain interval has passed. WARNING: This
Review Comment:
this Javadoc seems for the caching supplier. copy and paste error
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
also is this really used/needed here. Previously, it was created and passed to writer operator. you can find the PR on more context
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1690550344
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSink.java:
##########
@@ -0,0 +1,395 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.sink.IcebergSink.Builder;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
Review Comment:
Flink module has been fully migrated to junit5. please rebase with latest main branch. we would need to convert all test here to junit5.
https://github.com/apache/iceberg/pull/10770
I am also going to submit another PR to refactor the sink test to reduce the number of combinations we are running and the run time. Maybe you can wait after that for the tests change.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,308 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
Review Comment:
what is the purpose of this test class? what's the difference with `TestFlinkManifest`?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/SinkTestUtil.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+
+public class SinkTestUtil {
+
+ private SinkTestUtil() {}
+
+ @SuppressWarnings("unchecked")
+ public static List<StreamElement> fromOutput(Collection<Object> elements) {
+ return elements.stream()
+ .map(
+ element -> {
+ if (element instanceof StreamRecord) {
+ return new StreamRecord<>(
+ ((StreamRecord<CommittableMessage<?>>) element).getValue());
+ }
+ return (StreamElement) element;
+ })
+ .collect(Collectors.toList());
+ }
+
+ public static CommittableSummary<?> toCommittableSummary(StreamElement element) {
Review Comment:
maybe rename the method to make the assertion part explicit like `extractAndAssertCommitableSummary`.
see the `TestHelpers` util class e.g.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1472 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private FileFormat format;
+
+ @Parameter(index = 2)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 3)
+ private Boolean isCheckpointingEnabled;
Review Comment:
do we need this dimension? batch has no checkpoint. streaming has checkpoint enabled. I believe streaming write won't work with checkpoint disabled.
we need to be careful with the combination explosion of those parameterized tests, which leads to longer run time. this is especially important for test classes with many test methods like this one.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1472 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
Review Comment:
is the test coverage same as `TestIcebergFilesCommitter`? any new tests we added for the v2 sink?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSink.java:
##########
@@ -0,0 +1,395 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.sink.IcebergSink.Builder;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
Review Comment:
I am also wondering if we can avoid the code duplication via paremterization. the constructing API is similar. Can we plug in different provider depends on the parameter? But if it turns out to be very difficult, we can go with the copy and paste.
```
IcebergSink.forRowData(dataStream)
.table(table)
.tableLoader(tableLoader)
.writeParallelism(parallelism)
.append();
```
e.g. for the source
https://github.com/apache/iceberg/pull/10741/files
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1472 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private FileFormat format;
Review Comment:
I know this is in `TestIcebergFilesCommitter`. I am wondering if it is necessary to test different data file formats (Avro, Orc, Parquet) for committer logic?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1472 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private FileFormat format;
+
+ @Parameter(index = 2)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 3)
+ private Boolean isCheckpointingEnabled;
+
+ @Parameter(index = 4)
Review Comment:
move this to earlier position to align with `TestIcebergFilesCommitter`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1698264337
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+@Internal
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader) {
Review Comment:
We can check, but if my memory serves me well, we can not call `FlinkManifestUtil.createOutputFileFactory` with a static table. The issue was with `StaticTableOperations.metadataFileLocation`:
```
@Override
public String metadataFileLocation(String fileName) {
throw new UnsupportedOperationException("Cannot modify a static table");
}
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1707845200
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
How does `global()` affect autoscaler? it affects the output channel selection.
```
/**
* Sets the partitioning of the {@link DataStream} so that the output values all go to the first
* instance of the next processing operator. Use this setting with care since it might cause a
* serious performance bottleneck in the application.
*
* @return The DataStream with shuffle partitioning set.
*/
@PublicEvolving
public DataStream<T> global() {
return setConnectionType(new GlobalPartitioner<T>());
}
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1708076227
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
why is this resolved without comment?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1720888195
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
Review Comment:
why is this still `uidPrefix`?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ inputStream.name(operatorName(newUidSuffix)).uid(newUidSuffix + "-mapper");
Review Comment:
this should be just `"Sink pre-write mapper:" + uidSuffix`.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
Review Comment:
if we want to be consistent with the framework operator naming convention, it could be just `.uid("Sink pre-commit aggregator: " + uidPrefix)`
I don't know why we need `SuffixIfNotNull(...)`.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
@rodmeneses what is the error/exception you are seeing after removed the 1st `global`?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1445 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommittableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommittableWithLineage;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ public static final String OPERATOR_ID = "flink-sink";
+ @TempDir File temporaryFolder;
+
+ @TempDir File flinkManifestFolder;
+
+ private Table table;
+
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isStreamingMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final TestCommittableMessageTypeSerializer committableMessageTypeSerializer =
+ new TestCommittableMessageTypeSerializer();
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isStreamingMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isStreamingMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isStreamingMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry("flink.test", "org.apache.iceberg.flink.sink.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", OPERATOR_ID)
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+
+ long timestamp = 0;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+
+ harness.open();
+
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, OPERATOR_ID, dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, OPERATOR_ID, dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ OperatorSubtaskState snapshot = harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#2;
+ // 4. notifyCheckpointComplete for checkpoint#1;
+
+ long timestamp = 0;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, OPERATOR_ID, dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, OPERATOR_ID, dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testSingleCommit() throws Exception {
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness()) {
+ testHarness.open();
+
+ long checkpointId = 1;
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello1");
+ DataFile dataFile1 = writeDataFile("data-1-1", ImmutableList.of(row1));
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId, checkpointId, testHarness, 1, OPERATOR_ID, dataFile1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId, 1L);
+
+ List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ SinkV2Assertions.assertThat(extractAndAssertCommittableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommittableWithLineage(output.get(1)))
+ .hasSubtaskId(0)
+ .hasCheckpointId(checkpointId);
+ }
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, "1")
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
Review Comment:
`testStateRestoreFromPreJob` and `testStateRestoreFromPreJob2` names are not meaningful. should be sth like `testStateRestoreFromPreJobWithCommitted` and `testStateRestoreFromPreJobWithUncommitted`
recreate a new comment to surface it again.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ inputStream.name(operatorName(newUidSuffix)).uid(newUidSuffix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link SerializableTable} instance is used for initializing {@link
+ * IcebergStreamWriter} which will write all the records into {@link DataFile}s and emit them to
+ * downstream operator. Providing a table would avoid so many table loading from each separate
+ * task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = (SerializableTable) SerializableTable.copyOf(newTable);
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link
+ * org.apache.iceberg.flink.sink.IcebergCommitter} lazily, we need this loader because {@link
+ * Table} is not serializable and could not just use the loaded table from Builder#table in the
+ * remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid suffix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator). Actual operator uid will be
+ * prepended with a prefix like "Sink Committer: uidSuffix".
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newSuffix suffix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidSuffix(String newSuffix) {
+ this.uidSuffix = newSuffix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidSuffix != null ? uidSuffix + "-" + suffix : suffix;
+ }
+
+ IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ this.table = checkAndGetTable(tableLoader(), table);
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSupplier = new CachingTableSupplier(table, tableLoader(), tableRefreshInterval);
+ } else {
+ SerializableTable serializableTable = (SerializableTable) SerializableTable.copyOf(table);
Review Comment:
this line is not needed since `table` is already a `SerializableTable`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ inputStream.name(operatorName(newUidSuffix)).uid(newUidSuffix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link SerializableTable} instance is used for initializing {@link
+ * IcebergStreamWriter} which will write all the records into {@link DataFile}s and emit them to
+ * downstream operator. Providing a table would avoid so many table loading from each separate
+ * task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = (SerializableTable) SerializableTable.copyOf(newTable);
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link
+ * org.apache.iceberg.flink.sink.IcebergCommitter} lazily, we need this loader because {@link
+ * Table} is not serializable and could not just use the loaded table from Builder#table in the
+ * remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid suffix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator). Actual operator uid will be
+ * prepended with a prefix like "Sink Committer: uidSuffix".
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newSuffix suffix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidSuffix(String newSuffix) {
+ this.uidSuffix = newSuffix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidSuffix != null ? uidSuffix + "-" + suffix : suffix;
+ }
+
+ IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ this.table = checkAndGetTable(tableLoader(), table);
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSupplier = new CachingTableSupplier(table, tableLoader(), tableRefreshInterval);
+ } else {
+ SerializableTable serializableTable = (SerializableTable) SerializableTable.copyOf(table);
+ tableSupplier = () -> serializableTable;
+ }
+
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds =
+ SinkUtil.checkAndGetEqualityFieldIds(table, equalityFieldColumns);
+
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !overwriteMode,
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ return new IcebergSink(
+ tableLoader,
+ table,
+ snapshotSummary,
+ Optional.ofNullable(uidSuffix).orElse(""),
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf),
+ toFlinkRowType(table.schema(), tableSchema),
+ tableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ flinkWriteConf.branch(),
+ overwriteMode);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidSuffix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
Review Comment:
add a comment to explain that `V2 sink framework would apply the uid here to the framework created operators like writer, committer. E.g. "Sink writer: <uidSuffix>"`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ inputStream.name(operatorName(newUidSuffix)).uid(newUidSuffix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link SerializableTable} instance is used for initializing {@link
+ * IcebergStreamWriter} which will write all the records into {@link DataFile}s and emit them to
+ * downstream operator. Providing a table would avoid so many table loading from each separate
+ * task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = (SerializableTable) SerializableTable.copyOf(newTable);
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link
+ * org.apache.iceberg.flink.sink.IcebergCommitter} lazily, we need this loader because {@link
+ * Table} is not serializable and could not just use the loaded table from Builder#table in the
+ * remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid suffix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator). Actual operator uid will be
+ * prepended with a prefix like "Sink Committer: uidSuffix".
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newSuffix suffix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidSuffix(String newSuffix) {
+ this.uidSuffix = newSuffix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
Review Comment:
this seems wrong. it probably should be
```
private String operatorName(String prefix) {
return uidSuffix != null ? prefix + "-" + uidSuffix : prefix;
}
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718940359
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
I am not sure how to interpret the code snippet. are we saying `rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());` would propagate the proper parallelism to the writer operator transformation? If yes, let' add a unit test to verify it.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718101257
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
Review Comment:
This is not true. The `SinkWriter` will just write the data to the data files and generate the `WriteResult`s and that's it.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718779643
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
`tableSupplier` variable is later on passed as a constructor argument below, when we instantiate the `IcebergSink` ie:
```java
return new IcebergSink(
tableLoader,
table,
snapshotSummary,
Optional.ofNullable(uidPrefix).orElse(""),
writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf),
toFlinkRowType(table.schema(), tableSchema),
tableSupplier,
flinkWriteConf,
equalityFieldIds,
flinkWriteConf.branch(),
overwriteMode);
}
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718125248
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -21,18 +21,19 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
+import org.apache.flink.annotation.Internal;
import org.apache.iceberg.FileFormat;
import org.apache.iceberg.HasTableOperations;
import org.apache.iceberg.Table;
import org.apache.iceberg.TableOperations;
import org.apache.iceberg.io.OutputFile;
import org.apache.iceberg.relocated.com.google.common.base.Strings;
-class ManifestOutputFileFactory {
+@Internal
+public class ManifestOutputFileFactory {
// Users could define their own flink manifests directory by setting this value in table
// properties.
- static final String FLINK_MANIFEST_LOCATION = "flink.manifests.location";
-
+ public static final String FLINK_MANIFEST_LOCATION = "flink.manifests.location";
Review Comment:
Maybe add `@VisibleForTesting`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718122557
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergStreamWriterMetrics.java:
##########
@@ -62,7 +62,7 @@ class IcebergStreamWriterMetrics {
new DropwizardHistogramWrapper(dropwizardDeleteFilesSizeHistogram));
}
- void updateFlushResult(WriteResult result) {
+ public void updateFlushResult(WriteResult result) {
Review Comment:
Could we make the **class** `@Internal` - just to be consistent with `IcebergFilesCommitterMetrics`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1613636902
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommittable.java:
##########
@@ -0,0 +1,69 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.Serializable;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+
+/**
+ * The aggregated results of a single checkpoint which should be committed. Containing the
+ * serialized {@link org.apache.iceberg.flink.sink.DeltaManifests} file - which contains the commit
+ * data, and the jobId, operatorId, checkpointId triplet which helps identifying the specific commit
+ *
+ * <p>{@link SinkCommittableSerializer} is used for serializing the objects between the Writer and
+ * the Aggregator operator and between the Aggregator and the Committer as well.
+ */
+public class SinkCommittable implements Serializable {
+ private final byte[] manifest;
+ private final String jobId;
+ private final String operatorId;
+ private final long checkpointId;
+
+ public SinkCommittable(byte[] manifest, String jobId, String operatorId, long checkpointId) {
Review Comment:
It's a good idea, but I rather implement this in a new PR so that we can limit the scope of this PR, as it's already quite big
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714305902
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,761 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ this.table = checkAndGetTable(tableLoader(), table);
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds =
+ SinkUtil.checkAndGetEqualityFieldIds(table, equalityFieldColumns);
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ SerializableTable serializableTable = (SerializableTable) SerializableTable.copyOf(table);
Review Comment:
`table` type is `Table`, not `SerializableTable`.
I've made `checkAndGetTable` to return `SerializableTable` but we still need the type casting when we instantiate `tableSupplier`
We could do more refactor in the code to avoid the type cast, but IMO this would introduce additional complexity which is not needed
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714417771
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
Review Comment:
yes, in this case referring to checkpointId=0
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1619421059
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
Review Comment:
Iceberg style is to only use `this.` when changing the value, like from constructor or setter.
```
this.varA = newValue
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2311029567
thanks @rodmeneses for the contribution and @pvary for the review
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "nastra (via GitHub)" <gi...@apache.org>.
nastra commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1571831392
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkV2Committer.java:
##########
@@ -0,0 +1,808 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.writer.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RunWith(Parameterized.class)
Review Comment:
please update this (and other tests in this PR) to JUnit5 + AssertJ, because we're in the process of migrating JUnit4 to JUnit5 and new tests should be written using JUnit5 + AssertJ
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596976706
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommittable.java:
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.Serializable;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+
+/**
+ * The aggregated results of a single checkpoint which should be committed. Containing the
+ * serialized {@link org.apache.iceberg.flink.sink.DeltaManifests} file - which contains the commit
+ * data, and the jobId, operatorId, checkpointId triplet which helps identifying the specific commit
+ *
+ * <p>{@link SinkCommittableSerializer} is used for serializing the objects between the Writer and
+ * the Aggregator operator and between the Aggregator and the Committer as well.
+ */
+public class SinkCommittable implements Serializable {
+ private byte[] manifest;
+ private String jobId = "";
Review Comment:
nit: why is the default value? I do not think we need this
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591998705
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,478 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ *
+ * <p>Missing features from the Flink Sink V2 API:
Review Comment:
Please read and fix the comments
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1597002086
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeDataFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readDataFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
+ private static final Configuration CONF = new Configuration();
+ private @TempDir File folder;
+ private Table table;
+ private FileAppenderFactory<RowData> appenderFactory;
+ private final AtomicInteger fileCount = new AtomicInteger(0);
+
+ @BeforeEach
+ public void before() throws IOException {
+ String warehouse = folder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir())
+ .as("Should create the table directory correctly.")
+ .isTrue();
+
+ // Construct the iceberg table.
+ table = SimpleDataUtil.createTable(tablePath, ImmutableMap.of(), false);
+
+ int[] equalityFieldIds =
+ new int[] {
+ table.schema().findField("id").fieldId(), table.schema().findField("data").fieldId()
+ };
+ this.appenderFactory =
+ new FlinkAppenderFactory(
+ table,
+ table.schema(),
+ FlinkSchemaUtil.convert(table.schema()),
+ table.properties(),
+ table.spec(),
+ equalityFieldIds,
+ table.schema(),
+ null);
+ }
+
+ @Test
+ void testIO() throws IOException {
+ for (long checkpointId = 1; checkpointId <= 3; checkpointId++) {
+ ManifestOutputFileFactory factory =
+ SinkAggregator.createOutputFileFactory(() -> table, table.properties(), "prefix");
+ final long curCkpId = checkpointId;
+
+ List<DataFile> dataFiles = generateDataFiles(10);
+ List<DeleteFile> eqDeleteFiles = generateEqDeleteFiles(5);
+ List<DeleteFile> posDeleteFiles = generatePosDeleteFiles(5);
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ WriteResult.builder()
+ .addDataFiles(dataFiles)
+ .addDeleteFiles(eqDeleteFiles)
+ .addDeleteFiles(posDeleteFiles)
+ .build(),
+ () -> factory.create(curCkpId),
+ table.spec());
+
+ WriteResult result = readCompletedFiles(deltaManifests, table.io(), table.specs());
+ assertThat(result.deleteFiles()).as("Size of data file list are not equal").hasSize(10);
+
+ for (int i = 0; i < dataFiles.size(); i++) {
+ TestHelpers.assertEquals(dataFiles.get(i), result.dataFiles()[i]);
+ }
+ assertThat(result.dataFiles()).as("Size of delete file list are not equal.").hasSize(10);
Review Comment:
nit: newline
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeDataFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readDataFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
+ private static final Configuration CONF = new Configuration();
+ private @TempDir File folder;
+ private Table table;
+ private FileAppenderFactory<RowData> appenderFactory;
+ private final AtomicInteger fileCount = new AtomicInteger(0);
+
+ @BeforeEach
+ public void before() throws IOException {
+ String warehouse = folder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir())
+ .as("Should create the table directory correctly.")
+ .isTrue();
+
+ // Construct the iceberg table.
+ table = SimpleDataUtil.createTable(tablePath, ImmutableMap.of(), false);
+
+ int[] equalityFieldIds =
+ new int[] {
+ table.schema().findField("id").fieldId(), table.schema().findField("data").fieldId()
+ };
+ this.appenderFactory =
+ new FlinkAppenderFactory(
+ table,
+ table.schema(),
+ FlinkSchemaUtil.convert(table.schema()),
+ table.properties(),
+ table.spec(),
+ equalityFieldIds,
+ table.schema(),
+ null);
+ }
+
+ @Test
+ void testIO() throws IOException {
+ for (long checkpointId = 1; checkpointId <= 3; checkpointId++) {
+ ManifestOutputFileFactory factory =
+ SinkAggregator.createOutputFileFactory(() -> table, table.properties(), "prefix");
+ final long curCkpId = checkpointId;
+
+ List<DataFile> dataFiles = generateDataFiles(10);
+ List<DeleteFile> eqDeleteFiles = generateEqDeleteFiles(5);
+ List<DeleteFile> posDeleteFiles = generatePosDeleteFiles(5);
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ WriteResult.builder()
+ .addDataFiles(dataFiles)
+ .addDeleteFiles(eqDeleteFiles)
+ .addDeleteFiles(posDeleteFiles)
+ .build(),
+ () -> factory.create(curCkpId),
+ table.spec());
+
+ WriteResult result = readCompletedFiles(deltaManifests, table.io(), table.specs());
+ assertThat(result.deleteFiles()).as("Size of data file list are not equal").hasSize(10);
+
+ for (int i = 0; i < dataFiles.size(); i++) {
+ TestHelpers.assertEquals(dataFiles.get(i), result.dataFiles()[i]);
+ }
+ assertThat(result.dataFiles()).as("Size of delete file list are not equal.").hasSize(10);
+ for (int i = 0; i < 5; i++) {
+ TestHelpers.assertEquals(eqDeleteFiles.get(i), result.deleteFiles()[i]);
+ }
+ for (int i = 0; i < 5; i++) {
Review Comment:
nit: newline
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599425277
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link SinkCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ sink.initTable = sink.tableSupplier.get();
+ sink.uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ private static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ private static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = builder.table.schema();
+ PartitionSpec spec = builder.table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (builder.table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ builder.equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ /**
+ * Sets the currently used committer. Should be used for testing purposes only. Needs to be public
Review Comment:
nit: comment is not fixed
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599426224
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
Review Comment:
nit: Fix the comment to match the implementation
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587857184
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/SimpleTableSupplier.java:
##########
@@ -0,0 +1,45 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.util.SerializableSupplier;
+
+/**
+ * A table loader that will only reload a table after a certain interval has passed. WARNING: This
+ * table loader should be used carefully when used with writer tasks. It could result in heavy load
+ * on a catalog for jobs with many writers.
+ */
+@Internal
+public class SimpleTableSupplier implements SerializableSupplier<Table> {
Review Comment:
Could this be an inline static class in IcebergSink?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587862893
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergFlinkManifestUtil.java:
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+
+public class IcebergFlinkManifestUtil {
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ private IcebergFlinkManifestUtil() {}
+
+ static ManifestFile writeDataFiles(
Review Comment:
How much of these are really needed to be generic util methods?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626559065
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+
+ this.table = tableLoader.loadTable();
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(() -> table, table.properties(), prefix);
+ }
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<IcebergCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
+ results.clear();
+ }
+
+ /**
+ * Write all the completed data files to a newly created manifest file and return the manifest's
+ * avro serialized bytes.
+ */
+ public byte[] writeToManifest(Collection<WriteResult> writeResults, long checkpointId)
+ throws IOException {
+ if (writeResults.isEmpty()) {
+ return EMPTY_MANIFEST_DATA;
+ }
+
+ WriteResult result = WriteResult.builder().addAll(writeResults).build();
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ result, () -> icebergManifestOutputFileFactory.create(checkpointId), table.spec());
+
+ return SimpleVersionedSerialization.writeVersionAndSerialize(
+ DeltaManifestsSerializer.INSTANCE, deltaManifests);
+ }
+
+ /**
+ * Write the {@link WriteResult} to temporary manifest files.
+ *
+ * @param result all those DataFiles/DeleteFiles in this WriteResult should be written with same
+ * partition spec
+ */
+ public static DeltaManifests writeCompletedFiles(
+ WriteResult result, Supplier<OutputFile> outputFileSupplier, PartitionSpec spec)
+ throws IOException {
+
+ ManifestFile dataManifest = null;
+ ManifestFile deleteManifest = null;
+
+ // Write the completed data files into a newly created data manifest file.
+ if (result.dataFiles() != null && result.dataFiles().length > 0) {
+ dataManifest =
+ FlinkManifestUtil.writeDataFiles(
+ outputFileSupplier.get(), spec, Lists.newArrayList(result.dataFiles()));
+ }
+
+ // Write the completed delete files into a newly created delete manifest file.
+ if (result.deleteFiles() != null && result.deleteFiles().length > 0) {
+ OutputFile deleteManifestFile = outputFileSupplier.get();
+
+ ManifestWriter<DeleteFile> deleteManifestWriter =
+ ManifestFiles.writeDeleteManifest(FORMAT_V2, spec, deleteManifestFile, DUMMY_SNAPSHOT_ID);
+ try (ManifestWriter<DeleteFile> writer = deleteManifestWriter) {
+ for (DeleteFile deleteFile : result.deleteFiles()) {
+ writer.add(deleteFile);
+ }
+ }
+
+ deleteManifest = deleteManifestWriter.toManifestFile();
+ }
+
+ return new DeltaManifests(dataManifest, deleteManifest, result.referencedDataFiles());
+ }
+
+ @Override
+ public void processElement(StreamRecord<CommittableMessage<WriteResult>> element)
+ throws Exception {
+ if (element.isRecord() && element.getValue() instanceof CommittableWithLineage) {
Review Comment:
@stevenzwu did you mean that if `element.getValue()` is `true` but `element.getValue() instanceof CommittableWithLineage` is false, we should throw an error?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1693449382
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,350 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-committer-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ long maxCommittedCheckpointId =
+ getMaxCommittedCheckpointId(table, last.jobId(), last.operatorId(), branch);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> uncommitted =
+ commitRequestMap.tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitPendingRequests(uncommitted, last.jobId(), last.operatorId());
+ }
+ }
+
+ public static long getMaxCommittedCheckpointId(
+ Table table, String flinkJobId, String operatorId, String branch) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ if (flinkJobId.equals(snapshotFlinkJobId)
+ && (snapshotOperatorId == null || snapshotOperatorId.equals(operatorId))) {
+ String value = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (value != null) {
+ lastCommittedCheckpointId = Long.parseLong(value);
+ break;
+ }
+ }
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param commitRequestMap The checkpointId to {@link CommitRequest} map of the changes to commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ private void commitPendingRequests(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ String newFlinkJobId,
+ String operatorId)
+ throws IOException {
+ long checkpointId = commitRequestMap.lastKey();
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, CommitRequest<IcebergCommittable>> e : commitRequestMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue().getCommittable().manifest())) {
+ pendingResults.put(e.getKey(), EMPTY_WRITE_RESULT);
+ } else {
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue().getCommittable().manifest());
+ pendingResults.put(
+ e.getKey(),
+ FlinkManifestUtil.readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+
+ FlinkManifestUtil.deleteCommittedManifests(table, manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ long checkpointId = pendingResults.lastKey();
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ // fail all commits as really its only one
+ commitOperation(appendFiles, summary, "append", newFlinkJobId, operatorId, checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
+ }
+ }
+ }
+
+ private void commitOperation(
+ SnapshotUpdate<?> operation,
+ CommitSummary summary,
+ String description,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+
+ LOG.info(
+ "Committing {} for checkpoint {} to table {} branch {} with summary: {}",
+ description,
+ checkpointId,
+ table.name(),
+ branch,
+ summary);
+ snapshotProperties.forEach(operation::set);
+ // custom snapshot metadata properties will be overridden if they conflict with internal ones
+ // used by the sink.
+ operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
+ operation.set(FLINK_JOB_ID, newFlinkJobId);
+ operation.set(OPERATOR_ID, operatorId);
+ operation.toBranch(branch);
+
+ long startNano = System.nanoTime();
+ operation.commit(); // abort is automatically called if this fails.
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano);
+ LOG.info(
+ "Committed {} to table: {}, branch: {}, checkpointId {} in {} ms",
+ description,
+ table.name(),
+ branch,
+ checkpointId,
+ durationMs);
+ if (committerMetrics != null) {
+ committerMetrics.commitDuration(durationMs);
+ }
+ }
+
+ @Override
+ public void close() {
+ // Nothing to do
Review Comment:
thanks for clarifying
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718161707
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/UnpartitionedDeltaWriter.java:
##########
@@ -58,7 +58,7 @@ class UnpartitionedDeltaWriter extends BaseDeltaTaskWriter {
}
@Override
- RowDataDeltaWriter route(RowData row) {
+ protected RowDataDeltaWriter route(RowData row) {
Review Comment:
Is this change required?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718170262
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergFilesCommitter.java:
##########
@@ -138,7 +139,8 @@ public void testCommitTxnWithoutDataFiles() throws Exception {
assertSnapshotSize(0);
assertMaxCommittedCheckpointId(jobId, operatorId, -1L);
- // It's better to advance the max-committed-checkpoint-id in iceberg snapshot, so that the
+ // It's better to a
+ // dvance the max-committed-checkpoint-id in iceberg snapshot, so that the
Review Comment:
nit: fix the comment
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714189646
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
`tableSupplier` will be passed to the `IcebergSinkWriter` inside the `icebergSink.createWriter` method
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
`tableSupplier` will be passed to the `IcebergSinkWriter` inside the `IcebergSink.createWriter` method
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714330295
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
Review Comment:
renamed to this test to `testEmitCommittableSummary`
this test will do additional assertions on `currentSnapshot.summary()`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714184380
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
we use
```java
public Builder writeParallelism(int newWriteParallelism)
```
in the `IcebergSink` Builder
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714454918
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
Review Comment:
we are initializing the state from `snapshot` which is build previously with
```java
snapshot = testHarness.snapshot(checkpointId, 2L)
```
which corresponds to the previous job.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1715599893
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
autoscaler won't be affected as the max parallelism is set to 1
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1723543103
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
Review Comment:
`suffixIfNotNull(uidSuffix, suffix)` is used so that if the `$uidSuffix` is null, we return `$suffix` or instead we return `$uidSuffix-$suffix`. So it's merely a convenient method that deals with `$uidSuffix` is null use case
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717690616
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
@rodmeneses so do we need the first `global` or not?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2375118465
> Hi @rodmeneses, by everything works I meant that I did some manual tests and the results were the same as with the old one. Probably "everything" was an overkill here ;-) Yes, I can take this. I'll let you know on the slack if I need any help with this task.
Amazing! Thanks!!
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592008470
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.committer.IcebergManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeDataFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readDataFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
+ private static final Configuration CONF = new Configuration();
Review Comment:
Is this needed?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1619505665
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
Review Comment:
I manually verified that when a commit fails, the commit is retried, without having to call `retryLater`, so I think we don't have to manually call `retryLater`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626656514
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+
+ this.table = tableLoader.loadTable();
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(() -> table, table.properties(), prefix);
+ }
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<IcebergCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
+ results.clear();
+ }
+
+ /**
+ * Write all the completed data files to a newly created manifest file and return the manifest's
+ * avro serialized bytes.
+ */
+ public byte[] writeToManifest(Collection<WriteResult> writeResults, long checkpointId)
+ throws IOException {
+ if (writeResults.isEmpty()) {
+ return EMPTY_MANIFEST_DATA;
+ }
+
+ WriteResult result = WriteResult.builder().addAll(writeResults).build();
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ result, () -> icebergManifestOutputFileFactory.create(checkpointId), table.spec());
+
+ return SimpleVersionedSerialization.writeVersionAndSerialize(
+ DeltaManifestsSerializer.INSTANCE, deltaManifests);
+ }
+
+ /**
+ * Write the {@link WriteResult} to temporary manifest files.
+ *
+ * @param result all those DataFiles/DeleteFiles in this WriteResult should be written with same
+ * partition spec
+ */
+ public static DeltaManifests writeCompletedFiles(
+ WriteResult result, Supplier<OutputFile> outputFileSupplier, PartitionSpec spec)
+ throws IOException {
+
+ ManifestFile dataManifest = null;
+ ManifestFile deleteManifest = null;
+
+ // Write the completed data files into a newly created data manifest file.
+ if (result.dataFiles() != null && result.dataFiles().length > 0) {
+ dataManifest =
+ FlinkManifestUtil.writeDataFiles(
+ outputFileSupplier.get(), spec, Lists.newArrayList(result.dataFiles()));
+ }
+
+ // Write the completed delete files into a newly created delete manifest file.
+ if (result.deleteFiles() != null && result.deleteFiles().length > 0) {
+ OutputFile deleteManifestFile = outputFileSupplier.get();
+
+ ManifestWriter<DeleteFile> deleteManifestWriter =
+ ManifestFiles.writeDeleteManifest(FORMAT_V2, spec, deleteManifestFile, DUMMY_SNAPSHOT_ID);
+ try (ManifestWriter<DeleteFile> writer = deleteManifestWriter) {
+ for (DeleteFile deleteFile : result.deleteFiles()) {
+ writer.add(deleteFile);
+ }
+ }
+
+ deleteManifest = deleteManifestWriter.toManifestFile();
+ }
+
+ return new DeltaManifests(dataManifest, deleteManifest, result.referencedDataFiles());
+ }
+
+ @Override
+ public void processElement(StreamRecord<CommittableMessage<WriteResult>> element)
+ throws Exception {
+ if (element.isRecord() && element.getValue() instanceof CommittableWithLineage) {
Review Comment:
yes, there are valid cases where `isRecord` is true and the value isn't `CommittableWithLineage` but is a `CommitSummary`. In those cases, we simply don't add it to `results`. So, if we add `Preconditions` check, we will be throwing exception and failing this method. So I'll keep it as it is.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1605607730
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
Review Comment:
nit: `snapshotSummary` is probably more accurate. properties can cause a little confusion as table properties
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
Review Comment:
is this a valid requirement to have multiple sinks writing to the same Iceberg table in a single Flink job? can't the streams be unionized first before writing to Iceberg?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommittable.java:
##########
@@ -0,0 +1,69 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.Serializable;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+
+/**
+ * The aggregated results of a single checkpoint which should be committed. Containing the
+ * serialized {@link org.apache.iceberg.flink.sink.DeltaManifests} file - which contains the commit
+ * data, and the jobId, operatorId, checkpointId triplet which helps identifying the specific commit
+ *
+ * <p>{@link SinkCommittableSerializer} is used for serializing the objects between the Writer and
+ * the Aggregator operator and between the Aggregator and the Committer as well.
+ */
+public class SinkCommittable implements Serializable {
+ private final byte[] manifest;
+ private final String jobId;
+ private final String operatorId;
+ private final long checkpointId;
+
+ public SinkCommittable(byte[] manifest, String jobId, String operatorId, long checkpointId) {
Review Comment:
I think we should pass in `manifest` as `DeltaManifests` (not as `byte[]`). I know current code does byte[]. We can move the serialization and deserialization to the `SinkCommittableSerializer`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
Review Comment:
> Commit only on committer 1
This has always been a sticking issue to me. It will be confusing for users when looking at the DAG and wondering why other committer subtasks get no traffic or why there are so many committer subtasks.
I asked the Flink community on this before. There was a suggestion of supporting control of committer parallelism. That will be sufficient for this purpose. But I think it is not formally pursued and completed.
https://lists.apache.org/thread/82bgvlton9olb591bfg2djv0cshj1bxj
It will be great to fix the Flink side first.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
Review Comment:
nit: `IcebergSink.` not needed. applicable to other places
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "nastra (via GitHub)" <gi...@apache.org>.
nastra commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1571831722
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkV2Committer.java:
##########
@@ -0,0 +1,808 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.writer.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RunWith(Parameterized.class)
Review Comment:
```suggestion
@ExtendWith(ParameterizedTestExtension.class)
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "nastra (via GitHub)" <gi...@apache.org>.
nastra commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1571830165
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestFlinkManifest.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.committer.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.writer.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.Assert;
Review Comment:
please update these to Junit5 tests that use AssertJ assertions. You can use `TestRewriteDataFilesAction` as a reference
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1598876459
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,774 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link SinkCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ sink.initTable = sink.tableSupplier.get();
+ sink.uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ private static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ private static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = builder.table.schema();
+ PartitionSpec spec = builder.table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (builder.table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ builder.equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ /**
+ * Sets the currently used committer. Should be used for testing purposes only. Needs to be public
+ * as {@link IcebergSink} is used to test the {@link SinkCommitter} functionality.
+ *
+ * @param newCommitter The committer used for testing
+ */
+ public void setCommitter(Committer<SinkCommittable> newCommitter) {
Review Comment:
I made it package private and added `@VisibleForTesting`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596964200
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,774 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link SinkCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer". <br>
+ * <br>
Review Comment:
Maybe `<p>`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719082354
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
I did a debugging where I could assert that the parallelism of the framework operators matched the one set with `IcebergSink.writeParallelism($parallelism)`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717695753
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
`tableSupplier` is a stack local variable (not a class variable). it doesn't modify `this.tableSupplier`.
```
SerializableSupplier<Table> tableSupplier;
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717854156
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,761 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ this.table = checkAndGetTable(tableLoader(), table);
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds =
+ SinkUtil.checkAndGetEqualityFieldIds(table, equalityFieldColumns);
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ SerializableTable serializableTable = (SerializableTable) SerializableTable.copyOf(table);
Review Comment:
why can't `table` type be `SerializableTable`? what's the problem with instantiating `tableSupplier`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718098952
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/FlinkManifestUtil.java:
##########
@@ -129,4 +133,25 @@ static WriteResult readCompletedFiles(
return builder.addReferencedDataFiles(deltaManifests.referencedDataFiles()).build();
}
+
+ public static void deleteCommittedManifests(
+ Table table, List<ManifestFile> manifests, String newFlinkJobId, long checkpointId) {
+ for (ManifestFile manifest : manifests) {
+ try {
+ table.io().deleteFile(manifest.path());
+ } catch (Exception e) {
+ // The flink manifests cleaning failure shouldn't abort the completed checkpoint.
+ String details =
+ MoreObjects.toStringHelper(FlinkManifestUtil.class)
+ .add("flinkJobId", newFlinkJobId)
+ .add("checkpointId", checkpointId)
+ .add("manifestPath", manifest.path())
+ .toString();
Review Comment:
This seems a bit strange way for me to collect the parameters for the log line.
Minimally we need to add the tableName to the log line too.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718167153
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+@Internal
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+
+ public IcebergWriteAggregator(TableLoader tableLoader) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+ String flinkJobId = getContainingTask().getEnvironment().getJobID().toString();
+ String operatorId = getOperatorID().toString();
+ int subTaskId = 0;
Review Comment:
Could we add an assertion for the subTaskId to be 0? We strongly build around this
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719099167
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
Review Comment:
also with `build()` method exposed, `writeParallelism` and `uidsuffix` can't be applied, if users have to call the `DataStream#sinkTo(icebergSink)` themselves. The more I think about it, the more I prefer only expose the `append()` method.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718808231
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
Review Comment:
do we want to expose this as public?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1720132474
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
Review Comment:
I've made build() package private
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1715716674
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
Review Comment:
the `testStateRestoreFromPreJob2` test is already doing this
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1723984264
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ inputStream.name(operatorName(newUidSuffix)).uid(newUidSuffix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link SerializableTable} instance is used for initializing {@link
+ * IcebergStreamWriter} which will write all the records into {@link DataFile}s and emit them to
+ * downstream operator. Providing a table would avoid so many table loading from each separate
+ * task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = (SerializableTable) SerializableTable.copyOf(newTable);
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link
+ * org.apache.iceberg.flink.sink.IcebergCommitter} lazily, we need this loader because {@link
+ * Table} is not serializable and could not just use the loaded table from Builder#table in the
+ * remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid suffix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator). Actual operator uid will be
+ * prepended with a prefix like "Sink Committer: uidSuffix".
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newSuffix suffix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidSuffix(String newSuffix) {
+ this.uidSuffix = newSuffix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
Review Comment:
removed the method
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717611342
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
Review Comment:
I haven't been able to find a way to propagate the `uidPrefix` from the `IcebergSink` to the operators created by the v2 sink framework. There's this `CustomSinkOperatorUidHashes` where one could maybe plug in some uid hashes, but it's not clear to me how to generate this hashes, and if this is even the right approach. It seems to me that having to hash the uidPrefix is not the right approach? But I maybe wrong here.
So, something like this could work:
```java
public DataStreamSink<RowData> append() {
IcebergSink sink = build();
DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
String writeUidHash = hashFunction(uidPrefix+"-writer");
String commiterUidHash = hashFunction(uidPrefix+"-committer");
String globalCommitterUiHash = hashFunction(uidPrefix+"-global-committer");
CustomSinkOperatorUidHashes hashes = new CustomSinkOperatorUidHashes(writeUidHash, commiterUidHash, globalCommitterUiHash);
DataStreamSink<RowData> rowDataDataStreamSink =
rowDataInput.sinkTo(sink, hashes).uid(uidPrefix + "-sink");
if (sink.flinkWriteConf.writeParallelism() != null) {
rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
}
return rowDataDataStreamSink;
}
```
`hashFunction` being the hashing function (I'm not sure which one should be used here)
to me, the above seems a little hacky. There should be a more straightforward way to set this up.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1720335116
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergCommitter.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+class IcebergCommitter implements Committer<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private IcebergFilesCommitterMetrics committerMetrics;
+ private Table table;
+ private final TableLoader tableLoader;
+ private int maxContinuousEmptyCommits;
+ private ExecutorService workerPool;
+ private int continuousEmptyCheckpoints = 0;
+
+ IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+ this.tableLoader = tableLoader;
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool(
+ "iceberg-committer-pool-" + table.name() + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+ long maxCommittedCheckpointId =
+ SinkUtil.getMaxCommittedCheckpointId(table, last.jobId(), last.operatorId(), branch);
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> uncommitted =
+ commitRequestMap.tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitPendingRequests(uncommitted, last.jobId(), last.operatorId());
+ }
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests} ordered by the checkpointId, and writing the new
+ * snapshot to the Iceberg table. The {@link org.apache.iceberg.SnapshotSummary} will contain the
+ * jobId, snapshotId, checkpointId so in case of job restart we can identify which changes are
+ * committed, and which are still waiting for the commit.
+ *
+ * @param commitRequestMap The checkpointId to {@link CommitRequest} map of the changes to commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ private void commitPendingRequests(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ String newFlinkJobId,
+ String operatorId)
+ throws IOException {
+ long checkpointId = commitRequestMap.lastKey();
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, CommitRequest<IcebergCommittable>> e : commitRequestMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue().getCommittable().manifest())) {
+ pendingResults.put(e.getKey(), EMPTY_WRITE_RESULT);
+ } else {
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue().getCommittable().manifest());
+ pendingResults.put(
+ e.getKey(),
+ FlinkManifestUtil.readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+
+ FlinkManifestUtil.deleteCommittedManifests(table, manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ long checkpointId = pendingResults.lastKey();
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ // fail all commits as really its only one
+ commitOperation(appendFiles, summary, "append", newFlinkJobId, operatorId, checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
Review Comment:
This is a bit misleading here.
If we committing multiple checkpoints after each other (pendingResults.size()>1), then the summary contains every data/delete file. OTOH when we write out the summary in the logs, then we do it for every commit.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "bryanck (via GitHub)" <gi...@apache.org>.
bryanck commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1693429623
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,350 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-committer-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ long maxCommittedCheckpointId =
+ getMaxCommittedCheckpointId(table, last.jobId(), last.operatorId(), branch);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> uncommitted =
+ commitRequestMap.tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitPendingRequests(uncommitted, last.jobId(), last.operatorId());
+ }
+ }
+
+ public static long getMaxCommittedCheckpointId(
+ Table table, String flinkJobId, String operatorId, String branch) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ if (flinkJobId.equals(snapshotFlinkJobId)
+ && (snapshotOperatorId == null || snapshotOperatorId.equals(operatorId))) {
+ String value = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (value != null) {
+ lastCommittedCheckpointId = Long.parseLong(value);
+ break;
+ }
+ }
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param commitRequestMap The checkpointId to {@link CommitRequest} map of the changes to commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ private void commitPendingRequests(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap,
+ String newFlinkJobId,
+ String operatorId)
+ throws IOException {
+ long checkpointId = commitRequestMap.lastKey();
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, CommitRequest<IcebergCommittable>> e : commitRequestMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue().getCommittable().manifest())) {
+ pendingResults.put(e.getKey(), EMPTY_WRITE_RESULT);
+ } else {
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue().getCommittable().manifest());
+ pendingResults.put(
+ e.getKey(),
+ FlinkManifestUtil.readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+
+ FlinkManifestUtil.deleteCommittedManifests(table, manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ long checkpointId = pendingResults.lastKey();
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId) {
+ long checkpointId = pendingResults.lastKey();
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ // fail all commits as really its only one
+ commitOperation(appendFiles, summary, "append", newFlinkJobId, operatorId, checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
+ }
+ }
+ }
+
+ private void commitOperation(
+ SnapshotUpdate<?> operation,
+ CommitSummary summary,
+ String description,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+
+ LOG.info(
+ "Committing {} for checkpoint {} to table {} branch {} with summary: {}",
+ description,
+ checkpointId,
+ table.name(),
+ branch,
+ summary);
+ snapshotProperties.forEach(operation::set);
+ // custom snapshot metadata properties will be overridden if they conflict with internal ones
+ // used by the sink.
+ operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
+ operation.set(FLINK_JOB_ID, newFlinkJobId);
+ operation.set(OPERATOR_ID, operatorId);
+ operation.toBranch(branch);
+
+ long startNano = System.nanoTime();
+ operation.commit(); // abort is automatically called if this fails.
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano);
+ LOG.info(
+ "Committed {} to table: {}, branch: {}, checkpointId {} in {} ms",
+ description,
+ table.name(),
+ branch,
+ checkpointId,
+ durationMs);
+ if (committerMetrics != null) {
+ committerMetrics.commitDuration(durationMs);
+ }
+ }
+
+ @Override
+ public void close() {
+ // Nothing to do
Review Comment:
This was added so that the committer can refresh any file IO properties passed back by a REST catalog, e.g. auth tokens that might otherwise expire.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587875343
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,129 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
+ * only {@link WriteResult} objects) to a single {@link SinkCommittable} per checkpoint (storing the
+ * serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<SinkCommittable>, CommittableMessage<SinkCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(SinkAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
Review Comment:
Do we need a state for this?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587945650
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,444 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ *
+ * <p>Missing features from the Flink Sink V2 API:
+ *
+ * <ul>
+ * <li>No metrics access - The SinkV2 implementation will not emit committer metrics for now
+ * <li>Committer parallelism could not be set - There will be running instances of the Committer
+ * which does not receive any events
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ private final TableLoader tableLoader;
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient IcebergManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient int continuousEmptyCheckpoints = 0;
+ private transient boolean initialized = false;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix) {
+ this.tableLoader = tableLoader;
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ LOG.info("Noting to commit");
+ return;
+ }
+
+ if (!initialized) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
Review Comment:
nit:newline
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719090494
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
After doing several tests, we actually need both `global` invocations as well as the ` .setParallelism(1)
.setMaxParallelism(1)` calls.
This can be revisited once upstream Flink provides the capability of setting committer operator parallelism
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719081266
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
Review Comment:
I feel we should allow our users a way to instantiate a `IcebergSink` object. If we make this private, then they will have to use `append()` which gives back `DataStreamSink<RowData>`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717693414
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
I know users can pass in `writeParallelism` via builder. but I am saying it is not applied to the writer operator. it is currently applied to `rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());` after the `sinkTo`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718261189
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
Review Comment:
Do we need this (upsertMode/dataFileFormat/targetDataFileSize and workerPoolSize) as a constructor parameter? When called these are calculated from the `flinkWriteConf` anyway, which we get as an input too
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714293215
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/ManifestOutputFileFactory.java:
##########
@@ -41,6 +42,19 @@ class ManifestOutputFileFactory {
private final long attemptNumber;
private final AtomicInteger fileCount = new AtomicInteger(0);
+ public ManifestOutputFileFactory(
+ Supplier<Table> tableSupplier,
+ Map<String, String> props,
+ String flinkJobId,
+ String operatorId) {
+ this.tableSupplier = tableSupplier;
+ this.props = props;
+ this.flinkJobId = flinkJobId;
+ this.operatorUniqueId = operatorId;
+ this.subTaskId = 0;
Review Comment:
I forgot to actually remove the aforementioned constructor. So now we only have 1 constructor
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "nastra (via GitHub)" <gi...@apache.org>.
nastra commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1689730551
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSinkV2Base.java:
##########
@@ -0,0 +1,362 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.typeutils.RowTypeInfo;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.types.Row;
+import org.apache.flink.types.RowKind;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.data.IcebergGenerics;
+import org.apache.iceberg.data.Record;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.source.BoundedTestSource;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.StructLikeSet;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
Review Comment:
new tests shouldn't be using JUnit4 assertions. Please use AssertJ everywhere
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2371560378
> Hi, Is there a plan to replace the previous implementation (`FlinkSink`) with the new one (`IcebergSink`) also for dynamic tables (in `FlinkDynamicTableFactory`)? When it will happen?
Hi @arkadius
new IcebergSink will be released with 1.7.0
Both sink implementation will coexist for a while.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2322019030
It will be released in Iceberg 1.7.0
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2375119421
> > Hi @rodmeneses, by everything works I meant that I did some manual tests and the results were the same as with the old one. Probably "everything" was an overkill here ;-) Yes, I can take this. I'll let you know on the slack if I need any help with this task.
>
> Amazing! Thanks!! Definitely tag me for reviews!
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1697550150
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
Review Comment:
yes, I think so.
In the V1 Sink, we also have a code like the above when we do class cast without doing explicit check first:
https://github.com/apache/iceberg/blob/main/flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/FlinkSink.java#L470
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1723920526
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ inputStream.name(operatorName(newUidSuffix)).uid(newUidSuffix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link SerializableTable} instance is used for initializing {@link
+ * IcebergStreamWriter} which will write all the records into {@link DataFile}s and emit them to
+ * downstream operator. Providing a table would avoid so many table loading from each separate
+ * task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = (SerializableTable) SerializableTable.copyOf(newTable);
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link
+ * org.apache.iceberg.flink.sink.IcebergCommitter} lazily, we need this loader because {@link
+ * Table} is not serializable and could not just use the loaded table from Builder#table in the
+ * remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid suffix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator). Actual operator uid will be
+ * prepended with a prefix like "Sink Committer: uidSuffix".
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newSuffix suffix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidSuffix(String newSuffix) {
+ this.uidSuffix = newSuffix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidSuffix != null ? uidSuffix + "-" + suffix : suffix;
+ }
+
+ IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ this.table = checkAndGetTable(tableLoader(), table);
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSupplier = new CachingTableSupplier(table, tableLoader(), tableRefreshInterval);
+ } else {
+ SerializableTable serializableTable = (SerializableTable) SerializableTable.copyOf(table);
Review Comment:
| what is the error/exception you are seeing after removed the 1st global?
TestIcebergSink failures for parallelism > 1:
```
java.lang.UnsupportedOperationException: Forward partitioning does not allow change of parallelism. Upstream operation: Sink: Writer-36 parallelism: 2, downstream operation: Sink: -hadoop.default.t-e254fac5-1ac5-40f1-ac6f-87cf37af0de9-pre-commit-topology-38 parallelism: 1 You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global.
at org.apache.flink.streaming.api.graph.StreamGraph.createActualEdge(StreamGraph.java:697)
at org.apache.flink.streaming.api.graph.StreamGraph.addEdgeInternal(StreamGraph.java:649)
at org.apache.flink.streaming.api.graph.StreamGraph.addEdgeInternal(StreamGraph.java:639)
at org.apache.flink.streaming.api.graph.StreamGraph.addEdge(StreamGraph.java:596)
at org.apache.flink.streaming.api.graph.StreamGraph.addEdge(StreamGraph.java:588)
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596974983
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,197 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
+ * only {@link WriteResult} objects) to a single {@link SinkCommittable} per checkpoint (storing the
+ * serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<SinkCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(SinkAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+
+ private transient boolean initialized = false;
+
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public SinkAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ super.open();
+ if (!initialized) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
Review Comment:
nit: newline
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591971340
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ // end new
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+ // init
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
Review Comment:
nit: newline
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1593129422
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
+ private static final Configuration CONF = new Configuration();
Review Comment:
yes, on `writeDataFile` method
```java
return SimpleDataUtil.writeFile(
table,
table.schema(),
table.spec(),
CONF,
table.location(),
format.addExtension(filename),
rows);
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592005960
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSink.java:
##########
@@ -0,0 +1,399 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+// Like TestFlinkIcebergSink but only using the new IcebergSink
+@RunWith(Parameterized.class)
+public class TestIcebergSink extends TestFlinkIcebergSinkBase {
+
+ @ClassRule
+ public static final MiniClusterWithClientResource MINI_CLUSTER_RESOURCE =
+ MiniClusterResource.createWithClassloaderCheckDisabled();
+
+ @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder();
+
+ @Rule
+ public final HadoopCatalogResource catalogResource =
+ new HadoopCatalogResource(TEMPORARY_FOLDER, TestFixtures.DATABASE, TestFixtures.TABLE);
+
+ private TableLoader tableLoader;
+
+ private final FileFormat format;
+ private final int parallelism;
+ private final boolean partitioned;
+
+ @Parameterized.Parameters(name = "format={0}, parallelism = {1}, partitioned = {2}")
+ public static Object[][] parameters() {
+ return new Object[][] {
+ {"avro", 1, true},
+ {"avro", 1, false},
+ {"avro", 2, true},
+ {"avro", 2, false},
+ {"orc", 1, true},
+ {"orc", 1, false},
+ {"orc", 2, true},
+ {"orc", 2, false},
+ {"parquet", 1, true},
+ {"parquet", 1, false},
+ {"parquet", 2, true},
+ {"parquet", 2, false}
+ };
+ }
+
+ public TestIcebergSink(String format, int parallelism, boolean partitioned) {
+ this.format = FileFormat.fromString(format);
+ this.parallelism = parallelism;
+ this.partitioned = partitioned;
+ }
+
+ @Before
+ public void before() throws IOException {
+ table =
+ catalogResource
+ .catalog()
+ .createTable(
+ TestFixtures.TABLE_IDENTIFIER,
+ SimpleDataUtil.SCHEMA,
+ partitioned
+ ? PartitionSpec.builderFor(SimpleDataUtil.SCHEMA).identity("data").build()
+ : PartitionSpec.unpartitioned(),
+ ImmutableMap.of(TableProperties.DEFAULT_FILE_FORMAT, format.name()));
+
+ env =
+ StreamExecutionEnvironment.getExecutionEnvironment(
+ MiniClusterResource.DISABLE_CLASSLOADER_CHECK_CONFIG)
+ .enableCheckpointing(100)
+ .setParallelism(parallelism)
+ .setMaxParallelism(parallelism);
+
+ tableLoader = catalogResource.tableLoader();
+ }
+
+ @Test
+ public void testWriteRowData() throws Exception {
+ List<Row> rows = Lists.newArrayList(Row.of(1, "hello"), Row.of(2, "world"), Row.of(3, "foo"));
+ DataStream<RowData> dataStream =
+ env.addSource(createBoundedSource(rows), ROW_TYPE_INFO)
+ .map(CONVERTER::toInternal, FlinkCompatibilityUtil.toTypeInfo(SimpleDataUtil.ROW_TYPE));
+
+ IcebergSink.forRowData(dataStream)
+ .table(table)
+ .tableLoader(tableLoader)
+ .writeParallelism(parallelism)
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream");
+
+ // Assert the iceberg table's records.
+ SimpleDataUtil.assertTableRows(table, convertToRowData(rows));
+ }
+
+ private void testWriteRow(TableSchema tableSchema, DistributionMode distributionMode)
+ throws Exception {
+ List<Row> rows = createRows("");
+ DataStream<Row> dataStream = env.addSource(createBoundedSource(rows), ROW_TYPE_INFO);
+
+ IcebergSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA)
+ .table(table)
+ .tableLoader(tableLoader)
+ .tableSchema(tableSchema)
+ .writeParallelism(parallelism)
+ .distributionMode(distributionMode)
+ .append();
+
+ // Execute the program.
+ env.execute("Test Iceberg DataStream.");
+
+ SimpleDataUtil.assertTableRows(table, convertToRowData(rows));
+ }
+
+ private int partitionFiles(String partition) throws IOException {
+ return SimpleDataUtil.partitionDataFiles(table, ImmutableMap.of("data", partition)).size();
+ }
Review Comment:
nit: Please move these to the bottom of the class
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1593406692
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
+ private static final Configuration CONF = new Configuration();
Review Comment:
Let me rephrase: It's only used in a single place, do we really need a constant for it? 😄
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1607625268
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
Review Comment:
Use `RewriteDataFilesAction` in Spark, or Flink could help you with this. Also this PR is part of the ongoing work to provide continuous table maintenance in Flink
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1619378283
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
Review Comment:
Hi @stevenzwu
I don't really see much benefit in doing that refactor, as it mostly feel like coding style, but if you feel strong about it, please let me know. Thanks!
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1619419797
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
Review Comment:
```
global forces all output records send to subtask 0 of the downstream committer operator. This is to ensure commit only happen in one committer subtask. Once upstream Flink provides the capability of setting committer operator parallelism to 1, this can be removed.
link to Flink discussion thread.
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1634003009
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/SinkTestUtil.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+
+public class SinkTestUtil {
+
+ private SinkTestUtil() {}
+
+ @SuppressWarnings("unchecked")
+ public static List<StreamElement> fromOutput(Collection<Object> elements) {
+ return elements.stream()
+ .map(
+ element -> {
+ if (element instanceof StreamRecord) {
+ return new StreamRecord<>(
+ ((StreamRecord<CommittableMessage<?>>) element).getValue());
+ }
+ return (StreamElement) element;
+ })
+ .collect(Collectors.toList());
+ }
+
+ public static CommittableSummary<?> toCommittableSummary(StreamElement element) {
Review Comment:
as this is a unit test helper method, I don't see a problem with using `assertThat` inside of it. It's clear from the code what was the original intention of the author of this method.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1634032064
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2.java:
##########
@@ -140,8 +141,7 @@ public void testCheckAndGetEqualityFieldIds() {
DataStream<Row> dataStream =
env.addSource(new BoundedTestSource<>(ImmutableList.of()), ROW_TYPE_INFO);
- FlinkSink.Builder builder =
- FlinkSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
+ Builder builder = IcebergSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
Review Comment:
thanks for pointing this out. I did create separate tests to assert the new functionality. In this case, though, the naming of the unit test file got in the middle, and I didn't realize that I was switching existing tests.
In the current codebase we have unit tests with these names:
1. TestFlinkIcebergSinkV2
2. TestFlinkIcebergSink
3. TestFlinkIcebergSinkV2Base
4. TestFlinkIcebergSinkBase
5. TestFlinkIcebergSinkV2Branch
6. TestFlinkIcebergSinkBranch
I will introduce new ones called
1. TestIcebergSink
2. TestIcebergSinkBase
3. TestIcebergSinkV2
4. TestIcebergSinkV2Base
5. TestIcebergSinkBranch
6. TestIcebergSinkV2Branch
and the original 6 will keep the same
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2066090239
Please update the description of the PR. Also link the previous versions, docs, relevant stuff. So in the future it is easier to find them
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587870550
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergManifestOutputFileFactory.java:
##########
@@ -0,0 +1,80 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.HasTableOperations;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableOperations;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
+
+public class IcebergManifestOutputFileFactory {
Review Comment:
How much is the difference between this, and the `ManifestOutputFileFactory`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1707846715
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/SinkUtil.java:
##########
@@ -0,0 +1,61 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SinkUtil {
Review Comment:
this class and its methods can probably be package private
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
then how we set the sink v2 parallelism? basically we need to be able to apply the writer parallelism properly. current code is not correct
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714330295
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
Review Comment:
renamed to this test to `testSingleCommit` to cover the most basic test when we commit a single row, and then assert that we can read it back
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717685655
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
Review Comment:
`table` can be defined as `SerializableTable`. then we do need type cast here
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717695753
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
Review Comment:
`tableSupplier` is a stack local variable (not a class variable)
```
SerializableSupplier<Table> tableSupplier;
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718120854
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSinkWriter.java:
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * org.apache.iceberg.flink.sink.IcebergSink} (SinkV2). Writes out the data to the final place, and
+ * emits a single {@link WriteResult} at every checkpoint for every data/delete file created by this
+ * writer.
+ */
+class IcebergSinkWriter implements CommittingSinkWriter<RowData, WriteResult> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSinkWriter.class);
+
+ private final String fullTableName;
+ private final TaskWriterFactory<RowData> taskWriterFactory;
+ private final IcebergStreamWriterMetrics metrics;
+ private TaskWriter<RowData> writer;
+ private final int subTaskId;
+ private final int attemptId;
+
+ IcebergSinkWriter(
+ String fullTableName,
+ TaskWriterFactory<RowData> taskWriterFactory,
+ IcebergStreamWriterMetrics metrics,
+ int subTaskId,
+ int attemptId) {
+ this.fullTableName = fullTableName;
+ this.taskWriterFactory = taskWriterFactory;
+ // Initialize the task writer factory.
+ taskWriterFactory.initialize(subTaskId, attemptId);
+ // Initialize the task writer.
+ this.writer = taskWriterFactory.create();
+ this.metrics = metrics;
+ this.subTaskId = subTaskId;
+ this.attemptId = attemptId;
+ LOG.debug(
+ "Created Stream Writer for table {} subtask {} attemptId {}",
+ fullTableName,
+ subTaskId,
+ attemptId);
+ }
+
+ @Override
+ public void write(RowData element, Context context) throws IOException, InterruptedException {
+ writer.write(element);
+ }
+
+ @Override
+ public void flush(boolean endOfInput) {
+ // flush is used to handle flush/endOfInput, so no action is taken here.
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("table_name", fullTableName)
+ .add("subtask_id", subTaskId)
+ .add("attempt_id", attemptId)
Review Comment:
nit: Could we just change this to, it hurts my eyes:
```
.add("tableName", fullTableName)
.add("subtaskId", subTaskId)
.add("attemptId", attemptId)
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718169335
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2Base.java:
##########
@@ -362,7 +362,7 @@ protected Record record(int id, String data) {
return SimpleDataUtil.createRecord(id, data);
}
- private List<Snapshot> findValidSnapshots() {
+ protected List<Snapshot> findValidSnapshots() {
Review Comment:
nit: could this be package private?
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2Base.java:
##########
@@ -373,11 +373,11 @@ private List<Snapshot> findValidSnapshots() {
return validSnapshots;
}
- private StructLikeSet expectedRowSet(Record... records) {
+ protected StructLikeSet expectedRowSet(Record... records) {
Review Comment:
nit: could this be package private?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714462484
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
Review Comment:
correct
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719092107
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ public IcebergSink build() {
Review Comment:
if we are looking at parity with `FlinkSink`, this is not provided there. I actually think it is simpler to provide one way of building thing. here is how users would use `FlinkSink`. Users can directly replace `FlinkSink` with `IcebergSink` since we are trying to mimicking the APIs in `IcebergSink`, which can make the code switch straightforward.
Anyway, I prefer we make this private for now. Even if we decided it is needed down the road, we can make it public. It is easy to make a private method public. But it would be painful the deprecate a public API that we found not necessary later.
```
FlinkSink.forRowData(dataStream)
.table(table)
.tableLoader(tableLoader)
.writeParallelism(parallelism)
.append();
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1612188278
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
Review Comment:
I wouldn't want to wait for another release from Flink for this. The SinkV2 will be functional without this so I don't theink we need to block the whole post commit compaction on this.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626390005
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link IcebergCommittable} (jobId/operatorId/checkpointId). Only used by
+ * {@link IcebergCommittable} (SinkV2).
+ *
+ * @param requests The {@link IcebergCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
Review Comment:
the new `getMaxCommittedCheckpointId` has a totally different signature from the existing `IcebergFilesCommitter.getMaxCommittedCheckpointId`, so I don't think it is worth refactoring it which will require approches like
----
1. Create a new `IcebergComitterCommon` class
2. create generic static `getMaxCommittedCheckpointId`
3. Refactor current `IcebergFilesCommitter.getMaxCommittedCheckpointId` to use the new static method
4. Refactor the new `IcebergCommitter.getMaxCommittedCheckpointId`
OR
----
1. Make both `IcebergFilesCommitter.getMaxCommittedCheckpointId` and even `IcebergFilesCommitter` public
2. refactor `IcebergFilesCommitter.getMaxCommittedCheckpointId`
3. Have the new `IcebergComitter` to use `IcebergFilesCommitter.getMaxCommittedCheckpointId`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626393891
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
Review Comment:
adding the empty commit will make the code to keep flowing and reaching `commitPendingResult` method
so that this code is executed and `continuousEmptyCheckpoints` is updated
```java
continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626290543
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
Review Comment:
@stevenzwu
Existing code (`IcebergFilesCommitter` from 1.17 and 1.18) is using this pattern
```
private static final long INITIAL_CHECKPOINT_ID = -1L;
```
as `IcebergCommitter` is a new addition for SinkV2, I will do the changes there, but I will not change existing behavior
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626559065
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+
+ this.table = tableLoader.loadTable();
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(() -> table, table.properties(), prefix);
+ }
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<IcebergCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
+ results.clear();
+ }
+
+ /**
+ * Write all the completed data files to a newly created manifest file and return the manifest's
+ * avro serialized bytes.
+ */
+ public byte[] writeToManifest(Collection<WriteResult> writeResults, long checkpointId)
+ throws IOException {
+ if (writeResults.isEmpty()) {
+ return EMPTY_MANIFEST_DATA;
+ }
+
+ WriteResult result = WriteResult.builder().addAll(writeResults).build();
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ result, () -> icebergManifestOutputFileFactory.create(checkpointId), table.spec());
+
+ return SimpleVersionedSerialization.writeVersionAndSerialize(
+ DeltaManifestsSerializer.INSTANCE, deltaManifests);
+ }
+
+ /**
+ * Write the {@link WriteResult} to temporary manifest files.
+ *
+ * @param result all those DataFiles/DeleteFiles in this WriteResult should be written with same
+ * partition spec
+ */
+ public static DeltaManifests writeCompletedFiles(
+ WriteResult result, Supplier<OutputFile> outputFileSupplier, PartitionSpec spec)
+ throws IOException {
+
+ ManifestFile dataManifest = null;
+ ManifestFile deleteManifest = null;
+
+ // Write the completed data files into a newly created data manifest file.
+ if (result.dataFiles() != null && result.dataFiles().length > 0) {
+ dataManifest =
+ FlinkManifestUtil.writeDataFiles(
+ outputFileSupplier.get(), spec, Lists.newArrayList(result.dataFiles()));
+ }
+
+ // Write the completed delete files into a newly created delete manifest file.
+ if (result.deleteFiles() != null && result.deleteFiles().length > 0) {
+ OutputFile deleteManifestFile = outputFileSupplier.get();
+
+ ManifestWriter<DeleteFile> deleteManifestWriter =
+ ManifestFiles.writeDeleteManifest(FORMAT_V2, spec, deleteManifestFile, DUMMY_SNAPSHOT_ID);
+ try (ManifestWriter<DeleteFile> writer = deleteManifestWriter) {
+ for (DeleteFile deleteFile : result.deleteFiles()) {
+ writer.add(deleteFile);
+ }
+ }
+
+ deleteManifest = deleteManifestWriter.toManifestFile();
+ }
+
+ return new DeltaManifests(dataManifest, deleteManifest, result.referencedDataFiles());
+ }
+
+ @Override
+ public void processElement(StreamRecord<CommittableMessage<WriteResult>> element)
+ throws Exception {
+ if (element.isRecord() && element.getValue() instanceof CommittableWithLineage) {
Review Comment:
@stevenzwu did you mean that if `element.getValue()` is `true` but `element.getValue() instanceof CommittableWithLineage` is false, we should throw an error?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "hililiwei (via GitHub)" <gi...@apache.org>.
hililiwei commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626868928
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<IcebergCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
Review Comment:
+1
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1572022378
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkV2Aggregator.java:
##########
@@ -0,0 +1,127 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkV2Committable} results from the writer
+ * (storing only {@link WriteResult} objects) to a single {@link SinkV2Committable} per checkpoint
+ * (storing the serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkV2Aggregator extends AbstractStreamOperator<CommittableMessage<SinkV2Committable>>
Review Comment:
Could we remove the V2 from the name?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1572021707
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/DeltaManifests.java:
##########
@@ -0,0 +1,71 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.util.List;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+
+public class DeltaManifests {
Review Comment:
Is this different in anyway from the current `DeltaManifests`?
If not, we should reuse the old one, or move it here. Having 2 classes with the same implementation should not happen
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591981824
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ // end new
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+ // init
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+ sink.initTable = sink.tableSupplier.get();
+ this.uidPrefix = uidPrefix == null ? "" : uidPrefix;
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ public static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = builder.table.schema();
+ PartitionSpec spec = builder.table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (builder.table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ builder.equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ // end new stuff
+ private IcebergSink(IcebergSink.Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotProperties;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ /**
+ * Sets the currently used committer. Should be used for testing purposes only. Needs to be public
+ * as {@link IcebergSink} is used to test the {@link SinkCommitter} functionality.
+ *
+ * @param newCommitter The committer used for testing
+ */
+ public void setCommitter(Committer<SinkCommittable> newCommitter) {
+ this.sinkV2Committer = newCommitter;
+ }
+
+ // @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer_1() {
+ return new SinkCommittableSerializer();
+ }
+
+ // @Override
+ public void addPostCommitTopology_1(
+ DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from generic input data stream into iceberg
+ * table. We use {@link RowData} inside the sink connector, so users need to provide a mapper
+ * function and a {@link TypeInformation} to convert those generic records to a RowData
+ * DataStream.
+ *
+ * @param input the generic source input data stream.
+ * @param mapper function to convert the generic data to {@link RowData}
+ * @param outputType to define the {@link TypeInformation} for the input data.
+ * @param <T> the data type of records.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static <T> IcebergSink.Builder builderFor(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ return new Builder().forMapperOutputType(input, mapper, outputType);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link Row}s into
+ * iceberg table. We use {@link RowData} inside the sink connector, so users need to provide a
+ * {@link TableSchema} for builder to convert those {@link Row}s to a {@link RowData} DataStream.
+ *
+ * @param input the source input data stream with {@link Row}s.
+ * @param tableSchema defines the {@link TypeInformation} for input data.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static IcebergSink.Builder forRow(DataStream<Row> input, TableSchema tableSchema) {
+ return new Builder().forRow(input, tableSchema);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link RowData}s
+ * into iceberg table.
+ *
+ * @param input the source input data stream with {@link RowData}s.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static IcebergSink.Builder forRowData(DataStream<RowData> input) {
+ return new Builder().forRowData(input);
+ }
+
+ static String prefixIfNotNull(String uidPrefix, String suffix) {
Review Comment:
Why is this not private?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596982907
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,465 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ private final TableLoader tableLoader;
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+ private transient boolean initialized = false;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix) {
+ this.tableLoader = tableLoader;
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ LOG.info("Noting to commit");
+ return;
+ }
+
+ if (!initialized) {
Review Comment:
I think we can do this in the constructor
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1593409234
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.committer.IcebergManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeDataFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readDataFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
+ private static final Configuration CONF = new Configuration();
Review Comment:
But only once
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.committer.IcebergManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeDataFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readDataFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
+ private static final Configuration CONF = new Configuration();
Review Comment:
But only once
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596998147
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergSinkWriter.java:
##########
@@ -0,0 +1,122 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.sink.TaskWriterFactory;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * IcebergSink} (SinkV2). Writes out the data to the final place, and emits a single {@link
+ * SinkCommittable} on {@link CommittingSinkWriter#prepareCommit()} at every checkpoint. The emitted
Review Comment:
emits a `WriteResult`. Please revisit the comment
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599433519
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,439 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
Review Comment:
nit: could we please order the constants based on the visibility
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599431142
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommittable.java:
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.Serializable;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+
+/**
+ * The aggregated results of a single checkpoint which should be committed. Containing the
+ * serialized {@link org.apache.iceberg.flink.sink.DeltaManifests} file - which contains the commit
+ * data, and the jobId, operatorId, checkpointId triplet which helps identifying the specific commit
+ *
+ * <p>{@link SinkCommittableSerializer} is used for serializing the objects between the Writer and
+ * the Aggregator operator and between the Aggregator and the Committer as well.
+ */
+public class SinkCommittable implements Serializable {
+ private final byte[] manifest;
+ private final String jobId;
+ private final String operatorId;
+ private final long checkpointId;
+
+ public SinkCommittable(byte[] manifest, String jobId, String operatorId, long checkpointId) {
+ this.manifest = manifest;
+ this.jobId = jobId;
+ this.operatorId = operatorId;
+ this.checkpointId = checkpointId;
+ }
+
+ public byte[] manifest() {
+ return manifest;
+ }
+
+ public String jobId() {
+ return jobId;
+ }
+
+ public String operatorId() {
+ return operatorId;
+ }
+
+ public Long checkpointId() {
+ return checkpointId;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("jobId", jobId)
+ .add("checkpointId", checkpointId)
Review Comment:
Can we add the `operatorId` to the method as well?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599443353
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2.java:
##########
@@ -140,8 +140,8 @@ public void testCheckAndGetEqualityFieldIds() {
DataStream<Row> dataStream =
env.addSource(new BoundedTestSource<>(ImmutableList.of()), ROW_TYPE_INFO);
- FlinkSink.Builder builder =
- FlinkSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
+ IcebergSink.Builder builder =
Review Comment:
What was the original intention of this test? Why it was V2? Do we still has test for that case?
Same question for `TestFlinkIcebergSinkV2Base`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587796859
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/RowDataTaskWriterFactory.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.iceberg.flink.sink.writer;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionKey;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.RowDataWrapper;
+import org.apache.iceberg.flink.sink.CachingTableSupplier;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.OutputFileFactory;
+import org.apache.iceberg.io.PartitionedFanoutWriter;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.UnpartitionedWriter;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.ArrayUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+
+public class RowDataTaskWriterFactory implements TaskWriterFactory<RowData> {
+ private final Supplier<Table> tableSupplier;
+ private final Schema schema;
+ private final RowType flinkSchema;
+ private final PartitionSpec spec;
+ private final long targetFileSizeBytes;
+ private final FileFormat format;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsert;
+ private final FileAppenderFactory<RowData> appenderFactory;
+
+ private transient OutputFileFactory outputFileFactory;
+
+ public RowDataTaskWriterFactory(
+ Table table,
+ RowType flinkSchema,
+ long targetFileSizeBytes,
+ FileFormat format,
+ Map<String, String> writeProperties,
+ List<Integer> equalityFieldIds,
+ boolean upsert) {
+ this(
+ () -> table,
+ flinkSchema,
+ targetFileSizeBytes,
+ format,
+ writeProperties,
+ equalityFieldIds,
+ upsert);
+ }
+
+ public RowDataTaskWriterFactory(
+ SerializableSupplier<Table> tableSupplier,
+ RowType flinkSchema,
+ long targetFileSizeBytes,
+ FileFormat format,
+ Map<String, String> writeProperties,
+ List<Integer> equalityFieldIds,
+ boolean upsert) {
+ this.tableSupplier = tableSupplier;
+
+ Table table;
+ if (tableSupplier instanceof CachingTableSupplier) {
Review Comment:
I do not get this - we should not read any of the table metadata after the writer has been initialised for now.
Let's talk about this.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587845482
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,771 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.sink.writer.RowDataTaskWriterFactory;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperties(Map<String, String> properties) {
+ snapshotProperties.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder setSnapshotProperty(String property, String value) {
+ snapshotProperties.put(property, value);
+ return this;
+ }
+
+ public IcebergSink.Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ @VisibleForTesting
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ // end new
+ public IcebergSink build() {
+ IcebergSink sink = new IcebergSink(this);
+ // validations
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ sink.equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ sink.flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ sink.branch = sink.flinkWriteConf.branch();
+ sink.overwriteMode = sink.flinkWriteConf.overwriteMode();
+ sink.workerPoolSize = sink.flinkWriteConf.workerPoolSize();
+ if (sink.flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !sink.flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !sink.equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ sink.equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+ // init
+ sink.writeProperties =
+ writeProperties(table, sink.flinkWriteConf.dataFileFormat(), sink.flinkWriteConf);
+ sink.flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = sink.flinkWriteConf.tableRefreshInterval();
+
+ if (tableRefreshInterval != null) {
+ sink.tableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ sink.tableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+ sink.initTable = sink.tableSupplier.get();
+ this.uidPrefix = uidPrefix == null ? "" : uidPrefix;
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ sink.upsertMode = sink.flinkWriteConf.upsertMode();
+ sink.dataFileFormat = sink.flinkWriteConf.dataFileFormat();
+ sink.targetDataFileSize = sink.flinkWriteConf.targetDataFileSize();
+ return sink;
+ }
Review Comment:
nit: newline
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587851360
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/SimpleTableSupplier.java:
##########
@@ -0,0 +1,45 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.util.SerializableSupplier;
+
+/**
+ * A table loader that will only reload a table after a certain interval has passed. WARNING: This
+ * table loader should be used carefully when used with writer tasks. It could result in heavy load
+ * on a catalog for jobs with many writers.
+ */
+@Internal
+public class SimpleTableSupplier implements SerializableSupplier<Table> {
Review Comment:
This implementation does not refresh anything... so the class comment doesn't make sense.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587870550
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergManifestOutputFileFactory.java:
##########
@@ -0,0 +1,80 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.HasTableOperations;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableOperations;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
+
+public class IcebergManifestOutputFileFactory {
Review Comment:
How much is the difference this, and the `ManifestOutputFileFactory`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591967898
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
Review Comment:
This is not true - we use a different committer.
Please check the comments
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1592011406
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
+ private static final Configuration CONF = new Configuration();
+
+ @TempDir File temporaryFolder;
+
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private FileFormat format;
+
+ @Parameter(index = 2)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 3)
+ private Boolean isCheckpointingEnabled;
+
+ private final long dataFIleRowCount = 5L;
+ private SinkAggregator sinkAggregator;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @Parameters(name = "format={0}, isStreaming={1}, isCheckpointingEnabled={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (FileFormat format :
+ new FileFormat[] {FileFormat.PARQUET, FileFormat.AVRO, FileFormat.ORC}) {
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (Boolean isCheckpointingEnabled : new Boolean[] {true, false}) {
+ parameters.add(new Object[] {2, format, isBatchMode, isCheckpointingEnabled});
+ }
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.DEFAULT_FILE_FORMAT, format.name(),
+ TableProperties.FORMAT_VERSION, String.valueOf(formatVersion));
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+
+ sinkAggregator = new SinkAggregator(tableLoader, "flink_job");
+ sinkAggregator.open();
+ }
+
+ // public TestSinkCommitter(
+ // int formatVersion, FileFormat format, Boolean isBatchMode, Boolean isCheckpointingEnabled)
+ // {
+ // this.formatVersion = formatVersion;
+ // this.format = format;
+ // this.isBatchMode = isBatchMode;
+ // this.isCheckpointingEnabled = isCheckpointingEnabled;
+ // }
Review Comment:
remove
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1593132076
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergFlinkManifest.java:
##########
@@ -0,0 +1,306 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.committer.IcebergManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkAggregator.writeDataFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readCompletedFiles;
+import static org.apache.iceberg.flink.sink.committer.SinkCommitter.readDataFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestIcebergFlinkManifest {
+ private static final Configuration CONF = new Configuration();
Review Comment:
yes in `writeDataFile` method:
```java
private DataFile writeDataFile(String filename, List<RowData> rows) throws IOException {
return SimpleDataUtil.writeFile(
table,
table.schema(),
table.spec(),
CONF,
table.location(),
FileFormat.PARQUET.addExtension(filename),
rows);
}
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1593131221
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkCommitter.java:
##########
@@ -0,0 +1,812 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestSinkCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestSinkCommitter.class);
Review Comment:
yes, in the `commit` method
```java
commitRequests.forEach(
one -> LOG.info("Committed with numberOfRetries: {}", one.getNumberOfRetries()));
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591338007
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/BaseDeltaTaskWriter.java:
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionKey;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.StructLike;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.RowDataWrapper;
+import org.apache.iceberg.flink.data.RowDataProjection;
+import org.apache.iceberg.io.BaseTaskWriter;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.OutputFileFactory;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+
+abstract class BaseDeltaTaskWriter extends BaseTaskWriter<RowData> {
Review Comment:
none!, will refactor
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596997524
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/WriteResultSerializer.java:
##########
@@ -0,0 +1,60 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+import org.apache.flink.util.InstantiationUtil;
+import org.apache.iceberg.io.WriteResult;
+
+public class WriteResultSerializer implements SimpleVersionedSerializer<WriteResult> {
+ @Override
+ public int getVersion() {
+ return 0;
+ }
+
+ @Override
+ public byte[] serialize(WriteResult writeResult) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+ byte[] result = InstantiationUtil.serializeObject(writeResult);
+ view.writeInt(result.length);
Review Comment:
Add a version too, so if we want to move forward from here, then we can keep the state.
```
int version = in.readInt();
```
```
out.writeInt(this..getVersion());
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1714320355
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1452 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommitableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ @TempDir File temporaryFolder;
+ @TempDir File flinkManifestFolder;
+ private Table table;
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isBatchMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isBatchMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isBatchMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isBatchMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ final Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", "flink-sink")
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+ long timestamp = 0;
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness();
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, "flink-sink", dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, "flink-sink", dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+
+ @TestTemplate
+ public void testEmitCommittablesMock() throws Exception {
+ final ForwardingCommitter committer = new ForwardingCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+ String jobId1 = "jobId1";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true));
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId1, "flink-sink")
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId1,
+ "flink-sink",
+ 1L);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ testHarness.close();
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+ preJobTestHarness.open();
+ preJobTestHarness.setup();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ preJobTestHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(0);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 0);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness();
+
+ preJobTestHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = preJobTestHarness.snapshot(checkpointId, 2L);
+ // commit snapshot
+ preJobTestHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(preJobTestHarness.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ preJobTestHarness.close();
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId1, 0L);
+ table.refresh();
+ long proJobSnapshotId = table.snapshot(branch).snapshotId();
+
+ String jobId2 = "jobId2";
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // The committed data will not be recommitted, but the commit records are emitted
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ assertThat(output2).hasSize(2);
+
+ table.refresh();
+ long restoredSnapshotId = table.snapshot(branch).snapshotId();
+
+ assertThat(restoredSnapshotId)
+ .as("The table does not generate a new snapshot without data being committed.")
+ .isEqualTo(proJobSnapshotId);
+
+ assertThat(table.snapshot(branch).summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ // Commit new data file
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output3 = transformsToStreamElement(restored.getOutput());
+ assertThat(output3).hasSize(4);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(Long.parseLong(currentSnapshot2.summary().get(SnapshotSummary.TOTAL_RECORDS_PROP)))
+ .isEqualTo(dataFIleRowCount * 2);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testStateRestoreFromCurrJob() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ final OperatorSubtaskState snapshot = testHarness.snapshot(checkpointId, 2L);
+
+ assertThat(testHarness.getOutput()).isEmpty();
+ testHarness.close();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness();
+
+ restored.setup(committableMessageTypeSerializer);
+
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ final List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ String jobId2 = "jobId2";
+ checkpointId = 1;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, "flink-sink", dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+ restored.close();
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1L);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ @TestTemplate
+ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Exception {
+ // We've two steps in checkpoint: 1. snapshotState(ckp); 2. notifyCheckpointComplete(ckp). It's
+ // possible that we
+ // flink job will restore from a checkpoint with only step#1 finished.
+ long checkpointId = 0;
+ long timestamp = 0;
+ OperatorSubtaskState snapshot;
+ List<RowData> expectedRows = Lists.newArrayList();
+
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row = SimpleDataUtil.createRowData(1, "hello");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-1", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(), branch);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+ assertFlinkManifests(1);
+ }
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0L);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ // Did not write any new record, so it won't generate new manifest.
+ assertFlinkManifests(0);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(1);
+
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 0);
+
+ RowData row = SimpleDataUtil.createRowData(2, "world");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-2", ImmutableList.of(row));
+ processElement(jobId, checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ snapshot = harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(0);
+ }
+
+ // Redeploying flink job from external checkpoint.
+ JobID newJobId = new JobID();
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.initializeState(snapshot);
+ harness.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), -1);
+ assertMaxCommittedCheckpointId(jobId, operatorId.toString(), 2);
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+
+ RowData row = SimpleDataUtil.createRowData(3, "foo");
+ expectedRows.add(row);
+ DataFile dataFile = writeDataFile("data-3", ImmutableList.of(row));
+ processElement(
+ newJobId.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(++checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(newJobId.toString(), operatorId.toString(), 3);
+ }
+ }
+
+ @TestTemplate
+ public void testStartAnotherJobToWriteSameTable() throws Exception {
+ long checkpointId = 0;
+
+ List<RowData> rows = Lists.newArrayList();
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID oldJobId = new JobID();
+ OperatorID oldOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ oldOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), -1L);
+
+ for (int i = 1; i <= 3; i++) {
+ rows.add(SimpleDataUtil.createRowData(i, "hello" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+ processElement(
+ oldJobId.toString(), checkpointId, harness, 1, oldOperatorId.toString(), dataFile);
+
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), checkpointId);
+ checkpointId++;
+ }
+ }
+
+ // The new started job will start with checkpoint = 1 again.
+ checkpointId = 0;
+ JobID newJobId = new JobID();
+ OperatorID newOperatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.open();
+ newOperatorId = harness.getOperator().getOperatorID();
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(oldJobId.toString(), oldOperatorId.toString(), 2);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), -1);
+
+ rows.add(SimpleDataUtil.createRowData(2, "world"));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile("data-new-1", rows);
+
+ processElement(newJobId.toString(), 0, harness, 1, newOperatorId.toString(), dataFile);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(newJobId.toString(), newOperatorId.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleJobsWriteSameTable() throws Exception {
+ long timestamp = 0;
+ List<RowData> tableRows = Lists.newArrayList();
+
+ JobID[] jobs = new JobID[] {new JobID(), new JobID(), new JobID()};
+ OperatorID[] operatorIds =
+ new OperatorID[] {new OperatorID(), new OperatorID(), new OperatorID()};
+ for (int i = 0; i < 20; i++) {
+ int jobIndex = i % 3;
+ int checkpointId = i / 3;
+ JobID jobID = jobs[jobIndex];
+ OperatorID operatorId = operatorIds[jobIndex];
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.getStreamConfig().setOperatorID(operatorId);
+ harness.setup();
+ harness.open();
+
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(
+ jobID.toString(), operatorId.toString(), checkpointId == 0 ? -1 : checkpointId - 1);
+
+ List<RowData> rows = Lists.newArrayList(SimpleDataUtil.createRowData(i, "word-" + i));
+ tableRows.addAll(rows);
+
+ DataFile dataFile = writeDataFile(String.format("data-%d", i), rows);
+
+ processElement(jobID.toString(), checkpointId, harness, 1, operatorId.toString(), dataFile);
+
+ harness.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ harness.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, tableRows, branch);
+ assertSnapshotSize(i + 1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpointId);
+ }
+ }
+ }
+
+ @TestTemplate
+ public void testMultipleSinksRecoveryFromValidSnapshot() throws Exception {
+ long checkpointId = 0;
+ long timestamp = 0;
+ List<RowData> expectedRows = Lists.newArrayList();
+ OperatorSubtaskState snapshot1;
+ OperatorSubtaskState snapshot2;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId1 = new OperatorID();
+ OperatorID operatorId2 = new OperatorID();
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.open();
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.open();
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), -1L);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-1-1", ImmutableList.of(row1));
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ snapshot1 = harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(1, "hello2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-1-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ snapshot2 = harness2.snapshot(checkpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // Only notify one of the committers
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(1);
+
+ // Only the first row is committed at this point
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), -1);
+ }
+
+ // Restore from the given snapshot
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness1 = getTestHarness();
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness2 = getTestHarness()) {
+ harness1.getStreamConfig().setOperatorID(operatorId1);
+ harness1.setup();
+ harness1.initializeState(snapshot1);
+ harness1.open();
+
+ harness2.getStreamConfig().setOperatorID(operatorId2);
+ harness2.setup();
+ harness2.initializeState(snapshot2);
+ harness2.open();
+
+ // All flink manifests should be cleaned because it has committed the unfinished iceberg
+ // transaction.
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+
+ RowData row1 = SimpleDataUtil.createRowData(2, "world1");
+ expectedRows.add(row1);
+ DataFile dataFile1 = writeDataFile("data-2-1", ImmutableList.of(row1));
+
+ checkpointId++;
+ processElement(
+ jobID.toString(), checkpointId, harness1, 1, operatorId1.toString(), dataFile1);
+
+ harness1.snapshot(checkpointId, ++timestamp);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world2");
+ expectedRows.add(row2);
+ DataFile dataFile2 = writeDataFile("data-2-2", ImmutableList.of(row2));
+ processElement(
+ jobID.toString(), checkpointId, harness2, 1, operatorId2.toString(), dataFile2);
+
+ harness2.snapshot(checkpointId, ++timestamp);
+
+ assertFlinkManifests(2);
+
+ harness1.notifyOfCompletedCheckpoint(checkpointId);
+ harness2.notifyOfCompletedCheckpoint(checkpointId);
+ assertFlinkManifests(0);
+
+ SimpleDataUtil.assertTableRows(table, expectedRows, branch);
+ assertSnapshotSize(4);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId1.toString(), checkpointId);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId2.toString(), checkpointId);
+ }
+ }
+
+ @TestTemplate
+ public void testFlinkManifests() throws Exception {
+ long timestamp = 0;
+ final long checkpoint = 10;
+
+ JobID jobID = new JobID();
+ OperatorID operatorId;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+ harness.setup();
+ harness.open();
+ operatorId = harness.getOperator().getOperatorID();
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ // harness.processElement(of(dataFile1), ++timestamp);
+ processElement(jobID.toString(), checkpoint, harness, 1, operatorId.toString(), dataFile1);
+
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), -1L);
+
+ // 1. snapshotState for checkpoint#1
+ harness.snapshot(checkpoint, ++timestamp);
+ List<Path> manifestPaths = assertFlinkManifests(1);
+ Path manifestPath = manifestPaths.get(0);
+ assertThat(manifestPath.getFileName())
+ .asString()
+ .isEqualTo(
+ String.format("%s-%s-%05d-%d-%d-%05d.avro", jobID, operatorId, 0, 0, checkpoint, 1));
+ //
+ // 2. Read the data files from manifests and assert.
+ List<DataFile> dataFiles =
+ FlinkManifestUtil.readDataFiles(
+ createTestingManifestFile(manifestPath), table.io(), table.specs());
+ assertThat(dataFiles).hasSize(1);
+ TestHelpers.assertEquals(dataFile1, dataFiles.get(0));
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(checkpoint);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobID.toString(), operatorId.toString(), checkpoint);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testHandleEndInput() throws Exception {
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ String jobId1 = "jobId1";
+ long checkpointId = 0;
+ processElement(jobId1, checkpointId, testHarness, 1, "flink-sink", dataFileTest1);
+
+ testHarness.endInput();
+
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // Future emission calls should change the output
+ testHarness.notifyOfCompletedCheckpoint(2);
+ testHarness.endInput();
+
+ assertThat(testHarness.getOutput()).hasSize(2);
+ }
+
+ @TestTemplate
+ public void testDeleteFiles() throws Exception {
+
+ assumeThat(formatVersion).as("Only support delete in format v2").isGreaterThanOrEqualTo(2);
+ FileAppenderFactory<RowData> appenderFactory = createDeletableAppenderFactory();
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness();
+
+ testHarness.open();
+
+ long checkpointId = 0;
+ RowData row1 = SimpleDataUtil.createInsert(1, "aaa");
+ DataFile dataFile1 = writeDataFile("data-file-1", ImmutableList.of(row1));
+ processElement(jobId, checkpointId, testHarness, 1, "flink-sink", dataFile1);
+
+ testHarness.snapshot(checkpointId, 0);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+
+ final List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output).hasSize(2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output.get(0)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+
+ // The 2. commit
+ checkpointId = 1;
+ RowData row2 = SimpleDataUtil.createInsert(2, "bbb");
+ DataFile dataFile2 = writeDataFile("data-file-2", ImmutableList.of(row2));
+
+ RowData row3 = SimpleDataUtil.createInsert(3, "ccc");
+ DataFile dataFile3 = writeDataFile("data-file-3", ImmutableList.of(row3));
+ processElement(jobId, checkpointId, testHarness, 2, "flink-sink", dataFile2, dataFile3);
+
+ testHarness.snapshot(checkpointId, 1);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2, row3), branch);
+
+ final List<StreamElement> output2 = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output2).hasSize(2 + 2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output2.get(2)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+
+ // The 3. commit
+ checkpointId = 2;
+ RowData delete1 = SimpleDataUtil.createDelete(1, "aaa");
+ DeleteFile deleteFile1 =
+ writeEqDeleteFile(appenderFactory, "delete-file-1", ImmutableList.of(delete1));
+ RowData row4 = SimpleDataUtil.createInsert(4, "ddd");
+ DataFile dataFile4 = writeDataFile("data-file-4", ImmutableList.of(row4));
+
+ RowData row5 = SimpleDataUtil.createInsert(5, "eee");
+ DataFile dataFile5 = writeDataFile("data-file-5", ImmutableList.of(row5));
+ WriteResult withRecord4 =
+ WriteResult.builder()
+ .addDataFiles(dataFile4, dataFile5)
+ .addDeleteFiles(deleteFile1)
+ .build();
+ processElement(withRecord4, jobId, checkpointId, testHarness, 2, "flink-sink");
+
+ testHarness.snapshot(checkpointId, 3);
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(3);
+ assertMaxCommittedCheckpointId(jobId, checkpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row2, row3, row4, row5), branch);
+
+ final List<StreamElement> output3 = transformsToStreamElement(testHarness.getOutput());
+ assertThat(output3).hasSize(2 + 2 + 2);
+ SinkV2Assertions.assertThat(extractAndAssertCommitableSummary(output3.get(4)))
+ .hasCheckpointId(checkpointId)
+ .hasPendingCommittables(0)
+ .hasOverallCommittables(1)
+ .hasFailedCommittables(0);
+ }
+
+ private ManifestFile createTestingManifestFile(Path manifestPath) {
+ return new GenericManifestFile(
+ manifestPath.toAbsolutePath().toString(),
+ manifestPath.toFile().length(),
+ 0,
+ ManifestContent.DATA,
+ 0,
+ 0,
+ 0L,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ null,
+ null);
+ }
+
+ private IcebergWriteAggregator buildIcebergWriteAggregator(String myJobId, String operatorId) {
+ IcebergWriteAggregator icebergWriteAggregator = spy(new IcebergWriteAggregator(tableLoader));
+ StreamTask ctx = mock(StreamTask.class);
+ Environment env = mock(Environment.class);
+ StreamingRuntimeContext streamingRuntimeContext = mock(StreamingRuntimeContext.class);
+ TaskInfo taskInfo = mock(TaskInfo.class);
+ JobID myJobID = mock(JobID.class);
+ OperatorID operatorID = mock(OperatorID.class);
+ doReturn(myJobId).when(myJobID).toString();
+ doReturn(myJobID).when(env).getJobID();
+ doReturn(env).when(ctx).getEnvironment();
+ doReturn(ctx).when(icebergWriteAggregator).getContainingTask();
+ doReturn(operatorId).when(operatorID).toString();
+ doReturn(operatorID).when(icebergWriteAggregator).getOperatorID();
+ doReturn(0).when(taskInfo).getAttemptNumber();
+ doReturn(taskInfo).when(streamingRuntimeContext).getTaskInfo();
+ doReturn(streamingRuntimeContext).when(icebergWriteAggregator).getRuntimeContext();
+ try {
+ icebergWriteAggregator.open();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return icebergWriteAggregator;
+ }
+
+ private CommittableSummary<IcebergCommittable> processElement(
+ WriteResult withRecord,
+ String myJobId,
+ long checkpointId,
+ OneInputStreamOperatorTestHarness testHarness,
+ int subTaskId,
+ String operatorId)
+ throws Exception {
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(myJobId, operatorId)
+ .writeToManifest(Lists.newArrayList(withRecord), checkpointId),
+ myJobId,
+ operatorId,
+ checkpointId);
+
+ final CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(subTaskId, 1, checkpointId, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ final CommittableWithLineage<IcebergCommittable> committable =
+ new CommittableWithLineage<>(commit, checkpointId, subTaskId);
+ testHarness.processElement(new StreamRecord<>(committable));
+ return committableSummary;
+ }
+
+ private CommittableSummary<IcebergCommittable> processElement(
+ String myJobID,
+ long checkpointId,
+ OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness,
+ int subTaskId,
+ String operatorId,
+ DataFile... dataFile)
+ throws Exception {
+ WriteResult withRecord = WriteResult.builder().addDataFiles(dataFile).build();
+ return processElement(withRecord, myJobID, checkpointId, testHarness, subTaskId, operatorId);
+ }
+
+ private FileAppenderFactory<RowData> createDeletableAppenderFactory() {
+ int[] equalityFieldIds =
+ new int[] {
+ table.schema().findField("id").fieldId(), table.schema().findField("data").fieldId()
+ };
+ return new FlinkAppenderFactory(
+ table,
+ table.schema(),
+ FlinkSchemaUtil.convert(table.schema()),
+ table.properties(),
+ table.spec(),
+ equalityFieldIds,
+ table.schema(),
+ null);
+ }
+
+ private List<Path> assertFlinkManifests(int expectedCount) throws IOException {
+ List<Path> manifests =
+ Files.list(flinkManifestFolder.toPath())
+ .filter(p -> !p.toString().endsWith(".crc"))
+ .collect(Collectors.toList());
+ assertThat(manifests).hasSize(expectedCount);
+ return manifests;
+ }
+
+ private DataFile writeDataFile(String filename, List<RowData> rows) throws IOException {
+ return SimpleDataUtil.writeFile(
+ table,
+ table.schema(),
+ table.spec(),
+ new Configuration(),
+ table.location(),
+ FileFormat.PARQUET.addExtension(filename),
+ rows);
+ }
+
+ private DeleteFile writeEqDeleteFile(
+ FileAppenderFactory<RowData> appenderFactory, String filename, List<RowData> deletes)
+ throws IOException {
+ return SimpleDataUtil.writeEqDeleteFile(
+ table, FileFormat.PARQUET, filename, appenderFactory, deletes);
+ }
+
+ private OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ getTestHarness() throws Exception {
+ IcebergSink sink =
+ IcebergSink.forRowData(null).table(table).toBranch(branch).tableLoader(tableLoader).build();
+
+ final OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, isBatchMode, true));
+ testHarness.setup(committableMessageTypeSerializer);
+
+ return testHarness;
+ }
+
+ // ------------------------------- Mock Classes --------------------------------
+
+ private static class ForwardingCommitter extends IcebergCommitter {
Review Comment:
changed the class name.
and yes, it needs to extend from IcebergCommiter, as the instance is mocked for `IcebergSink.createCommitter` return value
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717690006
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
Review Comment:
previously, we control the creation of all operators and apply `uidPrefix` consistently across all operators.
with v2 sink framework, some operators (like aggregator) are created by the Iceberg connector and some operators (like writer and committer) are created by the framework. The committer operator is a stateful operator. Does the framework ensure a deterministic `uid` for the committer operator or is it a random UUID as the operator id?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718118513
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
I think this is the relevant part (from the `org.apache.flink.streaming.runtime.translators.SinkTransformationTranslator`):
```
// remember that the environment parallelism has been set to be default
// at the beginning. SubTransformations, whose parallelism has been
// customized, will skip this part. The customized parallelism value set by user
// will therefore be kept.
if (subTransformation.getParallelism() == ExecutionConfig.PARALLELISM_DEFAULT) {
// In this case, the subTransformation does not contain any customized
// parallelism value and will therefore inherit the parallelism value
// from the sinkTransformation.
subTransformation.setParallelism(transformation.getParallelism());
}
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718166603
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+@Internal
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+
+ public IcebergWriteAggregator(TableLoader tableLoader) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
Review Comment:
nit: new line after the block
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1719097127
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
Review Comment:
interesting. it is not intuitive here why the first `global` is needed if the aggregator has max parallelism of 1.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718160601
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,348 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+ this.tableLoader = tableLoader;
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool(
+ "iceberg-committer-pool-" + table.name() + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+ long maxCommittedCheckpointId =
+ getMaxCommittedCheckpointId(table, last.jobId(), last.operatorId(), branch);
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> uncommitted =
+ commitRequestMap.tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitPendingRequests(uncommitted, last.jobId(), last.operatorId());
+ }
+ }
+
+ public static long getMaxCommittedCheckpointId(
Review Comment:
In every other instance (`checkAndGetEqualityFieldIds`, `deleteCommittedManifests`) we created a static util class with the method. Why is this different? Is it by mistake, or it is planned?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718103756
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,752 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
Review Comment:
Maybe something like this (fix my English if applicable):
```
* <li>{@link SupportsPreCommitTopology} which we use to place the {@link
* IcebergWriteAggregator} which merges the individual {@link
* org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
* org.apache.iceberg.io.WriteResult}s to a single {@link IcebergCommittable}
```
Removed an extra `to` and fixed the return type
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718171691
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergFilesCommitter.java:
##########
@@ -396,7 +398,7 @@ public void testRecoveryFromSnapshotWithoutCompletedNotification() throws Except
// flink job will restore from a checkpoint with only step#1 finished.
long checkpointId = 0;
long timestamp = 0;
- OperatorSubtaskState snapshot;
+ OperatorSubtaskState snapshot = null;
Review Comment:
nit: is this needed?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2375754209
yes, we should have a config to determine which sink implementation used for Table API/SQL. Default should be using the old `FlinkSink`. When the new v2 sink implementation becomes stable and battle tested, we can switch the default to the new v2 sink.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1690550344
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSink.java:
##########
@@ -0,0 +1,395 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.types.Row;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.sink.IcebergSink.Builder;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
Review Comment:
Flink module has been fully migrated to junit5. please rebase with latest main branch. we would need to convert all test here to junit5.
https://github.com/apache/iceberg/pull/10770
I just submitted another PR to refactor the sink test to reduce the number of combinations we are running and the run time.
https://github.com/apache/iceberg/pull/10777
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1698215286
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
Review Comment:
I think we will create our own operators. Not just Writer/Committer/GlobalCommitter, but the operator in `PreCommitToplogy` and the operators in the `PostCommitTopology` as well. I think we would want to stick to the `uidPrefix` constrcut
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
Review Comment:
I think we will create our own operators. Not just Writer/Committer/GlobalCommitter, but the operator in `PreCommitToplogy` and the operators in the `PostCommitTopology` as well. I think we would want to stick to the `uidPrefix` construct
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1698233447
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
The parallelism of the SinkV2 writer is inherited from the SinkV2 parallelism.
We should check it, but I'm fairly sure reading from the code.
@afedulov could help confirm this, if he is available.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626390005
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link IcebergCommittable} (jobId/operatorId/checkpointId). Only used by
+ * {@link IcebergCommittable} (SinkV2).
+ *
+ * @param requests The {@link IcebergCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
Review Comment:
the new `getMaxCommittedCheckpointId` has a totally different signature from the existing `IcebergFilesCommitter.getMaxCommittedCheckpointId`, so I don't think it is worth refactoring it which will require approches like
1. Create a new `IcebergComitterCommon` class
2. create generic static `getMaxCommittedCheckpointId`
3. Refactor current `IcebergFilesCommitter.getMaxCommittedCheckpointId` to use the new static method
4. Refactor the new `IcebergCommitter.getMaxCommittedCheckpointId`
OR
1. Make both `IcebergFilesCommitter.getMaxCommittedCheckpointId` and even `IcebergFilesCommitter` public
2. refactor `IcebergFilesCommitter.getMaxCommittedCheckpointId`
3. Have the new `IcebergComitter` to use `IcebergFilesCommitter.getMaxCommittedCheckpointId`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626642809
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergWriteAggregator.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * IcebergCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class IcebergWriteAggregator
+ extends AbstractStreamOperator<CommittableMessage<IcebergCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<IcebergCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergWriteAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public IcebergWriteAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+
+ this.table = tableLoader.loadTable();
+ this.icebergManifestOutputFileFactory =
+ FlinkManifestUtil.createOutputFileFactory(() -> table, table.properties(), prefix);
+ }
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ IcebergCommittable committable =
+ new IcebergCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<IcebergCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<IcebergCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
+ results.clear();
+ }
+
+ /**
+ * Write all the completed data files to a newly created manifest file and return the manifest's
+ * avro serialized bytes.
+ */
+ public byte[] writeToManifest(Collection<WriteResult> writeResults, long checkpointId)
+ throws IOException {
+ if (writeResults.isEmpty()) {
+ return EMPTY_MANIFEST_DATA;
+ }
+
+ WriteResult result = WriteResult.builder().addAll(writeResults).build();
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ result, () -> icebergManifestOutputFileFactory.create(checkpointId), table.spec());
+
+ return SimpleVersionedSerialization.writeVersionAndSerialize(
+ DeltaManifestsSerializer.INSTANCE, deltaManifests);
+ }
+
+ /**
+ * Write the {@link WriteResult} to temporary manifest files.
+ *
+ * @param result all those DataFiles/DeleteFiles in this WriteResult should be written with same
+ * partition spec
+ */
+ public static DeltaManifests writeCompletedFiles(
+ WriteResult result, Supplier<OutputFile> outputFileSupplier, PartitionSpec spec)
+ throws IOException {
+
+ ManifestFile dataManifest = null;
+ ManifestFile deleteManifest = null;
+
+ // Write the completed data files into a newly created data manifest file.
+ if (result.dataFiles() != null && result.dataFiles().length > 0) {
+ dataManifest =
+ FlinkManifestUtil.writeDataFiles(
+ outputFileSupplier.get(), spec, Lists.newArrayList(result.dataFiles()));
+ }
+
+ // Write the completed delete files into a newly created delete manifest file.
+ if (result.deleteFiles() != null && result.deleteFiles().length > 0) {
+ OutputFile deleteManifestFile = outputFileSupplier.get();
+
+ ManifestWriter<DeleteFile> deleteManifestWriter =
+ ManifestFiles.writeDeleteManifest(FORMAT_V2, spec, deleteManifestFile, DUMMY_SNAPSHOT_ID);
+ try (ManifestWriter<DeleteFile> writer = deleteManifestWriter) {
+ for (DeleteFile deleteFile : result.deleteFiles()) {
+ writer.add(deleteFile);
+ }
+ }
+
+ deleteManifest = deleteManifestWriter.toManifestFile();
+ }
+
+ return new DeltaManifests(dataManifest, deleteManifest, result.referencedDataFiles());
+ }
+
+ @Override
+ public void processElement(StreamRecord<CommittableMessage<WriteResult>> element)
+ throws Exception {
+ if (element.isRecord() && element.getValue() instanceof CommittableWithLineage) {
Review Comment:
yes. Is there any valid case where `isRecord` is true but the value is not `CommittableWithLineage`?
`CommittableWithLineage` seems more like Flink Sink internal type. wondering if Flink changed it and we can fail silently here?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626416286
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/WriteResultSerializer.java:
##########
@@ -0,0 +1,63 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+import org.apache.flink.util.InstantiationUtil;
+import org.apache.iceberg.io.WriteResult;
+
+public class WriteResultSerializer implements SimpleVersionedSerializer<WriteResult> {
+ private static final int VERSION_0 = 0;
Review Comment:
I'll update this code to follow the same structure as `IcebergCommittableSerializer`
ie:
```
private static final int VERSION = 1;
```
and use the constant `1` inside `deserialize`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1634032064
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2.java:
##########
@@ -140,8 +141,7 @@ public void testCheckAndGetEqualityFieldIds() {
DataStream<Row> dataStream =
env.addSource(new BoundedTestSource<>(ImmutableList.of()), ROW_TYPE_INFO);
- FlinkSink.Builder builder =
- FlinkSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
+ Builder builder = IcebergSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
Review Comment:
thanks for pointing this out. I did create separate tests to assert the new functionality. In this case, though, the naming of the unit test file got in the middle, and I didn't realize that I was switching existing tests.
In the current codebase we have unit tests with these names:
1. TestFlinkIcebergSinkV2
2. TestFlinkIcebergSink
3. TestFlinkIcebergSinkV2Base
4. TestFlinkIcebergSinkBase
I will introduce new ones called
1. TestIcebergSink
2. TestIcebergSinkBase
and the original 4 will keep the same
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1621550312
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
Review Comment:
this will allow for the code to reach `commitPendingResult` to clean up state if there is no commits for the table for a longer time
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1631782354
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/SinkTestUtil.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+
+public class SinkTestUtil {
+
+ private SinkTestUtil() {}
+
+ @SuppressWarnings("unchecked")
+ public static List<StreamElement> fromOutput(Collection<Object> elements) {
+ return elements.stream()
+ .map(
+ element -> {
+ if (element instanceof StreamRecord) {
+ return new StreamRecord<>(
+ ((StreamRecord<CommittableMessage<?>>) element).getValue());
+ }
+ return (StreamElement) element;
+ })
+ .collect(Collectors.toList());
+ }
+
+ public static CommittableSummary<?> toCommittableSummary(StreamElement element) {
Review Comment:
this method does assertion along with type extraction. not sure if it is the best structure.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,362 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.runtime.checkpoint.CheckpointStoreUtil;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + sinkId, workerPoolSize);
Review Comment:
nit: I know the old code named it this way. maybe we can rename it to more accurate `iceberg-committer-pool`
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestFlinkIcebergSinkV2.java:
##########
@@ -140,8 +141,7 @@ public void testCheckAndGetEqualityFieldIds() {
DataStream<Row> dataStream =
env.addSource(new BoundedTestSource<>(ImmutableList.of()), ROW_TYPE_INFO);
- FlinkSink.Builder builder =
- FlinkSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
+ Builder builder = IcebergSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA).table(table);
Review Comment:
we can't just switch existing tests to the new sink. both sink implementations need to be tested.
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/SinkTestUtil.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+
+public class SinkTestUtil {
+
+ private SinkTestUtil() {}
+
+ @SuppressWarnings("unchecked")
+ public static List<StreamElement> fromOutput(Collection<Object> elements) {
Review Comment:
it is hard to understand the purpose of this method by just looking at the signature.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,362 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.runtime.checkpoint.CheckpointStoreUtil;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
Review Comment:
wondering if the following flow is a bit clearer
* get max committed checkpoint id
* mark the committed as finished
* use `tailMap` for the uncommitted view
* extract `NavigableMap<Long, byte[]> manifestMap` from the uncommitted view/map
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,362 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.runtime.checkpoint.CheckpointStoreUtil;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
Review Comment:
nit: move this to be beginning of the method
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/WriteResultSerializer.java:
##########
@@ -0,0 +1,63 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+import org.apache.flink.util.InstantiationUtil;
+import org.apache.iceberg.io.WriteResult;
+
+public class WriteResultSerializer implements SimpleVersionedSerializer<WriteResult> {
+ private static final int VERSION_0 = 0;
+
+ @Override
+ public int getVersion() {
+ return VERSION_0;
+ }
+
+ @Override
+ public byte[] serialize(WriteResult writeResult) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+ byte[] result = InstantiationUtil.serializeObject(writeResult);
Review Comment:
why do we need to include the length? looking at the `deserialize` method, `InstantiationUtil.deserializeObject` can directly parse the `byte[] serialized`
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergCommitter.java:
##########
@@ -0,0 +1,832 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
Review Comment:
in addition to new tests, does it cover all the existing tests in `IcebergFilesCommitter`? are there some existing test not quite applicable in v2 sink?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,362 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.runtime.checkpoint.CheckpointStoreUtil;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String sinkId,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + sinkId, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link IcebergCommittable} (jobId/operatorId/checkpointId). Only used by
+ * {@link IcebergCommittable} (SinkV2).
+ *
+ * @param requests The {@link IcebergCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> requests) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = CheckpointStoreUtil.INVALID_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ String snapshotCheckpointId = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (snapshotCheckpointId != null) {
+ long checkpointId = Long.parseLong(snapshotCheckpointId);
+ CommitRequest<IcebergCommittable> request = requests.get(checkpointId);
+ if (request != null
+ && request.getCommittable().jobId().equals(snapshotFlinkJobId)
+ && request.getCommittable().operatorId().equals(snapshotOperatorId)) {
+ lastCommittedCheckpointId = checkpointId;
+ break;
+ }
+ }
+
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param deltaManifestsMap The checkpointId to {@link DeltaManifests} map of the changes to
+ * commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param checkpointId The checkpointId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ public void commitUpToCheckpoint(
+ NavigableMap<Long, byte[]> deltaManifestsMap,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId)
+ throws IOException {
+ NavigableMap<Long, byte[]> pendingMap = deltaManifestsMap.headMap(checkpointId, true);
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, byte[]> e : pendingMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue())) {
+ // Skip the empty flink manifest.
+ continue;
+ }
+
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue());
+ pendingResults.put(
+ e.getKey(),
+ FlinkManifestUtil.readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+ pendingMap.clear();
+ FlinkManifestUtil.deleteCommittedManifests(table, manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
Review Comment:
I am wondering if it is necessary to pass in the last checkpointId through multiple upstream methods. For the delete file case, committables are committed one by one and the checkpoint id is extracted from the map entry `e.getKey()`. For the other case, why don't we check the max checkpointId from the last entry of `pendingResults` in this method.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,434 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<IcebergCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<IcebergCommittable>() {
+ @Override
+ public IcebergCommittable getCommittable() {
+ return new IcebergCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(IcebergCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public IcebergCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<IcebergCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<IcebergCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<IcebergCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ IcebergCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(
+ commitRequestMap, uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link IcebergCommittable} (jobId/operatorId/checkpointId). Only used by
+ * {@link IcebergCommittable} (SinkV2).
+ *
+ * @param requests The {@link IcebergCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
Review Comment:
you are right. the signature is different with CommitRequest now
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1723543103
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
Review Comment:
`suffixIfNotNull` is used so that if the uid is null, we return empty string instead of `"null"`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1617943778
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * SinkCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
Review Comment:
maybe `IcebergWriteAggregator` or sth else more specific than `Sink`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/BaseDeltaTaskWriter.java:
##########
@@ -109,7 +109,8 @@ public void write(RowData row) throws IOException {
}
protected class RowDataDeltaWriter extends BaseEqualityDeltaWriter {
- RowDataDeltaWriter(PartitionKey partition) {
+
Review Comment:
nit: remove unnecessary whitespace change
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
Review Comment:
nit: `this.` is not needed
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergSinkWriter.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.sink.TaskWriterFactory;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * IcebergSink} (SinkV2). Writes out the data to the final place, and emits a single {@link
+ * WriteResult} at every checkpoint for every data/delete file created by this writer.
+ */
+public class IcebergSinkWriter implements CommittingSinkWriter<RowData, WriteResult> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSinkWriter.class);
+
+ private final String fullTableName;
+ private final TaskWriterFactory<RowData> taskWriterFactory;
+ private final IcebergStreamWriterMetrics metrics;
+ private TaskWriter<RowData> writer;
+ private final int subTaskId;
+ private final int attemptId;
+
+ public IcebergSinkWriter(
+ SerializableSupplier<Table> tableSupplier,
+ TaskWriterFactory<RowData> taskWriterFactory,
+ IcebergStreamWriterMetrics metrics,
+ int subTaskId,
+ int attemptId) {
+ this.fullTableName = tableSupplier.get().name();
+ this.taskWriterFactory = taskWriterFactory;
+ // Initialize the task writer factory.
+ taskWriterFactory.initialize(subTaskId, attemptId);
+ // Initialize the task writer.
+ this.writer = taskWriterFactory.create();
+ this.metrics = metrics;
+ this.subTaskId = subTaskId;
+ this.attemptId = attemptId;
+
+ LOG.debug(
+ "StreamWriter created for table {} subtask {} parallelSubtasks {}",
+ fullTableName,
+ subTaskId,
+ attemptId);
+ }
+
+ @Override
+ public void write(RowData element, Context context) throws IOException, InterruptedException {
+ writer.write(element);
+ }
+
+ @Override
+ public void flush(boolean endOfInput) {
+ // flush is used to handle flush/endOfInput, so no action is taken here.
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("table_name", fullTableName)
+ .add("subtask_id", subTaskId)
+ .add("attempt_id", attemptId)
+ .toString();
+ }
+
+ @Override
+ public Collection<WriteResult> prepareCommit() throws IOException {
+ long startNano = System.nanoTime();
+ WriteResult result = writer.complete();
+ this.writer = taskWriterFactory.create();
+ metrics.updateFlushResult(result);
+ metrics.flushDuration(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano));
+ LOG.debug(
+ "WriteResult {} created for table {} subtask {} attempt {}",
Review Comment:
`WriteResult` doesn't implement `toString` method. it is probably not meaningful to log the value. if we want, we can probably log a few summary attributes like
```
Iceberg writer subtask {} attempt {} flushed {} data files and {} delete files
```
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
Review Comment:
how do we notify commit success/failure to the `CommitRequest`?
```
/**
* The commit failed for known reason and should not be retried.
*
* <p>Currently calling this method only logs the error, discards the comittable and
* continues. In the future the behaviour might be configurable.
*/
void signalFailedWithKnownReason(Throwable t);
/**
* The commit failed for unknown reason and should not be retried.
*
* <p>Currently calling this method fails the job. In the future the behaviour might be
* configurable.
*/
void signalFailedWithUnknownReason(Throwable t);
/**
* The commit failed for a retriable reason. If the sink supports a retry maximum, this may
* permanently fail after reaching that maximum. Else the committable will be retried as
* long as this method is invoked after each attempt.
*/
void retryLater();
```
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link SinkCommittable} (jobId/operatorId/checkpointId). Only used by {@link
+ * SinkCommitter} (SinkV2).
+ *
+ * @param requests The {@link SinkCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
+ NavigableMap<Long, CommitRequest<SinkCommittable>> requests) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ String snapshotCheckpointId = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (snapshotCheckpointId != null) {
+ long checkpointId = Long.parseLong(snapshotCheckpointId);
+ CommitRequest<SinkCommittable> request = requests.get(checkpointId);
+ if (request != null
+ && request.getCommittable().jobId().equals(snapshotFlinkJobId)
+ && request.getCommittable().operatorId().equals(snapshotOperatorId)) {
+ lastCommittedCheckpointId = checkpointId;
+ break;
+ }
+ }
+
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ @VisibleForTesting
+ static ListStateDescriptor<SortedMap<Long, byte[]>> buildStateDescriptor() {
+ Comparator<Long> longComparator = Comparators.forType(Types.LongType.get());
+ // Construct a SortedMapTypeInfo.
+ SortedMapTypeInfo<Long, byte[]> sortedMapTypeInfo =
+ new SortedMapTypeInfo<>(
+ BasicTypeInfo.LONG_TYPE_INFO,
+ PrimitiveArrayTypeInfo.BYTE_PRIMITIVE_ARRAY_TYPE_INFO,
+ longComparator);
+ return new ListStateDescriptor<>("iceberg-files-committer-state", sortedMapTypeInfo);
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param deltaManifestsMap The checkpointId to {@link DeltaManifests} map of the changes to
+ * commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param checkpointId The checkpointId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ public void commitUpToCheckpoint(
+ NavigableMap<Long, byte[]> deltaManifestsMap,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId)
+ throws IOException {
+ NavigableMap<Long, byte[]> pendingMap = deltaManifestsMap.headMap(checkpointId, true);
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, byte[]> e : pendingMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue())) {
+ // Skip the empty flink manifest.
+ continue;
+ }
+
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue());
+ pendingResults.put(e.getKey(), readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+ pendingMap.clear();
+ deleteCommittedManifests(manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ commitOperation(appendFiles, summary, "append", newFlinkJobId, operatorId, checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
+ }
+ }
+ }
+
+ private void commitOperation(
+ SnapshotUpdate<?> operation,
+ CommitSummary summary,
+ String description,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ LOG.info(
+ "Committing {} for checkpoint {} to table {} branch {} with summary: {}",
+ description,
+ checkpointId,
+ table.name(),
+ branch,
+ summary);
+ snapshotProperties.forEach(operation::set);
+ // custom snapshot metadata properties will be overridden if they conflict with internal ones
+ // used by the sink.
+ operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
+ operation.set(FLINK_JOB_ID, newFlinkJobId);
+ operation.set(OPERATOR_ID, operatorId);
+ operation.toBranch(branch);
+
+ long startNano = System.nanoTime();
+ operation.commit(); // abort is automatically called if this fails.
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano);
+ LOG.info(
+ "Committed {} to table: {}, branch: {}, checkpointId {} in {} ms",
+ description,
+ table.name(),
+ branch,
+ checkpointId,
+ durationMs);
+ if (committerMetrics != null) {
+ committerMetrics.commitDuration(durationMs);
+ }
+ }
+
+ private void deleteCommittedManifests(
+ List<ManifestFile> manifests, String newFlinkJobId, long checkpointId) {
+ for (ManifestFile manifest : manifests) {
+ try {
+ table.io().deleteFile(manifest.path());
+ } catch (Exception e) {
+ // The flink manifests cleaning failure shouldn't abort the completed checkpoint.
+ String details =
+ MoreObjects.toStringHelper(this)
+ .add("flinkJobId", newFlinkJobId)
+ .add("checkpointId", checkpointId)
+ .add("manifestPath", manifest.path())
+ .toString();
+ LOG.warn(
+ "The iceberg transaction has been committed, but we failed to clean the temporary flink manifests: {}",
+ details,
+ e);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ // Nothing to do
+ }
+
+ public static WriteResult readCompletedFiles(
Review Comment:
probably can keep those static util methods inside `FlinkManifestUtil`?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link SinkCommittable} (jobId/operatorId/checkpointId). Only used by {@link
+ * SinkCommitter} (SinkV2).
+ *
+ * @param requests The {@link SinkCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
+ NavigableMap<Long, CommitRequest<SinkCommittable>> requests) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ String snapshotCheckpointId = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (snapshotCheckpointId != null) {
+ long checkpointId = Long.parseLong(snapshotCheckpointId);
+ CommitRequest<SinkCommittable> request = requests.get(checkpointId);
+ if (request != null
+ && request.getCommittable().jobId().equals(snapshotFlinkJobId)
+ && request.getCommittable().operatorId().equals(snapshotOperatorId)) {
+ lastCommittedCheckpointId = checkpointId;
+ break;
+ }
+ }
+
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ @VisibleForTesting
+ static ListStateDescriptor<SortedMap<Long, byte[]>> buildStateDescriptor() {
Review Comment:
this seems not used here
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergSinkWriter.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.sink.TaskWriterFactory;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * IcebergSink} (SinkV2). Writes out the data to the final place, and emits a single {@link
+ * WriteResult} at every checkpoint for every data/delete file created by this writer.
+ */
+public class IcebergSinkWriter implements CommittingSinkWriter<RowData, WriteResult> {
Review Comment:
nit: `@Internal`
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
Review Comment:
nit: add some comment that this force commit to happen only on committer subtask 0.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link WriteResult} objects) to a single {@link
+ * SinkCommittable} per checkpoint (storing the serialized {@link DeltaManifests}, jobId,
+ * operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<SinkCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(SinkAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+
+ private transient boolean initialized = false;
+
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public SinkAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+
+ this.table = tableLoader.loadTable();
+ this.icebergManifestOutputFileFactory =
+ createOutputFileFactory(() -> table, table.properties(), prefix);
+ }
+ }
+
+ @Override
+ public void finish() throws IOException {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws IOException {
+ SinkCommittable committable =
+ new SinkCommittable(
+ writeToManifest(results, checkpointId),
+ getContainingTask().getEnvironment().getJobID().toString(),
+ getRuntimeContext().getOperatorUniqueID(),
+ checkpointId);
+ CommittableMessage<SinkCommittable> message =
+ new CommittableWithLineage<>(committable, checkpointId, 0);
+ CommittableMessage<SinkCommittable> summary =
+ new CommittableSummary<>(0, 1, checkpointId, 1, 1, 0);
+ output.collect(new StreamRecord<>(summary));
+ output.collect(new StreamRecord<>(message));
+ LOG.info("Aggregated commit message emitted {}", message);
+ results.clear();
+ }
+
+ /**
+ * Write all the completed data files to a newly created manifest file and return the manifest's
+ * avro serialized bytes.
+ */
+ public byte[] writeToManifest(Collection<WriteResult> writeResults, long checkpointId)
+ throws IOException {
+ if (writeResults.isEmpty()) {
+ return EMPTY_MANIFEST_DATA;
+ }
+
+ WriteResult result = WriteResult.builder().addAll(writeResults).build();
+ DeltaManifests deltaManifests =
+ writeCompletedFiles(
+ result, () -> icebergManifestOutputFileFactory.create(checkpointId), table.spec());
+
+ return SimpleVersionedSerialization.writeVersionAndSerialize(
+ DeltaManifestsSerializer.INSTANCE, deltaManifests);
+ }
+
+ /**
+ * Write the {@link WriteResult} to temporary manifest files.
+ *
+ * @param result all those DataFiles/DeleteFiles in this WriteResult should be written with same
+ * partition spec
+ */
+ public static DeltaManifests writeCompletedFiles(
+ WriteResult result, Supplier<OutputFile> outputFileSupplier, PartitionSpec spec)
+ throws IOException {
+
+ ManifestFile dataManifest = null;
+ ManifestFile deleteManifest = null;
+
+ // Write the completed data files into a newly created data manifest file.
+ if (result.dataFiles() != null && result.dataFiles().length > 0) {
+ dataManifest =
+ writeDataFiles(outputFileSupplier.get(), spec, Lists.newArrayList(result.dataFiles()));
+ }
+
+ // Write the completed delete files into a newly created delete manifest file.
+ if (result.deleteFiles() != null && result.deleteFiles().length > 0) {
+ OutputFile deleteManifestFile = outputFileSupplier.get();
+
+ ManifestWriter<DeleteFile> deleteManifestWriter =
+ ManifestFiles.writeDeleteManifest(FORMAT_V2, spec, deleteManifestFile, DUMMY_SNAPSHOT_ID);
+ try (ManifestWriter<DeleteFile> writer = deleteManifestWriter) {
+ for (DeleteFile deleteFile : result.deleteFiles()) {
+ writer.add(deleteFile);
+ }
+ }
+
+ deleteManifest = deleteManifestWriter.toManifestFile();
+ }
+
+ return new DeltaManifests(dataManifest, deleteManifest, result.referencedDataFiles());
+ }
+
+ static ManifestFile writeDataFiles(
Review Comment:
this doesn't need to be copied here. we can probably call the static util method directly `FlinkManifestUtil#writeDataFiles`. same thing for other methods copied from `FlinkManifestUtil `
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
Review Comment:
nit: maybe `IcebergCommitter` is a more specific/informative name.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
Review Comment:
pass the `InitContext` directly to the `IcebergSinkWriter` constructor directly. move the `IcebergStreamWriterMetrics` inside the constructor too. feels a bit cleaner this way
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
Review Comment:
why would `sinkCommitter` not be null? is it related to the `setCommitter` for testing purpose? if yes, should we use Mockito to replace committer during unit test?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommittableSerializer.java:
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+
+/**
+ * This serializer is used for serializing the {@link SinkCommittable} objects between the Writer
+ * and the Aggregator operator and between the Aggregator and the Committer as well.
+ *
+ * <p>In both cases only the respective part is serialized.
+ */
+public class SinkCommittableSerializer implements SimpleVersionedSerializer<SinkCommittable> {
+ private static final int VERSION_1 = 1;
Review Comment:
nit: why not just `VERSION`? it could mean the current version. or just get rid of it completely based on the way it is used currently.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommittable.java:
##########
@@ -0,0 +1,69 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.Serializable;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+
+/**
+ * The aggregated results of a single checkpoint which should be committed. Containing the
+ * serialized {@link org.apache.iceberg.flink.sink.DeltaManifests} file - which contains the commit
+ * data, and the jobId, operatorId, checkpointId triplet which helps identifying the specific commit
+ *
+ * <p>{@link SinkCommittableSerializer} is used for serializing the objects between the Writer and
+ * the Aggregator operator and between the Aggregator and the Committer as well.
+ */
+public class SinkCommittable implements Serializable {
Review Comment:
`IcebergCommittable` is probably more accurate/specific
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,780 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link SinkCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<WriteResult, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
+ this.tableLoader = builder.tableLoader();
+ this.snapshotProperties = builder.snapshotSummary;
+
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return sinkCommitter != null
+ ? sinkCommitter
+ : new SinkCommitter(
+ tableLoader,
+ branch,
+ snapshotProperties,
+ overwriteMode,
+ workerPoolSize,
+ sinkId,
+ metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link SinkCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder setSnapshotSummary(Map<String, String> properties) {
Review Comment:
oh. I see why you named it `snapshotProperties` earlier. it is to be consistent with the current `FlinkSink`. I will take back my earlier comment. I guess it is is probably good to keep the name consistent
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
Review Comment:
this involves a check/read of the table snapshot history for every commit, which add to latency. the behavior/overhead is different. Current code only retrieve the max committed checkpoint id during job initialization phase.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/WriteResultSerializer.java:
##########
@@ -0,0 +1,63 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+import org.apache.flink.util.InstantiationUtil;
+import org.apache.iceberg.io.WriteResult;
+
+public class WriteResultSerializer implements SimpleVersionedSerializer<WriteResult> {
+ private static final int VERSION_0 = 0;
+
+ @Override
+ public int getVersion() {
+ return VERSION_0;
+ }
+
+ @Override
+ public byte[] serialize(WriteResult writeResult) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+ byte[] result = InstantiationUtil.serializeObject(writeResult);
Review Comment:
why not return this byte[] directly? why do we need to wrap?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
Review Comment:
what's the reason of adding this?
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergStreamWriter.java:
##########
@@ -0,0 +1,154 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.BoundedOneInput;
+import org.apache.flink.streaming.api.operators.ChainingStrategy;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.util.SerializableSupplier;
+
+@Internal
+public class IcebergStreamWriter<T> extends AbstractStreamOperator<WriteResult>
Review Comment:
why do we copy the code here?
or do we just want to move the code here? I didn't see the old file removed. so doesn't look like a move to me.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link SinkCommittable} (jobId/operatorId/checkpointId). Only used by {@link
+ * SinkCommitter} (SinkV2).
+ *
+ * @param requests The {@link SinkCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
+ NavigableMap<Long, CommitRequest<SinkCommittable>> requests) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ String snapshotCheckpointId = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (snapshotCheckpointId != null) {
+ long checkpointId = Long.parseLong(snapshotCheckpointId);
+ CommitRequest<SinkCommittable> request = requests.get(checkpointId);
+ if (request != null
+ && request.getCommittable().jobId().equals(snapshotFlinkJobId)
+ && request.getCommittable().operatorId().equals(snapshotOperatorId)) {
+ lastCommittedCheckpointId = checkpointId;
+ break;
+ }
+ }
+
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ @VisibleForTesting
+ static ListStateDescriptor<SortedMap<Long, byte[]>> buildStateDescriptor() {
+ Comparator<Long> longComparator = Comparators.forType(Types.LongType.get());
+ // Construct a SortedMapTypeInfo.
+ SortedMapTypeInfo<Long, byte[]> sortedMapTypeInfo =
+ new SortedMapTypeInfo<>(
+ BasicTypeInfo.LONG_TYPE_INFO,
+ PrimitiveArrayTypeInfo.BYTE_PRIMITIVE_ARRAY_TYPE_INFO,
+ longComparator);
+ return new ListStateDescriptor<>("iceberg-files-committer-state", sortedMapTypeInfo);
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param deltaManifestsMap The checkpointId to {@link DeltaManifests} map of the changes to
+ * commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param checkpointId The checkpointId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ public void commitUpToCheckpoint(
+ NavigableMap<Long, byte[]> deltaManifestsMap,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId)
+ throws IOException {
+ NavigableMap<Long, byte[]> pendingMap = deltaManifestsMap.headMap(checkpointId, true);
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, byte[]> e : pendingMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue())) {
+ // Skip the empty flink manifest.
+ continue;
+ }
+
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue());
+ pendingResults.put(e.getKey(), readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+ pendingMap.clear();
+ deleteCommittedManifests(manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ commitOperation(appendFiles, summary, "append", newFlinkJobId, operatorId, checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
+ }
+ }
+ }
+
+ private void commitOperation(
+ SnapshotUpdate<?> operation,
+ CommitSummary summary,
+ String description,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ LOG.info(
+ "Committing {} for checkpoint {} to table {} branch {} with summary: {}",
+ description,
+ checkpointId,
+ table.name(),
+ branch,
+ summary);
+ snapshotProperties.forEach(operation::set);
+ // custom snapshot metadata properties will be overridden if they conflict with internal ones
+ // used by the sink.
+ operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
+ operation.set(FLINK_JOB_ID, newFlinkJobId);
+ operation.set(OPERATOR_ID, operatorId);
+ operation.toBranch(branch);
+
+ long startNano = System.nanoTime();
+ operation.commit(); // abort is automatically called if this fails.
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano);
+ LOG.info(
+ "Committed {} to table: {}, branch: {}, checkpointId {} in {} ms",
+ description,
+ table.name(),
+ branch,
+ checkpointId,
+ durationMs);
+ if (committerMetrics != null) {
+ committerMetrics.commitDuration(durationMs);
+ }
+ }
+
+ private void deleteCommittedManifests(
Review Comment:
this class seems to share a lot of code with current `IcebergFilesCommitter`. Can we make them static util method to share the code? I would imagine both old and new sink will be maintained for quite a long time (like 1 year or longer).
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
+ }
+ }
+
+ /**
+ * Gets the last checkpointId which is committed to this branch of the table. The commits are
+ * identified by the {@link SinkCommittable} (jobId/operatorId/checkpointId). Only used by {@link
+ * SinkCommitter} (SinkV2).
+ *
+ * @param requests The {@link SinkCommittable}s ordered by checkpointId
+ * @return The checkpointId for the first commit (historically backward)
+ */
+ public long getMaxCommittedCheckpointId(
+ NavigableMap<Long, CommitRequest<SinkCommittable>> requests) {
+ Snapshot snapshot = table.snapshot(branch);
+ long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
+
+ while (snapshot != null) {
+ Map<String, String> summary = snapshot.summary();
+ String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
+ String snapshotOperatorId = summary.get(OPERATOR_ID);
+ String snapshotCheckpointId = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
+ if (snapshotCheckpointId != null) {
+ long checkpointId = Long.parseLong(snapshotCheckpointId);
+ CommitRequest<SinkCommittable> request = requests.get(checkpointId);
+ if (request != null
+ && request.getCommittable().jobId().equals(snapshotFlinkJobId)
+ && request.getCommittable().operatorId().equals(snapshotOperatorId)) {
+ lastCommittedCheckpointId = checkpointId;
+ break;
+ }
+ }
+
+ Long parentSnapshotId = snapshot.parentId();
+ snapshot = parentSnapshotId != null ? table.snapshot(parentSnapshotId) : null;
+ }
+
+ return lastCommittedCheckpointId;
+ }
+
+ @VisibleForTesting
+ static ListStateDescriptor<SortedMap<Long, byte[]>> buildStateDescriptor() {
+ Comparator<Long> longComparator = Comparators.forType(Types.LongType.get());
+ // Construct a SortedMapTypeInfo.
+ SortedMapTypeInfo<Long, byte[]> sortedMapTypeInfo =
+ new SortedMapTypeInfo<>(
+ BasicTypeInfo.LONG_TYPE_INFO,
+ PrimitiveArrayTypeInfo.BYTE_PRIMITIVE_ARRAY_TYPE_INFO,
+ longComparator);
+ return new ListStateDescriptor<>("iceberg-files-committer-state", sortedMapTypeInfo);
+ }
+
+ /**
+ * Commits the data to the Iceberg table by reading the file data from the {@link DeltaManifests}
+ * ordered by the checkpointId, and writing the new snapshot to the Iceberg table. The {@link
+ * org.apache.iceberg.SnapshotSummary} will contain the jobId, snapshotId, checkpointId so in case
+ * of job restart we can identify which changes are committed, and which are still waiting for the
+ * commit.
+ *
+ * @param deltaManifestsMap The checkpointId to {@link DeltaManifests} map of the changes to
+ * commit
+ * @param newFlinkJobId The jobId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param operatorId The operatorId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @param checkpointId The checkpointId to store in the {@link org.apache.iceberg.SnapshotSummary}
+ * @throws IOException On commit failure
+ */
+ public void commitUpToCheckpoint(
+ NavigableMap<Long, byte[]> deltaManifestsMap,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId)
+ throws IOException {
+ NavigableMap<Long, byte[]> pendingMap = deltaManifestsMap.headMap(checkpointId, true);
+ List<ManifestFile> manifests = Lists.newArrayList();
+ NavigableMap<Long, WriteResult> pendingResults = Maps.newTreeMap();
+ for (Map.Entry<Long, byte[]> e : pendingMap.entrySet()) {
+ if (Arrays.equals(EMPTY_MANIFEST_DATA, e.getValue())) {
+ // Skip the empty flink manifest.
+ continue;
+ }
+
+ DeltaManifests deltaManifests =
+ SimpleVersionedSerialization.readVersionAndDeSerialize(
+ DeltaManifestsSerializer.INSTANCE, e.getValue());
+ pendingResults.put(e.getKey(), readCompletedFiles(deltaManifests, table.io(), table.specs()));
+ manifests.addAll(deltaManifests.manifests());
+ }
+
+ CommitSummary summary = new CommitSummary(pendingResults);
+ commitPendingResult(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ if (committerMetrics != null) {
+ committerMetrics.updateCommitSummary(summary);
+ }
+ pendingMap.clear();
+ deleteCommittedManifests(manifests, newFlinkJobId, checkpointId);
+ }
+
+ private void commitPendingResult(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ long totalFiles = summary.dataFilesCount() + summary.deleteFilesCount();
+ continuousEmptyCheckpoints = totalFiles == 0 ? continuousEmptyCheckpoints + 1 : 0;
+ if (totalFiles != 0 || continuousEmptyCheckpoints % maxContinuousEmptyCommits == 0) {
+ if (replacePartitions) {
+ replacePartitions(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ } else {
+ commitDeltaTxn(pendingResults, summary, newFlinkJobId, operatorId, checkpointId);
+ }
+ continuousEmptyCheckpoints = 0;
+ } else {
+ LOG.info("Skip commit for checkpoint {} due to no data files or delete files.", checkpointId);
+ }
+ }
+
+ private void replacePartitions(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ Preconditions.checkState(
+ summary.deleteFilesCount() == 0, "Cannot overwrite partitions with delete files.");
+ // Commit the overwrite transaction.
+ ReplacePartitions dynamicOverwrite = table.newReplacePartitions().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0, "Should have no referenced data files.");
+ Arrays.stream(result.dataFiles()).forEach(dynamicOverwrite::addFile);
+ }
+
+ commitOperation(
+ dynamicOverwrite,
+ summary,
+ "dynamic partition overwrite",
+ newFlinkJobId,
+ operatorId,
+ checkpointId);
+ }
+
+ private void commitDeltaTxn(
+ NavigableMap<Long, WriteResult> pendingResults,
+ CommitSummary summary,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ if (summary.deleteFilesCount() == 0) {
+ // To be compatible with iceberg format V1.
+ AppendFiles appendFiles = table.newAppend().scanManifestsWith(workerPool);
+ for (WriteResult result : pendingResults.values()) {
+ Preconditions.checkState(
+ result.referencedDataFiles().length == 0,
+ "Should have no referenced data files for append.");
+ Arrays.stream(result.dataFiles()).forEach(appendFiles::appendFile);
+ }
+ commitOperation(appendFiles, summary, "append", newFlinkJobId, operatorId, checkpointId);
+ } else {
+ // To be compatible with iceberg format V2.
+ for (Map.Entry<Long, WriteResult> e : pendingResults.entrySet()) {
+ // We don't commit the merged result into a single transaction because for the sequential
+ // transaction txn1 and txn2, the equality-delete files of txn2 are required to be applied
+ // to data files from txn1. Committing the merged one will lead to the incorrect delete
+ // semantic.
+ WriteResult result = e.getValue();
+
+ // Row delta validations are not needed for streaming changes that write equality deletes.
+ // Equality deletes are applied to data in all previous sequence numbers, so retries may
+ // push deletes further in the future, but do not affect correctness. Position deletes
+ // committed to the table in this path are used only to delete rows from data files that are
+ // being added in this commit. There is no way for data files added along with the delete
+ // files to be concurrently removed, so there is no need to validate the files referenced by
+ // the position delete files that are being committed.
+ RowDelta rowDelta = table.newRowDelta().scanManifestsWith(workerPool);
+
+ Arrays.stream(result.dataFiles()).forEach(rowDelta::addRows);
+ Arrays.stream(result.deleteFiles()).forEach(rowDelta::addDeletes);
+ commitOperation(rowDelta, summary, "rowDelta", newFlinkJobId, operatorId, e.getKey());
+ }
+ }
+ }
+
+ private void commitOperation(
+ SnapshotUpdate<?> operation,
+ CommitSummary summary,
+ String description,
+ String newFlinkJobId,
+ String operatorId,
+ long checkpointId) {
+ LOG.info(
+ "Committing {} for checkpoint {} to table {} branch {} with summary: {}",
+ description,
+ checkpointId,
+ table.name(),
+ branch,
+ summary);
+ snapshotProperties.forEach(operation::set);
+ // custom snapshot metadata properties will be overridden if they conflict with internal ones
+ // used by the sink.
+ operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
+ operation.set(FLINK_JOB_ID, newFlinkJobId);
+ operation.set(OPERATOR_ID, operatorId);
+ operation.toBranch(branch);
+
+ long startNano = System.nanoTime();
+ operation.commit(); // abort is automatically called if this fails.
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNano);
+ LOG.info(
+ "Committed {} to table: {}, branch: {}, checkpointId {} in {} ms",
+ description,
+ table.name(),
+ branch,
+ checkpointId,
+ durationMs);
+ if (committerMetrics != null) {
+ committerMetrics.commitDuration(durationMs);
+ }
+ }
+
+ private void deleteCommittedManifests(
+ List<ManifestFile> manifests, String newFlinkJobId, long checkpointId) {
+ for (ManifestFile manifest : manifests) {
+ try {
+ table.io().deleteFile(manifest.path());
+ } catch (Exception e) {
+ // The flink manifests cleaning failure shouldn't abort the completed checkpoint.
+ String details =
+ MoreObjects.toStringHelper(this)
+ .add("flinkJobId", newFlinkJobId)
+ .add("checkpointId", checkpointId)
+ .add("manifestPath", manifest.path())
+ .toString();
+ LOG.warn(
+ "The iceberg transaction has been committed, but we failed to clean the temporary flink manifests: {}",
+ details,
+ e);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ // Nothing to do
+ }
+
+ public static WriteResult readCompletedFiles(
+ DeltaManifests deltaManifests, FileIO io, Map<Integer, PartitionSpec> specsById)
+ throws IOException {
+ WriteResult.Builder builder = WriteResult.builder();
+
+ // Read the completed data files from persisted data manifest file.
+ if (deltaManifests.dataManifest() != null) {
+ builder.addDataFiles(readDataFiles(deltaManifests.dataManifest(), io, specsById));
+ }
+
+ // Read the completed delete files from persisted delete manifests file.
+ if (deltaManifests.deleteManifest() != null) {
+ try (CloseableIterable<DeleteFile> deleteFiles =
+ ManifestFiles.readDeleteManifest(deltaManifests.deleteManifest(), io, specsById)) {
+ builder.addDeleteFiles(deleteFiles);
+ }
+ }
+
+ return builder.addReferencedDataFiles(deltaManifests.referencedDataFiles()).build();
+ }
+
+ static List<DataFile> readDataFiles(
Review Comment:
probably can keep those static util methods inside `FlinkManifestUtil`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1621389662
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,463 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private static final CommitRequest<SinkCommittable> EMPTY_COMMIT_REQUEST =
+ new CommitRequest<SinkCommittable>() {
+ @Override
+ public SinkCommittable getCommittable() {
+ return new SinkCommittable(EMPTY_MANIFEST_DATA, "jobId", "operatorId", -1L);
+ }
+
+ @Override
+ public int getNumberOfRetries() {
+ return 0;
+ }
+
+ @Override
+ public void signalFailedWithKnownReason(Throwable t) {}
+
+ @Override
+ public void signalFailedWithUnknownReason(Throwable t) {}
+
+ @Override
+ public void retryLater() {}
+
+ @Override
+ public void updateAndRetryLater(SinkCommittable committable) {}
+
+ @Override
+ public void signalAlreadyCommitted() {}
+ };
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ NavigableMap<Long, byte[]> manifestMap = Maps.newTreeMap();
+ NavigableMap<Long, CommitRequest<SinkCommittable>> commitRequestMap = Maps.newTreeMap();
+
+ if (commitRequests.isEmpty()) {
+ commitRequests.add(EMPTY_COMMIT_REQUEST);
+ }
+
+ for (CommitRequest<SinkCommittable> request : commitRequests) {
+ manifestMap.put(request.getCommittable().checkpointId(), request.getCommittable().manifest());
+ commitRequestMap.put(request.getCommittable().checkpointId(), request);
+ }
+
+ long maxCommittedCheckpointId = getMaxCommittedCheckpointId(commitRequestMap);
+
+ // Mark the already committed FilesCommittable(s) as finished
+ commitRequestMap
+ .headMap(maxCommittedCheckpointId, true)
+ .values()
+ .forEach(CommitRequest::signalAlreadyCommitted);
+
+ // Commit the remaining
+ SinkCommittable last = commitRequestMap.lastEntry().getValue().getCommittable();
+
+ NavigableMap<Long, byte[]> uncommitted =
+ Maps.newTreeMap(manifestMap).tailMap(maxCommittedCheckpointId, false);
+ if (!uncommitted.isEmpty()) {
+ commitUpToCheckpoint(uncommitted, last.jobId(), last.operatorId(), last.checkpointId());
Review Comment:
yeah. `CheckpointCommittableManagerImpl` marks those requests as committed if the commit doesn't throw an exception. but then why is `CommitRequest` even exposed to `Committer`. Let's say hypothetically IcebergCommitter wants to `signalFailedWithUnknownReason`. it will be ignored by the `CheckpointCommittableManagerImpl` which always call `CommitRequestImpl::setCommittedIfNoError` if no exception is thrown. A bit weird.
```
@Override
public Collection<CommittableWithLineage<CommT>> commit(
boolean fullyReceived, Committer<CommT> committer)
throws IOException, InterruptedException {
Collection<CommitRequestImpl<CommT>> requests = getPendingRequests(fullyReceived);
requests.forEach(CommitRequestImpl::setSelected);
committer.commit(new ArrayList<>(requests));
requests.forEach(CommitRequestImpl::setCommittedIfNoError);
Collection<CommittableWithLineage<CommT>> committed = drainFinished();
metricGroup.setCurrentPendingCommittablesGauge(() -> getPendingRequests(false).size());
return committed;
}
```
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1688804698
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
+ }
+ return rowDataDataStreamSink;
+ }
+ }
+
+ private static Table checkAndGetTable(TableLoader tableLoader, Table table) {
+ if (table == null) {
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ try (TableLoader loader = tableLoader) {
+ return SerializableTable.copyOf(loader.loadTable());
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to load iceberg table from table loader: " + tableLoader, e);
+ }
+ }
+
+ return SerializableTable.copyOf(table);
+ }
+
+ private static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ /**
+ * Based on the {@link FileFormat} overwrites the table level compression properties for the table
+ * write.
+ *
+ * @param table The table to get the table level settings
+ * @param format The FileFormat to use
+ * @param conf The write configuration
+ * @return The properties to use for writing
+ */
+ private static Map<String, String> writeProperties(
+ Table table, FileFormat format, FlinkWriteConf conf) {
+ Map<String, String> writeProperties = Maps.newHashMap(table.properties());
+
+ switch (format) {
+ case PARQUET:
+ writeProperties.put(PARQUET_COMPRESSION, conf.parquetCompressionCodec());
+ String parquetCompressionLevel = conf.parquetCompressionLevel();
+ if (parquetCompressionLevel != null) {
+ writeProperties.put(PARQUET_COMPRESSION_LEVEL, parquetCompressionLevel);
+ }
+
+ break;
+ case AVRO:
+ writeProperties.put(AVRO_COMPRESSION, conf.avroCompressionCodec());
+ String avroCompressionLevel = conf.avroCompressionLevel();
+ if (avroCompressionLevel != null) {
+ writeProperties.put(AVRO_COMPRESSION_LEVEL, conf.avroCompressionLevel());
+ }
+
+ break;
+ case ORC:
+ writeProperties.put(ORC_COMPRESSION, conf.orcCompressionCodec());
+ writeProperties.put(ORC_COMPRESSION_STRATEGY, conf.orcCompressionStrategy());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown file format %s", format));
+ }
+
+ return writeProperties;
+ }
+
+ DataStream<RowData> distributeDataStream(DataStream<RowData> input) {
+ DistributionMode mode = flinkWriteConf.distributionMode();
+ Schema schema = table.schema();
+ PartitionSpec spec = table.spec();
+ LOG.info("Write distribution mode is '{}'", mode.modeName());
+ switch (mode) {
+ case NONE:
+ if (equalityFieldIds.isEmpty()) {
+ return input;
+ } else {
+ LOG.info("Distribute rows by equality fields, because there are equality fields set");
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ case HASH:
+ if (equalityFieldIds.isEmpty()) {
+ if (table.spec().isUnpartitioned()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and table is unpartitioned");
+ return input;
+ } else {
+ if (BucketPartitionerUtil.hasOneBucketField(spec)) {
+ return input.partitionCustom(
+ new BucketPartitioner(spec),
+ new BucketPartitionKeySelector(spec, schema, flinkRowType));
+ } else {
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+ } else {
+ if (spec.isUnpartitioned()) {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and table is unpartitioned");
+ return input.keyBy(
+ new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ } else {
+ for (PartitionField partitionField : spec.fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In 'hash' distribution mode with equality fields set, partition field '%s' "
+ + "should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ return input.keyBy(new PartitionKeySelector(spec, schema, flinkRowType));
+ }
+ }
+
+ case RANGE:
+ if (equalityFieldIds.isEmpty()) {
+ LOG.warn(
+ "Fallback to use 'none' distribution mode, because there are no equality fields set "
+ + "and {}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input;
+ } else {
+ LOG.info(
+ "Distribute rows by equality fields, because there are equality fields set "
+ + "and{}=range is not supported yet in flink",
+ WRITE_DISTRIBUTION_MODE);
+ return input.keyBy(new EqualityFieldKeySelector(schema, flinkRowType, equalityFieldIds));
+ }
+
+ default:
+ throw new RuntimeException("Unrecognized " + WRITE_DISTRIBUTION_MODE + ": " + mode);
+ }
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from generic input data stream into iceberg
+ * table. We use {@link RowData} inside the sink connector, so users need to provide a mapper
+ * function and a {@link TypeInformation} to convert those generic records to a RowData
+ * DataStream.
+ *
+ * @param input the generic source input data stream.
+ * @param mapper function to convert the generic data to {@link RowData}
+ * @param outputType to define the {@link TypeInformation} for the input data.
+ * @param <T> the data type of records.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static <T> Builder builderFor(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ return new Builder().forMapperOutputType(input, mapper, outputType);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link Row}s into
+ * iceberg table. We use {@link RowData} inside the sink connector, so users need to provide a
+ * {@link TableSchema} for builder to convert those {@link Row}s to a {@link RowData} DataStream.
+ *
+ * @param input the source input data stream with {@link Row}s.
+ * @param tableSchema defines the {@link TypeInformation} for input data.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static Builder forRow(DataStream<Row> input, TableSchema tableSchema) {
+ return new Builder().forRow(input, tableSchema);
+ }
+
+ /**
+ * Initialize a {@link Builder} to export the data from input data stream with {@link RowData}s
+ * into iceberg table.
+ *
+ * @param input the source input data stream with {@link RowData}s.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public static Builder forRowData(DataStream<RowData> input) {
+ return new Builder().forRowData(input);
+ }
+
+ private static String prefixIfNotNull(String uidPrefix, String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ /**
+ * A table loader that will only reload a table after a certain interval has passed. WARNING: This
Review Comment:
do we need this? can't we use the lambda just like the `FlinkSink` code?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "nastra (via GitHub)" <gi...@apache.org>.
nastra commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1689732063
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSinkV2Branch.java:
##########
@@ -0,0 +1,134 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import java.io.IOException;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.flink.HadoopCatalogResource;
+import org.apache.iceberg.flink.MiniClusterResource;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.junit.Assert;
Review Comment:
this should all be using JUnit5 imports + AssertJ assertions
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1707833724
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
Review Comment:
that code from FlinkSink is different
```
SerializableTable serializableTable = (SerializableTable) SerializableTable.copyOf(table);
```
It creates SerializableTable object from a regular table. it is just that `copyOf` returns an regular `Table` interface and hence the casting.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1697699881
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1472 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
Review Comment:
yes, ie:
`testEmitCommittablesMock(), testEmitCommittables(), testStateRestoreFromPreJob(), etc`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1730445602
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,747 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidSuffix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidSuffix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidSuffix = uidSuffix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ String suffix = defaultSuffix(uidSuffix, "sink");
+ String operatorName =
Review Comment:
we don't we just set operatorName as uid. currently, the operator name is not friendly because the `sinkId` is a `uuid`. here is a snippet from the stack trace you pasted in another comment. The order may have changed. But you get what I meant with the `sinkId` uuid.
```
Sink: -hadoop.default.t-e254fac5-1ac5-40f1-ac6f-87cf37af0de9-pre-commit-topology-38
```
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,747 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidSuffix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidSuffix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidSuffix = uidSuffix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ String suffix = defaultSuffix(uidSuffix, "sink");
+ String operatorName =
+ String.format("%s-%s-%s-%s", "Sink pre-commit aggregator", sinkId, table.name(), suffix);
+
+ String preCommitAggregatorUid = String.format("Sink pre-commit aggregator: %s", suffix);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(operatorName, typeInformation, new IcebergWriteAggregator(tableLoader))
+ .uid(preCommitAggregatorUid)
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = "";
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ String name = "Sink pre-write mapper: " + newUidSuffix;
+ String uid = "pre-write-mapper-" + newUidSuffix;
Review Comment:
nit: this isn't consistent with the other pre-commit operator, sth like `Sink pre-commit aggregator: <newUidSuffix>`. Maybe this should be `Sink pre-writer mapper: <suffix>`
again, we can simply keep operator name and uid the same.
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,747 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidSuffix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidSuffix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidSuffix = uidSuffix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ String suffix = defaultSuffix(uidSuffix, "sink");
+ String operatorName =
+ String.format("%s-%s-%s-%s", "Sink pre-commit aggregator", sinkId, table.name(), suffix);
+
+ String preCommitAggregatorUid = String.format("Sink pre-commit aggregator: %s", suffix);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(operatorName, typeInformation, new IcebergWriteAggregator(tableLoader))
+ .uid(preCommitAggregatorUid)
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidSuffix = "";
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private SerializableTable table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidSuffix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidSuffix != null) {
+ String name = "Sink pre-write mapper: " + newUidSuffix;
+ String uid = "pre-write-mapper-" + newUidSuffix;
+ inputStream.name(name).uid(uid);
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link SerializableTable} instance is used for initializing {@link
+ * IcebergStreamWriter} which will write all the records into {@link DataFile}s and emit them to
+ * downstream operator. Providing a table would avoid so many table loading from each separate
+ * task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = (SerializableTable) SerializableTable.copyOf(newTable);
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link
+ * org.apache.iceberg.flink.sink.IcebergCommitter} lazily, we need this loader because {@link
+ * Table} is not serializable and could not just use the loaded table from Builder#table in the
+ * remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid suffix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator). Actual operator uid will be
+ * prepended with a prefix like "Sink Committer: $uidSuffix".
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newSuffix suffix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidSuffix(String newSuffix) {
+ this.uidSuffix = newSuffix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ SerializableTable serializableTable = checkAndGetTable(tableLoader(), table);
+ this.table = serializableTable;
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSupplier = new CachingTableSupplier(table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSupplier = () -> serializableTable;
+ }
+
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds =
+ SinkUtil.checkAndGetEqualityFieldIds(table, equalityFieldColumns);
+
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !overwriteMode,
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ return new IcebergSink(
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidSuffix,
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf),
+ toFlinkRowType(table.schema(), tableSchema),
+ tableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ flinkWriteConf.branch(),
+ overwriteMode);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ String suffix = defaultSuffix(uidSuffix, "icebergSink");
+ DataStream<RowData> rowDataInput = inputCreator.apply(suffix);
+ // Please note that V2 sink framework will apply the uid here to the framework created
+ // operators like writer,
+ // committer. E.g. "Sink writer: <uidSuffix>
+ String uid = "sink-" + suffix;
Review Comment:
I thought we want to set the uid and name for the sink operator to just `suffix`.
The framework would name the writer operator to sth like `Sink writer: <suffix>`. add a `sink-` or `Sink: ` can make the operator name and uid look weird.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718126198
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/PartitionedDeltaWriter.java:
##########
@@ -66,7 +66,7 @@ class PartitionedDeltaWriter extends BaseDeltaTaskWriter {
}
@Override
- RowDataDeltaWriter route(RowData row) {
+ protected RowDataDeltaWriter route(RowData row) {
Review Comment:
Why is this change?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718940359
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,811 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private final Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(
+ Table initTable,
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ boolean upsertMode,
+ FileFormat dataFileFormat,
+ long targetDataFileSize,
+ String branch,
+ boolean overwriteMode,
+ int workerPoolSize) {
+ this.initTable = initTable;
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.upsertMode = upsertMode;
+ this.dataFileFormat = dataFileFormat;
+ this.targetDataFileSize = targetDataFileSize;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.workerPoolSize = workerPoolSize;
+ this.table = table;
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<WriteResult> getWriteResultSerializer() {
+ return new WriteResultSerializer();
+ }
+
+ public static class Builder {
+ private TableLoader tableLoader;
+ private String uidPrefix = null;
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableSchema tableSchema;
+ private Table table;
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+ private final Map<String, String> snapshotSummary = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private List<String> equalityFieldColumns = null;
+
+ private Builder() {}
+
+ private Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism of map operator same as its input to keep map operator
+ // chaining its input, and avoid rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergCommitter} lazily, we need this
+ * loader because {@link Table} is not serializable and could not just use the loaded table from
+ * Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for IcebergSink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the IcebergSink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for IcebergSink operators. Note that IcebergSink internally consists of
+ * multiple operators (like writer, committer, aggregator) Actual operator uid will be appended
+ * with a suffix like "uidPrefix-writer".
+ *
+ * <p>If provided, this prefix is also applied to operator names.
+ *
+ * <p>Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid.
+ *
+ * <p>Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous IcebergSink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore IcebergSink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link Builder} to connect the iceberg table.
+ */
+ public Builder uidPrefix(String newPrefix) {
+ this.uidPrefix = newPrefix;
+ return this;
+ }
+
+ public Builder snapshotProperties(Map<String, String> properties) {
+ snapshotSummary.putAll(properties);
+ return this;
+ }
+
+ public Builder setSnapshotProperty(String property, String value) {
+ snapshotSummary.put(property, value);
+ return this;
+ }
+
+ public Builder toBranch(String branch) {
+ writeOptions.put(FlinkWriteOptions.BRANCH.key(), branch);
+ return this;
+ }
+
+ private String operatorName(String suffix) {
+ return uidPrefix != null ? uidPrefix + "-" + suffix : suffix;
+ }
+
+ List<Integer> checkAndGetEqualityFieldIds() {
+ List<Integer> equalityFieldIds = Lists.newArrayList(table.schema().identifierFieldIds());
+ if (equalityFieldColumns != null && !equalityFieldColumns.isEmpty()) {
+ Set<Integer> equalityFieldSet =
+ Sets.newHashSetWithExpectedSize(equalityFieldColumns.size());
+ for (String column : equalityFieldColumns) {
+ org.apache.iceberg.types.Types.NestedField field = table.schema().findField(column);
+ Preconditions.checkNotNull(
+ field,
+ "Missing required equality field column '%s' in table schema %s",
+ column,
+ table.schema());
+ equalityFieldSet.add(field.fieldId());
+ }
+
+ if (!equalityFieldSet.equals(table.schema().identifierFieldIds())) {
+ LOG.warn(
+ "The configured equality field column IDs {} are not matched with the schema identifier field IDs"
+ + " {}, use job specified equality field columns as the equality fields by default.",
+ equalityFieldSet,
+ table.schema().identifierFieldIds());
+ }
+ equalityFieldIds = Lists.newArrayList(equalityFieldSet);
+ }
+ return equalityFieldIds;
+ }
+
+ public IcebergSink build() {
+
+ Preconditions.checkArgument(
+ inputCreator != null,
+ "Please use forRowData() or forMapperOutputType() to initialize the input DataStream.");
+ Preconditions.checkNotNull(tableLoader(), "Table loader shouldn't be null");
+
+ // Set the table if it is not yet set in the builder, so we can do the equalityId checks
+ table(checkAndGetTable(tableLoader(), table));
+
+ // Validate the equality fields and partition fields if we enable the upsert mode.
+ List<Integer> equalityFieldIds = checkAndGetEqualityFieldIds();
+
+ // Init the `flinkWriteConf` here, so we can do the checks
+ FlinkWriteConf flinkWriteConf = new FlinkWriteConf(table, writeOptions, readableConfig);
+ String branch = flinkWriteConf.branch();
+ boolean overwriteMode = flinkWriteConf.overwriteMode();
+ int workerPoolSize = flinkWriteConf.workerPoolSize();
+ if (flinkWriteConf.upsertMode()) {
+ Preconditions.checkState(
+ !flinkWriteConf.overwriteMode(),
+ "OVERWRITE mode shouldn't be enable when configuring to use UPSERT data stream.");
+ Preconditions.checkState(
+ !equalityFieldIds.isEmpty(),
+ "Equality field columns shouldn't be empty when configuring to use UPSERT data stream.");
+ if (!table.spec().isUnpartitioned()) {
+ for (PartitionField partitionField : table.spec().fields()) {
+ Preconditions.checkState(
+ equalityFieldIds.contains(partitionField.sourceId()),
+ "In UPSERT mode, partition field '%s' should be included in equality fields: '%s'",
+ partitionField,
+ equalityFieldColumns);
+ }
+ }
+ }
+
+ Map<String, String> writeProperties =
+ writeProperties(table, flinkWriteConf.dataFileFormat(), flinkWriteConf);
+ RowType flinkRowType = toFlinkRowType(table.schema(), tableSchema);
+
+ Duration tableRefreshInterval = flinkWriteConf.tableRefreshInterval();
+ SerializableSupplier<Table> tableSerializableSupplier;
+ if (tableRefreshInterval != null) {
+ tableSerializableSupplier =
+ new CachingTableSupplier(
+ (SerializableTable) table, tableLoader(), tableRefreshInterval);
+ } else {
+ tableSerializableSupplier = new SimpleTableSupplier((SerializableTable) table);
+ }
+
+ Table initTable = tableSerializableSupplier.get();
+ uidPrefix = Optional.ofNullable(uidPrefix).orElse("");
+
+ // FlinkWriteConf properties needed to be set separately, so we do not have to serialize the
+ // full conf
+ boolean upsertMode = flinkWriteConf.upsertMode();
+ FileFormat dataFileFormat = flinkWriteConf.dataFileFormat();
+ long targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ return new IcebergSink(
+ initTable,
+ tableLoader,
+ table,
+ snapshotSummary,
+ uidPrefix,
+ writeProperties,
+ flinkRowType,
+ tableSerializableSupplier,
+ flinkWriteConf,
+ equalityFieldIds,
+ upsertMode,
+ dataFileFormat,
+ targetDataFileSize,
+ branch,
+ overwriteMode,
+ workerPoolSize);
+ }
+
+ /**
+ * Append the iceberg sink operators to write records to iceberg table.
+ *
+ * @return {@link DataStreamSink} for sink.
+ */
+ public DataStreamSink<RowData> append() {
+ IcebergSink sink = build();
+ DataStream<RowData> rowDataInput = inputCreator.apply(uidPrefix);
+ DataStreamSink<RowData> rowDataDataStreamSink =
+ rowDataInput.sinkTo(sink).uid(uidPrefix + "-sink");
+ if (sink.flinkWriteConf.writeParallelism() != null) {
+ rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());
Review Comment:
I am not sure how to interpret the code snippet. are we saying `rowDataDataStreamSink.setParallelism(sink.flinkWriteConf.writeParallelism());` would propagate the proper parallelism to the writer operator transformation? If yes, let' add a unit test to verify it.
But I doubt that is how it works. the sink has a series of operators: pre-write, write, pre-commit, commit, post-commit. `rowDataDataStreamSink` is after post-commit stage.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1718244647
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergCommitter.java:
##########
@@ -0,0 +1,348 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link IcebergCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+@Internal
+public class IcebergCommitter implements Committer<IcebergCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final WriteResult EMPTY_WRITE_RESULT =
+ WriteResult.builder()
+ .addDataFiles(Lists.newArrayList())
+ .addDeleteFiles(Lists.newArrayList())
+ .build();
+
+ private static final long INITIAL_CHECKPOINT_ID = -1L;
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
Review Comment:
I don't think `IcebergCommitter` needs to be serializable, since it is created on the TaskManager when needed by the `createCommitter` method.
Also this means that we don't need/don't have transient fields anymore.
Am I right?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "arkadius (via GitHub)" <gi...@apache.org>.
arkadius commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2371580187
Thank you for the quick response. By coexistence do you mean that it will be possible to pick the new implementation for dynamic tables for example by some property in the catalog configuration? Currently, the old one is hardcoded. For a test, I changed the implementation to the new one here: https://github.com/arkadius/iceberg/pull/1/files#diff-af1fa2eee823938a1ceaf6f212ebeb12226c7d853a2f47109c284a59c44f39a3R80 and so far I see that everything works. Are there some known limitations of the new implementation?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "arkadius (via GitHub)" <gi...@apache.org>.
arkadius commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2375109539
Hi @rodmeneses, by everything works I meant that I did some manual tests and the results were the same as with the old one. Probably "everything" was an overkill here ;-) Yes, I can take this. I'll let you know on the slack if I need any help with this task.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "arkadius (via GitHub)" <gi...@apache.org>.
arkadius commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2371390372
Hi, Is there a plan to replace the previous implementation (`FlinkSink`) with the new one (`IcebergSink`) also for dynamic tables (in `FlinkDynamicTableFactory`)? When it will happen?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2373432086
@rodmeneses: I would follow the steps taken by the IcebergTableSource, but instead of `createFLIP27Stream` and `table.exec.iceberg.use-flip27-source` we should find a better name. I remember that we discussed that the choice of the parameters were not the best one.
Maybe even V2 could be better in this case.
WDYT @stevenzwu ?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591425847
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/BaseDeltaTaskWriter.java:
##########
@@ -38,7 +38,7 @@
import org.apache.iceberg.relocated.com.google.common.collect.Sets;
import org.apache.iceberg.types.TypeUtil;
-abstract class BaseDeltaTaskWriter extends BaseTaskWriter<RowData> {
+public abstract class BaseDeltaTaskWriter extends BaseTaskWriter<RowData> {
Review Comment:
In all of these cases where we need to expose the class, we might want to add the `@Internal` annotation to highlight that these are internal classes
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591411561
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestSinkV2Committer.java:
##########
@@ -0,0 +1,808 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.SinkTestUtil.fromOutput;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.toCommittableSummary;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.writer.FlinkAppenderFactory;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RunWith(Parameterized.class)
Review Comment:
Should we do that in a different PR? Either before, or after this?
One of the important thing with this PR is, that the original behaviour is not changing. It is hard to reason about it, if we change the test files in the same PR.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596998147
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergSinkWriter.java:
##########
@@ -0,0 +1,122 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.connector.sink2.CommittingSinkWriter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.table.data.RowData;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.flink.sink.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.sink.TaskWriterFactory;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Iceberg writer implementation for the {@link SinkWriter} interface. Used by the {@link
+ * IcebergSink} (SinkV2). Writes out the data to the final place, and emits a single {@link
+ * SinkCommittable} on {@link CommittingSinkWriter#prepareCommit()} at every checkpoint. The emitted
Review Comment:
emits a `WriteResult`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591970688
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,789 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.WithPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.SinkAggregator;
+import org.apache.iceberg.flink.sink.committer.SinkCommittable;
+import org.apache.iceberg.flink.sink.committer.SinkCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.SinkCommitter;
+import org.apache.iceberg.flink.sink.writer.IcebergSinkWriter;
+import org.apache.iceberg.flink.sink.writer.IcebergStreamWriterMetrics;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link WithPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link SinkCommittable} to store the {@link org.apache.iceberg.io.WriteResult}
+ * <li>{@link WithPreCommitTopology} which we use to to place the {@link SinkAggregator} which
+ * merges the individual {@link org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link Committer} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link WithPostCommitTopology} we could use for incremental compaction later. This is not
+ * implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<SinkCommittable>,
+ SupportsPreCommitTopology<SinkCommittable, SinkCommittable>,
+ SupportsPostCommitTopology<SinkCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<SinkCommittable> sinkV2Committer;
+ private final String sinkId;
+ private transient IcebergSink.Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ // FlinkWriteConf properties stored here for serialization
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) {
+ if (requestedSchema != null) {
+ // Convert the flink schema to iceberg schema firstly, then reassign ids to match the existing
+ // iceberg schema.
+ Schema writeSchema = TypeUtil.reassignIds(FlinkSchemaUtil.convert(requestedSchema), schema);
+ TypeUtil.validateWriteSchema(schema, writeSchema, true, true);
+
+ // We use this flink schema to read values from RowData. The flink's TINYINT and SMALLINT will
+ // be promoted to iceberg INTEGER, that means if we use iceberg's table schema to read TINYINT
+ // (backend by 1 'byte'), we will read 4 bytes rather than 1 byte, it will mess up the byte
+ // array in
+ // BinaryRowData. So here we must use flink schema.
+ return (RowType) requestedSchema.toRowDataType().getLogicalType();
+ } else {
+ return FlinkSchemaUtil.convert(schema);
+ }
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) throws IOException {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), initTable.name());
+ return new IcebergSinkWriter(
+ tableSupplier,
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<SinkCommittable> createCommitter(CommitterInitContext context)
+ throws IOException {
+ return sinkV2Committer != null
+ ? sinkV2Committer
+ : new SinkCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getCommittableSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(DataStream<CommittableMessage<SinkCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<SinkCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<SinkCommittable>> writeResults) {
+ TypeInformation<CommittableMessage<SinkCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ return writeResults
+ .global()
+ .transform(
+ prefixIfNotNull(uidPrefix, initTable.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new SinkAggregator(tableLoader, sinkId))
+ .uid(prefixIfNotNull(uidPrefix, "pre-commit-topology"))
+ .setParallelism(1)
+ .setMaxParallelism(1)
+ .global();
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SinkCommittable> getWriteResultSerializer() {
+ return new SinkCommittableSerializer();
+ }
+
+ public static class Builder {
+ private Function<String, DataStream<RowData>> inputCreator = null;
+ private TableLoader tableLoader;
+ private Table table;
+ private TableSchema tableSchema;
+ private List<String> equalityFieldColumns = null;
+ private String uidPrefix = null;
+ private final Map<String, String> snapshotProperties = Maps.newHashMap();
+ private ReadableConfig readableConfig = new Configuration();
+ private final Map<String, String> writeOptions = Maps.newHashMap();
+
+ private Builder() {}
+
+ private IcebergSink.Builder forRowData(DataStream<RowData> newRowDataInput) {
+ this.inputCreator = ignored -> newRowDataInput;
+ return this;
+ }
+
+ private IcebergSink.Builder forRow(DataStream<Row> input, TableSchema inputTableSchema) {
+ RowType rowType = (RowType) inputTableSchema.toRowDataType().getLogicalType();
+ DataType[] fieldDataTypes = inputTableSchema.getFieldDataTypes();
+
+ DataFormatConverters.RowConverter rowConverter =
+ new DataFormatConverters.RowConverter(fieldDataTypes);
+ return this.forMapperOutputType(
+ input, rowConverter::toInternal, FlinkCompatibilityUtil.toTypeInfo(rowType))
+ .tableSchema(inputTableSchema);
+ }
+
+ private <T> IcebergSink.Builder forMapperOutputType(
+ DataStream<T> input, MapFunction<T, RowData> mapper, TypeInformation<RowData> outputType) {
+ this.inputCreator =
+ newUidPrefix -> {
+ // Input stream order is crucial for some situation(e.g. in cdc case). Therefore, we
+ // need to set the parallelism
+ // of map operator same as its input to keep map operator chaining its input, and avoid
+ // rebalanced by default.
+ SingleOutputStreamOperator<RowData> inputStream =
+ input.map(mapper, outputType).setParallelism(input.getParallelism());
+ if (newUidPrefix != null) {
+ inputStream.name(operatorName(newUidPrefix)).uid(newUidPrefix + "-mapper");
+ }
+ return inputStream;
+ };
+ return this;
+ }
+
+ /**
+ * This iceberg {@link Table} instance is used for initializing {@link IcebergStreamWriter}
+ * which will write all the records into {@link DataFile}s and emit them to downstream operator.
+ * Providing a table would avoid so many table loading from each separate task.
+ *
+ * @param newTable the loaded iceberg table instance.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder table(Table newTable) {
+ this.table = newTable;
+ return this;
+ }
+
+ /**
+ * The table loader is used for loading tables in {@link IcebergFilesCommitter} lazily, we need
+ * this loader because {@link Table} is not serializable and could not just use the loaded table
+ * from Builder#table in the remote task manager.
+ *
+ * @param newTableLoader to load iceberg table inside tasks.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder tableLoader(TableLoader newTableLoader) {
+ this.tableLoader = newTableLoader;
+ return this;
+ }
+
+ // new
+ TableLoader tableLoader() {
+ return tableLoader;
+ }
+ // end new
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder set(String property, String value) {
+ writeOptions.put(property, value);
+ return this;
+ }
+
+ /**
+ * Set the write properties for Flink sink. View the supported properties in {@link
+ * FlinkWriteOptions}
+ */
+ public IcebergSink.Builder setAll(Map<String, String> properties) {
+ writeOptions.putAll(properties);
+ return this;
+ }
+
+ public IcebergSink.Builder tableSchema(TableSchema newTableSchema) {
+ this.tableSchema = newTableSchema;
+ return this;
+ }
+
+ public IcebergSink.Builder overwrite(boolean newOverwrite) {
+ writeOptions.put(FlinkWriteOptions.OVERWRITE_MODE.key(), Boolean.toString(newOverwrite));
+ return this;
+ }
+
+ public IcebergSink.Builder flinkConf(ReadableConfig config) {
+ this.readableConfig = config;
+ return this;
+ }
+
+ /**
+ * Configure the write {@link DistributionMode} that the flink sink will use. Currently, flink
+ * support {@link DistributionMode#NONE} and {@link DistributionMode#HASH}.
+ *
+ * @param mode to specify the write distribution mode.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder distributionMode(DistributionMode mode) {
+ Preconditions.checkArgument(
+ !DistributionMode.RANGE.equals(mode),
+ "Flink does not support 'range' write distribution mode now.");
+ if (mode != null) {
+ writeOptions.put(FlinkWriteOptions.DISTRIBUTION_MODE.key(), mode.modeName());
+ }
+ return this;
+ }
+
+ /**
+ * Configuring the write parallel number for iceberg stream writer.
+ *
+ * @param newWriteParallelism the number of parallel iceberg stream writer.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder writeParallelism(int newWriteParallelism) {
+ writeOptions.put(
+ FlinkWriteOptions.WRITE_PARALLELISM.key(), Integer.toString(newWriteParallelism));
+ return this;
+ }
+
+ /**
+ * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which
+ * means it will DELETE the old records and then INSERT the new records. In partitioned table,
+ * the partition fields should be a subset of equality fields, otherwise the old row that
+ * located in partition-A could not be deleted by the new row that located in partition-B.
+ *
+ * @param enabled indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder upsert(boolean enabled) {
+ writeOptions.put(FlinkWriteOptions.WRITE_UPSERT_ENABLED.key(), Boolean.toString(enabled));
+ return this;
+ }
+
+ /**
+ * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events.
+ *
+ * @param columns defines the iceberg table's key.
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder equalityFieldColumns(List<String> columns) {
+ this.equalityFieldColumns = columns;
+ return this;
+ }
+
+ /**
+ * Set the uid prefix for FlinkSink operators. Note that FlinkSink internally consists of
+ * multiple operators (like writer, committer, dummy sink etc.) Actually operator uid will be
+ * appended with a suffix like "uidPrefix-writer". <br>
+ * <br>
+ * If provided, this prefix is also applied to operator names. <br>
+ * <br>
+ * Flink auto generates operator uid if not set explicitly. It is a recommended <a
+ * href="https://ci.apache.org/projects/flink/flink-docs-master/docs/ops/production_ready/">
+ * best-practice to set uid for all operators</a> before deploying to production. Flink has an
+ * option to {@code pipeline.auto-generate-uid=false} to disable auto-generation and force
+ * explicit setting of all operator uid. <br>
+ * <br>
+ * Be careful with setting this for an existing job, because now we are changing the operator
+ * uid from an auto-generated one to this new value. When deploying the change with a
+ * checkpoint, Flink won't be able to restore the previous Flink sink operator state (more
+ * specifically the committer operator state). You need to use {@code --allowNonRestoredState}
+ * to ignore the previous sink state. During restore Flink sink state is used to check if last
+ * commit was actually successful or not. {@code --allowNonRestoredState} can lead to data loss
+ * if the Iceberg commit failed in the last completed checkpoint.
+ *
+ * @param newPrefix prefix for Flink sink operator uid and name
+ * @return {@link IcebergSink.Builder} to connect the iceberg table.
+ */
+ public IcebergSink.Builder uidPrefix(String newPrefix) {
Review Comment:
Do we handle this correctly?
Is the comment correct?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591995708
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,129 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
+ * only {@link WriteResult} objects) to a single {@link SinkCommittable} per checkpoint (storing the
+ * serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<SinkCommittable>, CommittableMessage<SinkCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(SinkAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
Review Comment:
This could be interesting with unaligned checkpoints
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1591983820
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/IcebergFlinkManifestUtil.java:
##########
@@ -0,0 +1,33 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.iceberg.Table;
+
+public class IcebergFlinkManifestUtil {
+
+ private IcebergFlinkManifestUtil() {}
+
+ public static IcebergManifestOutputFileFactory createOutputFileFactory(
Review Comment:
Do we need a class for this?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587953911
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/IcebergStreamWriter.java:
##########
@@ -0,0 +1,154 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.BoundedOneInput;
+import org.apache.flink.streaming.api.operators.ChainingStrategy;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.io.TaskWriter;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.util.SerializableSupplier;
+
+@Internal
+public class IcebergStreamWriter<T> extends AbstractStreamOperator<WriteResult>
Review Comment:
Apart from this, and a few other classes, I think most other classes could be shared between the to Sinks.
Do I forgot something? 😄
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1587950378
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/writer/BaseDeltaTaskWriter.java:
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.iceberg.flink.sink.writer;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionKey;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.StructLike;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.RowDataWrapper;
+import org.apache.iceberg.flink.data.RowDataProjection;
+import org.apache.iceberg.io.BaseTaskWriter;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.OutputFileFactory;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+
+abstract class BaseDeltaTaskWriter extends BaseTaskWriter<RowData> {
Review Comment:
What's the difference between this, and `org.apache.iceberg.flink.sink.BaseDeltaTaskWriter`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#issuecomment-2311032052
Thanks a lot. This was a great team collaboration effort. Thanks for all the reviews and the time spent on this PR: @stevenzwu @pvary @nastra
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1723543103
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,740 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data/delete files, and
+ * generates the {@link org.apache.iceberg.io.WriteResult} objects for the files
+ * <li>{@link SupportsPreCommitTopology} which we use to place the {@link
+ * org.apache.iceberg.flink.sink.IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}
+ * <li>{@link org.apache.iceberg.flink.sink.IcebergCommitter} which commits the incoming{@link
+ * org.apache.iceberg.flink.sink.IcebergCommittable}s to the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+@Experimental
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private final String uidPrefix;
+ private final String sinkId;
+ private final Map<String, String> writeProperties;
+ private final RowType flinkRowType;
+ private final SerializableSupplier<Table> tableSupplier;
+ private final transient FlinkWriteConf flinkWriteConf;
+ private final List<Integer> equalityFieldIds;
+ private final boolean upsertMode;
+ private final FileFormat dataFileFormat;
+ private final long targetDataFileSize;
+ private final String branch;
+ private final boolean overwriteMode;
+ private final int workerPoolSize;
+
+ private final Table table;
+ private final List<String> equalityFieldColumns = null;
+
+ private IcebergSink(
+ TableLoader tableLoader,
+ Table table,
+ Map<String, String> snapshotProperties,
+ String uidPrefix,
+ Map<String, String> writeProperties,
+ RowType flinkRowType,
+ SerializableSupplier<Table> tableSupplier,
+ FlinkWriteConf flinkWriteConf,
+ List<Integer> equalityFieldIds,
+ String branch,
+ boolean overwriteMode) {
+ this.tableLoader = tableLoader;
+ this.snapshotProperties = snapshotProperties;
+ this.uidPrefix = uidPrefix;
+ this.writeProperties = writeProperties;
+ this.flinkRowType = flinkRowType;
+ this.tableSupplier = tableSupplier;
+ this.flinkWriteConf = flinkWriteConf;
+ this.equalityFieldIds = equalityFieldIds;
+ this.branch = branch;
+ this.overwriteMode = overwriteMode;
+ this.table = table;
+ this.upsertMode = flinkWriteConf.upsertMode();
+ this.dataFileFormat = flinkWriteConf.dataFileFormat();
+ this.targetDataFileSize = flinkWriteConf.targetDataFileSize();
+ this.workerPoolSize = flinkWriteConf.workerPoolSize();
+ // We generate a random UUID every time when a sink is created.
+ // This is used to separate files generated by different sinks writing the same table.
+ // Also used to generate the aggregator operator name
+ this.sinkId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ public SinkWriter<RowData> createWriter(InitContext context) {
+ RowDataTaskWriterFactory taskWriterFactory =
+ new RowDataTaskWriterFactory(
+ tableSupplier,
+ flinkRowType,
+ targetDataFileSize,
+ dataFileFormat,
+ writeProperties,
+ equalityFieldIds,
+ upsertMode);
+ IcebergStreamWriterMetrics metrics =
+ new IcebergStreamWriterMetrics(context.metricGroup(), table.name());
+ return new IcebergSinkWriter(
+ tableSupplier.get().name(),
+ taskWriterFactory,
+ metrics,
+ context.getSubtaskId(),
+ context.getAttemptNumber());
+ }
+
+ @Override
+ public Committer<IcebergCommittable> createCommitter(CommitterInitContext context) {
+ IcebergFilesCommitterMetrics metrics =
+ new IcebergFilesCommitterMetrics(context.metricGroup(), table.name());
+ return new IcebergCommitter(
+ tableLoader, branch, snapshotProperties, overwriteMode, workerPoolSize, sinkId, metrics);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<IcebergCommittable> getCommittableSerializer() {
+ return new IcebergCommittableSerializer();
+ }
+
+ @Override
+ public void addPostCommitTopology(
+ DataStream<CommittableMessage<IcebergCommittable>> committables) {
+ // TODO Support small file compaction
+ }
+
+ @Override
+ public DataStream<RowData> addPreWriteTopology(DataStream<RowData> inputDataStream) {
+ return distributeDataStream(inputDataStream);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<IcebergCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<WriteResult>> writeResults) {
+ TypeInformation<CommittableMessage<IcebergCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+
+ // global forces all output records send to subtask 0 of the downstream committer operator.
+ // This is to ensure commit only happen in one committer subtask.
+ // Once upstream Flink provides the capability of setting committer operator
+ // parallelism to 1, this can be removed.
+ return writeResults
+ .global()
+ .transform(
+ suffixIfNotNull(uidPrefix, table.name() + "-" + sinkId + "-pre-commit-topology"),
+ typeInformation,
+ new IcebergWriteAggregator(tableLoader))
+ .uid(suffixIfNotNull(uidPrefix, "pre-commit-topology"))
Review Comment:
`suffixIfNotNull(uidSuffix, suffix)` is used so that if the `$uidSuffix` is null, we return `$suffix` or instead we return `$uidSuffix-$uidSiffx`. So it's merely a convenient method that deals with `$uidSuffix` is null use case
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "stevenzwu (via GitHub)" <gi...@apache.org>.
stevenzwu commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1717845355
##########
flink/v1.19/flink/src/test/java/org/apache/iceberg/flink/sink/committer/TestIcebergCommitter.java:
##########
@@ -0,0 +1,1526 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import static org.apache.iceberg.flink.sink.ManifestOutputFileFactory.FLINK_MANIFEST_LOCATION;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommittableSummary;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.extractAndAssertCommittableWithLineage;
+import static org.apache.iceberg.flink.sink.SinkTestUtil.transformsToStreamElement;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.connector.sink2.SinkV2Assertions;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.runtime.operators.sink.CommitterOperatorFactory;
+import org.apache.flink.streaming.runtime.streamrecord.StreamElement;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.GenericManifestFile;
+import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.Metrics;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SnapshotSummary;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestBase;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.SimpleDataUtil;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.TestHelpers;
+import org.apache.iceberg.flink.sink.FlinkAppenderFactory;
+import org.apache.iceberg.flink.sink.FlinkManifestUtil;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.flink.sink.IcebergSink;
+import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(ParameterizedTestExtension.class)
+class TestIcebergCommitter extends TestBase {
+ private static final Logger LOG = LoggerFactory.getLogger(TestIcebergCommitter.class);
+ public static final String OPERATOR_ID = "flink-sink";
+ @TempDir File temporaryFolder;
+
+ @TempDir File flinkManifestFolder;
+
+ private Table table;
+
+ private TableLoader tableLoader;
+
+ @Parameter(index = 1)
+ private Boolean isStreamingMode;
+
+ @Parameter(index = 2)
+ private String branch;
+
+ private final String jobId = "jobId";
+ private final long dataFIleRowCount = 5L;
+
+ private final TestCommittableMessageTypeSerializer committableMessageTypeSerializer =
+ new TestCommittableMessageTypeSerializer();
+
+ private final DataFile dataFileTest1 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-1.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ private final DataFile dataFileTest2 =
+ DataFiles.builder(PartitionSpec.unpartitioned())
+ .withPath("/path/to/data-2.parquet")
+ .withFileSizeInBytes(0)
+ .withMetrics(
+ new Metrics(
+ dataFIleRowCount,
+ null, // no column sizes
+ ImmutableMap.of(1, 5L), // value count
+ ImmutableMap.of(1, 0L), // null count
+ null,
+ ImmutableMap.of(1, longToBuffer(0L)), // lower bounds
+ ImmutableMap.of(1, longToBuffer(4L)) // upper bounds
+ ))
+ .build();
+
+ @SuppressWarnings("checkstyle:NestedForDepth")
+ @Parameters(name = "formatVersion={0} isStreaming={1}, branch={2}")
+ protected static List<Object> parameters() {
+ List<Object> parameters = Lists.newArrayList();
+ for (Boolean isStreamingMode : new Boolean[] {true, false}) {
+ for (int formatVersion : new int[] {1, 2}) {
+ parameters.add(new Object[] {formatVersion, isStreamingMode, SnapshotRef.MAIN_BRANCH});
+ parameters.add(new Object[] {formatVersion, isStreamingMode, "test-branch"});
+ }
+ }
+ return parameters;
+ }
+
+ @BeforeEach
+ public void before() throws Exception {
+ String warehouse = temporaryFolder.getAbsolutePath();
+
+ String tablePath = warehouse.concat("/test");
+ assertThat(new File(tablePath).mkdir()).as("Should create the table path correctly.").isTrue();
+
+ Map<String, String> props =
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION,
+ String.valueOf(formatVersion),
+ FLINK_MANIFEST_LOCATION,
+ flinkManifestFolder.getAbsolutePath(),
+ IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS,
+ "1");
+ table = SimpleDataUtil.createTable(tablePath, props, false);
+ tableLoader = TableLoader.fromHadoopTable(tablePath);
+ }
+
+ @TestTemplate
+ public void testCommitTxnWithoutDataFiles() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ SimpleDataUtil.assertTableRows(table, Lists.newArrayList(), branch);
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId, -1);
+
+ for (long i = 1; i <= 3; i++) {
+ Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertMaxCommittedCheckpointId(jobId, i);
+ assertSnapshotSize((int) i);
+ }
+ }
+
+ @TestTemplate
+ public void testMxContinuousEmptyCommits() throws Exception {
+ table.updateProperties().set(IcebergCommitter.MAX_CONTINUOUS_EMPTY_COMMITS, "3").commit();
+ IcebergCommitter committer = getCommitter();
+ for (int i = 1; i <= 9; i++) {
+ Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList());
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ assertSnapshotSize(i / 3);
+ }
+ }
+
+ @TestTemplate
+ public void testCommitTxn() throws Exception {
+ IcebergCommitter committer = getCommitter();
+ assertSnapshotSize(0);
+ List<RowData> rows = Lists.newArrayListWithExpectedSize(3);
+ for (int i = 1; i <= 3; i++) {
+ RowData rowData = SimpleDataUtil.createRowData(i, "hello" + i);
+ DataFile dataFile = writeDataFile("data-" + i, ImmutableList.of(rowData));
+ rows.add(rowData);
+ WriteResult writeResult = of(dataFile);
+ Committer.CommitRequest<IcebergCommittable> commitRequest =
+ buildCommitRequestFor(jobId, i, Lists.newArrayList(writeResult));
+ committer.commit(Lists.newArrayList(commitRequest));
+ assertFlinkManifests(0);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.copyOf(rows), branch);
+ assertSnapshotSize(i);
+ assertMaxCommittedCheckpointId(jobId, i);
+ Map<String, String> summary = SimpleDataUtil.latestSnapshot(table, branch).summary();
+ assertThat(summary)
+ .containsEntry(
+ "flink.test", "org.apache.iceberg.flink.sink.committer.TestIcebergCommitter")
+ .containsEntry("added-data-files", "1")
+ .containsEntry("flink.operator-id", OPERATOR_ID)
+ .containsEntry("flink.job-id", "jobId");
+ }
+ }
+
+ @TestTemplate
+ public void testOrderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#1;
+ // 4. notifyCheckpointComplete for checkpoint#2;
+
+ long timestamp = 0;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+
+ harness.open();
+
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, OPERATOR_ID, dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, OPERATOR_ID, dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ OperatorSubtaskState snapshot = harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ assertMaxCommittedCheckpointId(jobId, firstCheckpointId);
+ assertFlinkManifests(1);
+
+ // 4. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testDisorderedEventsBetweenCheckpoints() throws Exception {
+ // It's possible that two checkpoints happen in the following orders:
+ // 1. snapshotState for checkpoint#1;
+ // 2. snapshotState for checkpoint#2;
+ // 3. notifyCheckpointComplete for checkpoint#2;
+ // 4. notifyCheckpointComplete for checkpoint#1;
+
+ long timestamp = 0;
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ harness = getTestHarness()) {
+
+ harness.open();
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello");
+ DataFile dataFile1 = writeDataFile("data-1", ImmutableList.of(row1));
+
+ processElement(jobId, 1, harness, 1, OPERATOR_ID, dataFile1);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 1. snapshotState for checkpoint#1
+ long firstCheckpointId = 1;
+ harness.snapshot(firstCheckpointId, ++timestamp);
+ assertFlinkManifests(1);
+
+ RowData row2 = SimpleDataUtil.createRowData(2, "world");
+ DataFile dataFile2 = writeDataFile("data-2", ImmutableList.of(row2));
+ processElement(jobId, 2, harness, 1, OPERATOR_ID, dataFile2);
+ assertMaxCommittedCheckpointId(jobId, -1L);
+
+ // 2. snapshotState for checkpoint#2
+ long secondCheckpointId = 2;
+ harness.snapshot(secondCheckpointId, ++timestamp);
+ assertFlinkManifests(2);
+
+ // 3. notifyCheckpointComplete for checkpoint#2
+ harness.notifyOfCompletedCheckpoint(secondCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+
+ // 4. notifyCheckpointComplete for checkpoint#1
+ harness.notifyOfCompletedCheckpoint(firstCheckpointId);
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1, row2), branch);
+ assertMaxCommittedCheckpointId(jobId, secondCheckpointId);
+ assertFlinkManifests(0);
+ }
+ }
+
+ @TestTemplate
+ public void testEmitCommittables() throws Exception {
+ TestCommitter committer = new TestCommitter(tableLoader);
+ IcebergSink sink =
+ spy(IcebergSink.forRowData(null).table(table).tableLoader(tableLoader).build());
+ doReturn(committer).when(sink).createCommitter(any());
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness =
+ new OneInputStreamOperatorTestHarness<>(
+ new CommitterOperatorFactory<>(sink, false, true))) {
+ testHarness.open();
+
+ WriteResult writeResult = WriteResult.builder().build();
+
+ IcebergCommittable commit =
+ new IcebergCommittable(
+ buildIcebergWriteAggregator(jobId, OPERATOR_ID)
+ .writeToManifest(Lists.newArrayList(writeResult), 1L),
+ jobId,
+ OPERATOR_ID,
+ 1L);
+
+ CommittableSummary<IcebergCommittable> committableSummary =
+ new CommittableSummary<>(1, 1, 1L, 1, 1, 0);
+ testHarness.processElement(new StreamRecord<>(committableSummary));
+ CommittableWithLineage<IcebergCommittable> committableWithLineage =
+ new CommittableWithLineage<>(commit, 1L, 1);
+ testHarness.processElement(new StreamRecord<>(committableWithLineage));
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(1);
+
+ assertThat(committer.getSuccessfulCommits()).isEqualTo(1);
+ List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommittableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommittableWithLineage(output.get(1)))
+ .hasCheckpointId(1L)
+ .hasSubtaskId(0)
+ .hasCommittable(commit);
+ }
+ }
+
+ @TestTemplate
+ public void testSingleCommit() throws Exception {
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ testHarness = getTestHarness()) {
+ testHarness.open();
+
+ long checkpointId = 1;
+
+ RowData row1 = SimpleDataUtil.createRowData(1, "hello1");
+ DataFile dataFile1 = writeDataFile("data-1-1", ImmutableList.of(row1));
+ CommittableSummary<IcebergCommittable> committableSummary =
+ processElement(jobId, checkpointId, testHarness, 1, OPERATOR_ID, dataFile1);
+
+ // Trigger commit
+ testHarness.notifyOfCompletedCheckpoint(checkpointId);
+
+ assertSnapshotSize(1);
+ assertMaxCommittedCheckpointId(jobId, 1L);
+
+ List<StreamElement> output = transformsToStreamElement(testHarness.getOutput());
+
+ SimpleDataUtil.assertTableRows(table, ImmutableList.of(row1), branch);
+ SinkV2Assertions.assertThat(extractAndAssertCommittableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommittableWithLineage(output.get(1)))
+ .hasSubtaskId(0)
+ .hasCheckpointId(checkpointId);
+ }
+
+ table.refresh();
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, "1")
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1");
+ }
+
+ /** The data was not committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob() throws Exception {
+ String jobId1 = "jobId1";
+ OperatorSubtaskState snapshot;
+
+ // We cannot test a different checkpoint thant 0 because when using the OperatorTestHarness
+ // for recovery the lastCompleted checkpoint is always reset to 0.
+ // see: https://github.com/apache/iceberg/issues/10942
+ long checkpointId = 0;
+ long timestamp = 0;
+ CommittableSummary<IcebergCommittable> committableSummary;
+
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ preJobTestHarness = getTestHarness()) {
+
+ preJobTestHarness.open();
+
+ committableSummary =
+ processElement(jobId1, checkpointId, preJobTestHarness, 1, OPERATOR_ID, dataFileTest1);
+
+ snapshot = preJobTestHarness.snapshot(checkpointId, ++timestamp);
+
+ assertThat(preJobTestHarness.getOutput()).isEmpty();
+ }
+
+ assertSnapshotSize(0);
+ assertMaxCommittedCheckpointId(jobId1, -1L);
+
+ String jobId2 = "jobId2";
+ try (OneInputStreamOperatorTestHarness<
+ CommittableMessage<IcebergCommittable>, CommittableMessage<IcebergCommittable>>
+ restored = getTestHarness()) {
+ restored.setup(committableMessageTypeSerializer);
+ restored.initializeState(snapshot);
+ restored.open();
+
+ // Previous committables are immediately committed if possible
+ List<StreamElement> output = transformsToStreamElement(restored.getOutput());
+ assertThat(output).hasSize(2);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommittableSummary(output.get(0)))
+ .hasFailedCommittables(committableSummary.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommittableWithLineage(output.get(1)))
+ .hasCheckpointId(0L)
+ .hasSubtaskId(0);
+
+ table.refresh();
+
+ Snapshot currentSnapshot = table.snapshot(branch);
+
+ assertThat(currentSnapshot.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "1")
+ .containsEntry("flink.job-id", jobId1);
+
+ checkpointId++;
+ CommittableSummary<IcebergCommittable> committableSummary2 =
+ processElement(jobId2, checkpointId, restored, 1, OPERATOR_ID, dataFileTest2);
+
+ // Trigger commit
+ restored.notifyOfCompletedCheckpoint(checkpointId);
+
+ List<StreamElement> output2 = transformsToStreamElement(restored.getOutput());
+ SinkV2Assertions.assertThat(extractAndAssertCommittableSummary(output2.get(0)))
+ .hasFailedCommittables(committableSummary2.getNumberOfFailedCommittables())
+ .hasOverallCommittables(committableSummary2.getNumberOfCommittables())
+ .hasPendingCommittables(0);
+
+ SinkV2Assertions.assertThat(extractAndAssertCommittableWithLineage(output2.get(1)))
+ .hasCheckpointId(0L)
+ .hasSubtaskId(0);
+ }
+
+ assertSnapshotSize(2);
+ assertMaxCommittedCheckpointId(jobId2, 1);
+
+ table.refresh();
+ Snapshot currentSnapshot2 = table.snapshot(branch);
+
+ assertThat(currentSnapshot2.summary())
+ .containsEntry(SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(dataFIleRowCount * 2))
+ .containsEntry(SnapshotSummary.TOTAL_DATA_FILES_PROP, "2")
+ .containsEntry("flink.job-id", jobId2);
+ }
+
+ /** The data was committed in the previous job. */
+ @TestTemplate
+ public void testStateRestoreFromPreJob2() throws Exception {
Review Comment:
`testStateRestoreFromPreJob` and `testStateRestoreFromPreJob2` names are not meaningful. should be sth like `testStateRestoreFromPreJobWithCommitted` and `testStateRestoreFromPreJobWithUncommitted`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1627770495
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.iceberg.flink.sink;
+
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION;
+import static org.apache.iceberg.TableProperties.AVRO_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION;
+import static org.apache.iceberg.TableProperties.ORC_COMPRESSION_STRATEGY;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION;
+import static org.apache.iceberg.TableProperties.PARQUET_COMPRESSION_LEVEL;
+import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import org.apache.flink.api.common.functions.MapFunction;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.CommitterInitContext;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.SupportsCommitter;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPostCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreCommitTopology;
+import org.apache.flink.streaming.api.connector.sink2.SupportsPreWriteTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.DataStreamSink;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.util.DataFormatConverters;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.Preconditions;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DistributionMode;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.PartitionField;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.SerializableTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.FlinkSchemaUtil;
+import org.apache.iceberg.flink.FlinkWriteConf;
+import org.apache.iceberg.flink.FlinkWriteOptions;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittable;
+import org.apache.iceberg.flink.sink.committer.IcebergCommittableSerializer;
+import org.apache.iceberg.flink.sink.committer.IcebergCommitter;
+import org.apache.iceberg.flink.sink.committer.IcebergWriteAggregator;
+import org.apache.iceberg.flink.sink.committer.WriteResultSerializer;
+import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.TypeUtil;
+import org.apache.iceberg.util.SerializableSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flink v2 sink offer different hooks to insert custom topologies into the sink. We will use the
+ * following:
+ *
+ * <ul>
+ * <li>{@link SupportsPreWriteTopology} which redistributes the data to the writers based on the
+ * {@link DistributionMode}
+ * <li>{@link org.apache.flink.api.connector.sink2.SinkWriter} which writes data files, and
+ * generates a {@link IcebergCommittable} to store the {@link
+ * org.apache.iceberg.io.WriteResult}
+ * <li>{@link SupportsPreCommitTopology} which we use to to place the {@link
+ * IcebergWriteAggregator} which merges the individual {@link
+ * org.apache.flink.api.connector.sink2.SinkWriter}'s {@link
+ * org.apache.iceberg.io.WriteResult}s to a single {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}
+ * <li>{@link IcebergCommitter} which stores the incoming {@link
+ * org.apache.iceberg.flink.sink.DeltaManifests}s in state for recovery, and commits them to
+ * the Iceberg table
+ * <li>{@link SupportsPostCommitTopology} we could use for incremental compaction later. This is
+ * not implemented yet.
+ * </ul>
+ *
+ * The job graph looks like below:
+ *
+ * <pre>{@code
+ * Flink sink
+ * +-----------------------------------------------------------------------------------+
+ * | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | Map 1 | ==> | | writer 1 | | committer 1 | ---> | post commit 1 | |
+ * +-------+ | +----------+ +-------------+ +---------------+ |
+ * | \ / \ |
+ * | \ / \ |
+ * | \ / \ |
+ * +-------+ | +----------+ \ +-------------------+ / +-------------+ \ +---------------+ |
+ * | Map 2 | ==> | | writer 2 | --->| commit aggregator | | committer 2 | | post commit 2 | |
+ * +-------+ | +----------+ +-------------------+ +-------------+ +---------------+ |
+ * | Commit only on |
+ * | committer 1 |
+ * +-----------------------------------------------------------------------------------+
+ * }</pre>
+ */
+public class IcebergSink
+ implements Sink<RowData>,
+ SupportsPreWriteTopology<RowData>,
+ SupportsCommitter<IcebergCommittable>,
+ SupportsPreCommitTopology<WriteResult, IcebergCommittable>,
+ SupportsPostCommitTopology<IcebergCommittable> {
+ private Table initTable;
+ private final TableLoader tableLoader;
+ private final Map<String, String> snapshotProperties;
+ private String uidPrefix;
+ private Committer<IcebergCommittable> sinkCommitter;
+ private final String sinkId;
+ private transient Builder builder;
+ private Map<String, String> writeProperties;
+ private RowType flinkRowType;
+ private SerializableSupplier<Table> tableSupplier;
+ private transient FlinkWriteConf flinkWriteConf;
+ private List<Integer> equalityFieldIds;
+ private boolean upsertMode;
+ private FileFormat dataFileFormat;
+ private long targetDataFileSize;
+ private String branch;
+ private boolean overwriteMode;
+ private int workerPoolSize;
+ private static final Logger LOG = LoggerFactory.getLogger(IcebergSink.class);
+
+ private IcebergSink(Builder builder) {
+ this.builder = builder;
Review Comment:
Yeah! The builder pattern is totally broken due to many, many refactors. I will rewrite it completely
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "rodmeneses (via GitHub)" <gi...@apache.org>.
rodmeneses commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1626273063
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/WriteResultSerializer.java:
##########
@@ -0,0 +1,63 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
+import org.apache.flink.util.InstantiationUtil;
+import org.apache.iceberg.io.WriteResult;
+
+public class WriteResultSerializer implements SimpleVersionedSerializer<WriteResult> {
+ private static final int VERSION_0 = 0;
+
+ @Override
+ public int getVersion() {
+ return VERSION_0;
+ }
+
+ @Override
+ public byte[] serialize(WriteResult writeResult) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+ byte[] result = InstantiationUtil.serializeObject(writeResult);
Review Comment:
> why not return this byte[] directly? why do we need to wrap?
Because the serialized bytes (out.toByteArray()) contains the `result.length` which is not inside `result`
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] A new implementation of an Iceberg Sink [WIP] that will be used with upcoming Flink Compaction jobs [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1596983987
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,465 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ private final TableLoader tableLoader;
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+ private transient boolean initialized = false;
+
+ public SinkCommitter(
Review Comment:
How do we handle Metrics?
FLIP-371 was for enable metrics for the Committers. We need to use it.
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599434778
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,439 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ private final TableLoader tableLoader;
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.tableLoader = tableLoader;
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ LOG.info("Noting to commit");
Review Comment:
Do we need to increment `continuousEmptyCheckpoints` here, and handle `maxContinuousEmptyCommits`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599434778
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkCommitter.java:
##########
@@ -0,0 +1,439 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
+import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.table.runtime.typeutils.SortedMapTypeInfo;
+import org.apache.iceberg.AppendFiles;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.ReplacePartitions;
+import org.apache.iceberg.RowDelta;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotUpdate;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.CommitSummary;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.IcebergFilesCommitterMetrics;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Maps;
+import org.apache.iceberg.types.Comparators;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.PropertyUtil;
+import org.apache.iceberg.util.ThreadPools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements the Flink SinkV2 {@link Committer} interface to implement the Iceberg
+ * commits. The implementation builds on the following assumptions:
+ *
+ * <ul>
+ * <li>There is a single {@link SinkCommittable} for every checkpoint
+ * <li>There is no late checkpoint - if checkpoint 'x' has received in one call, then after a
+ * successful run only checkpoints > x will arrive
+ * <li>There is no other writer which would generate another commit to the same branch with the
+ * same jobId-operatorId-checkpointId triplet
+ * </ul>
+ */
+public class SinkCommitter implements Committer<SinkCommittable>, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = LoggerFactory.getLogger(SinkCommitter.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ public static final long INITIAL_CHECKPOINT_ID = -1L;
+ public static final String FLINK_JOB_ID = "flink.job-id";
+ public static final String OPERATOR_ID = "flink.operator-id";
+ private static final String MAX_COMMITTED_CHECKPOINT_ID = "flink.max-committed-checkpoint-id";
+
+ @VisibleForTesting
+ static final String MAX_CONTINUOUS_EMPTY_COMMITS = "flink.max-continuous-empty-commits";
+
+ private final TableLoader tableLoader;
+ private final String branch;
+ private final Map<String, String> snapshotProperties;
+ private final boolean replacePartitions;
+ private final int workerPoolSize;
+ private final String prefix;
+ private transient IcebergFilesCommitterMetrics committerMetrics;
+ private transient Table table;
+ private transient int maxContinuousEmptyCommits;
+ private transient ExecutorService workerPool;
+ private transient int continuousEmptyCheckpoints = 0;
+
+ public SinkCommitter(
+ TableLoader tableLoader,
+ String branch,
+ Map<String, String> snapshotProperties,
+ boolean replacePartitions,
+ int workerPoolSize,
+ String prefix,
+ IcebergFilesCommitterMetrics committerMetrics) {
+ this.tableLoader = tableLoader;
+ this.branch = branch;
+ this.snapshotProperties = snapshotProperties;
+ this.replacePartitions = replacePartitions;
+ this.workerPoolSize = workerPoolSize;
+ this.prefix = prefix;
+ this.committerMetrics = committerMetrics;
+
+ if (!tableLoader.isOpen()) {
+ tableLoader.open();
+ }
+
+ this.table = tableLoader.loadTable();
+ this.maxContinuousEmptyCommits =
+ PropertyUtil.propertyAsInt(table.properties(), MAX_CONTINUOUS_EMPTY_COMMITS, 10);
+ Preconditions.checkArgument(
+ maxContinuousEmptyCommits > 0, MAX_CONTINUOUS_EMPTY_COMMITS + " must be positive");
+ this.workerPool =
+ ThreadPools.newWorkerPool("iceberg-worker-pool-" + table + "-" + prefix, workerPoolSize);
+ this.continuousEmptyCheckpoints = 0;
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<SinkCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ LOG.info("Noting to commit");
Review Comment:
Do we need to increment `continuousEmptyCheckpoints` here?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] Introduces the new IcebergSink based on the new V2 Flink Sink Abstraction [iceberg]
Posted by "pvary (via GitHub)" <gi...@apache.org>.
pvary commented on code in PR #10179:
URL: https://github.com/apache/iceberg/pull/10179#discussion_r1599429291
##########
flink/v1.19/flink/src/main/java/org/apache/iceberg/flink/sink/committer/SinkAggregator.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.iceberg.flink.sink.committer;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.flink.core.io.SimpleVersionedSerialization;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestWriter;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.flink.TableLoader;
+import org.apache.iceberg.flink.sink.DeltaManifests;
+import org.apache.iceberg.flink.sink.DeltaManifestsSerializer;
+import org.apache.iceberg.flink.sink.ManifestOutputFileFactory;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.WriteResult;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Operator which aggregates the individual {@link SinkCommittable} results from the writer (storing
+ * only {@link WriteResult} objects) to a single {@link SinkCommittable} per checkpoint (storing the
+ * serialized {@link DeltaManifests}, jobId, operatorId, checkpointId)
+ */
+public class SinkAggregator extends AbstractStreamOperator<CommittableMessage<SinkCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<WriteResult>, CommittableMessage<SinkCommittable>> {
+ private static final Logger LOG = LoggerFactory.getLogger(SinkAggregator.class);
+ private static final byte[] EMPTY_MANIFEST_DATA = new byte[0];
+ private final Collection<WriteResult> results;
+ private transient ManifestOutputFileFactory icebergManifestOutputFileFactory;
+ private transient Table table;
+ private final TableLoader tableLoader;
+ private final String prefix;
+
+ private transient boolean initialized = false;
+
+ private static final int FORMAT_V2 = 2;
+ private static final Long DUMMY_SNAPSHOT_ID = 0L;
+
+ public SinkAggregator(TableLoader tableLoader, String prefix) {
+ this.results = Sets.newHashSet();
+ this.tableLoader = tableLoader;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void open() throws Exception {
+ super.open();
+ if (!initialized) {
Review Comment:
Do we need this `initialized`?
--
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: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org