This is an automated email from the ASF dual-hosted git repository.
lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 2a3410cdc [flink] Introduce partition mark done (#3317)
2a3410cdc is described below
commit 2a3410cdc5e6d876436f2b9cbfe09ce660171bf6
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue May 14 12:10:27 2024 +0800
[flink] Introduce partition mark done (#3317)
---
docs/content/flink/sql-write.md | 30 +++
.../generated/flink_connector_configuration.html | 18 ++
.../main/java/org/apache/paimon/utils/IOUtils.java | 14 ++
.../paimon/partition/PartitionTimeExtractor.java | 8 +-
.../flink/sink/cdc/FlinkCdcMultiTableSink.java | 2 +-
.../apache/paimon/flink/FlinkConnectorOptions.java | 35 ++++
.../org/apache/paimon/flink/sink/Committer.java | 55 +++++-
.../paimon/flink/sink/CommitterOperator.java | 9 +-
.../apache/paimon/flink/sink/CompactorSink.java | 5 +-
.../org/apache/paimon/flink/sink/FlinkSink.java | 5 +-
.../apache/paimon/flink/sink/FlinkWriteSink.java | 12 +-
.../flink/sink/MultiTablesCompactorSink.java | 3 +-
.../apache/paimon/flink/sink/StoreCommitter.java | 41 +++-
.../paimon/flink/sink/StoreMultiCommitter.java | 25 +--
.../flink/sink/UnawareBucketCompactionSink.java | 5 +-
.../sink/partition/AddDonePartitionAction.java | 61 ++++++
.../flink/sink/partition/PartitionMarkDone.java | 207 +++++++++++++++++++++
.../sink/partition/PartitionMarkDoneAction.java | 27 +++
.../sink/partition/PartitionMarkDoneTrigger.java | 118 ++++++++++++
.../paimon/flink/sink/partition/SuccessFile.java | 105 +++++++++++
.../sink/partition/SuccessFileMarkDoneAction.java | 54 ++++++
.../paimon/flink/sink/CommitterOperatorTest.java | 22 ++-
.../paimon/flink/sink/StoreMultiCommitterTest.java | 8 +-
.../sink/partition/AddDonePartitionActionTest.java | 77 ++++++++
.../partition/PartitionMarkDoneTriggerTest.java | 107 +++++++++++
.../partition/SuccessFileMarkDoneActionTest.java | 52 ++++++
.../apache/paimon/hive/HiveCatalogITCaseBase.java | 60 +++++-
27 files changed, 1105 insertions(+), 60 deletions(-)
diff --git a/docs/content/flink/sql-write.md b/docs/content/flink/sql-write.md
index 52b8e4a62..8b275dbbc 100644
--- a/docs/content/flink/sql-write.md
+++ b/docs/content/flink/sql-write.md
@@ -206,3 +206,33 @@ DELETE FROM my_table WHERE currency = 'UNKNOWN';
{{< /tab >}}
{{< /tabs >}}
+
+## Partition Mark Done
+
+For partitioned tables, each partition may need to be scheduled to trigger
downstream batch computation. Therefore,
+it is necessary to choose this timing to indicate that it is ready for
scheduling and to minimize the amount of data
+drift during scheduling. We call this process: "Partition Mark Done".
+
+Example to mark done:
+```sql
+CREATE TABLE my_partitioned_table (
+ f0 INT,
+ f1 INT,
+ f2 INT,
+ ...
+ dt STRING
+) PARTITIONED BY (dt) WITH (
+ 'partition.timestamp-formatter'='yyyyMMdd',
+ 'partition.timestamp-pattern'='$dt',
+ 'partition.time-interval'='1 d',
+ 'partition.idle-time-to-done'='15 m'
+);
+```
+
+1. Firstly, you need to define the time parser of the partition and the time
interval between partitions in order to
+ determine when the partition can be properly marked done.
+2. Secondly, you need to define idle-time, which determines how long it takes
for the partition to have no new data,
+ and then it will be marked as done.
+3. Thirdly, by default, partition mark done will create _SUCCESS file, the
content of _SUCCESS file is a json, contains
+ `creationTime` and `modificationTime`, they can help you understand if
there is any delayed data. You can also
+ configure other actions.
diff --git
a/docs/layouts/shortcodes/generated/flink_connector_configuration.html
b/docs/layouts/shortcodes/generated/flink_connector_configuration.html
index b6befc9a1..0280581d2 100644
--- a/docs/layouts/shortcodes/generated/flink_connector_configuration.html
+++ b/docs/layouts/shortcodes/generated/flink_connector_configuration.html
@@ -68,6 +68,24 @@ under the License.
<td>Duration</td>
<td>Specific dynamic partition refresh interval for lookup, scan
all partitions and obtain corresponding partition.</td>
</tr>
+ <tr>
+ <td><h5>partition.idle-time-to-done</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>Duration</td>
+ <td>Set a time duration when a partition has no new data after
this time duration, mark the done status to indicate that the data is
ready.</td>
+ </tr>
+ <tr>
+ <td><h5>partition.mark-done-action</h5></td>
+ <td style="word-wrap: break-word;">"success-file"</td>
+ <td>String</td>
+ <td>Action to mark a partition done is to notify the downstream
application that the partition has finished writing, the partition is ready to
be read.<br />1. 'success-file': add '_success' file to directory.<br />2.
'done-partition': add 'xxx.done' partition to metastore.<br />Both can be
configured at the same time: 'done-partition,success-file'.</td>
+ </tr>
+ <tr>
+ <td><h5>partition.time-interval</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>Duration</td>
+ <td>You can specify time interval for partition, for example,
daily partition is '1 d', hourly partition is '1 h'.</td>
+ </tr>
<tr>
<td><h5>scan.infer-parallelism</h5></td>
<td style="word-wrap: break-word;">true</td>
diff --git a/paimon-common/src/main/java/org/apache/paimon/utils/IOUtils.java
b/paimon-common/src/main/java/org/apache/paimon/utils/IOUtils.java
index f8a78eb21..9878dec36 100644
--- a/paimon-common/src/main/java/org/apache/paimon/utils/IOUtils.java
+++ b/paimon-common/src/main/java/org/apache/paimon/utils/IOUtils.java
@@ -21,10 +21,13 @@ package org.apache.paimon.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
import static java.util.Arrays.asList;
@@ -125,6 +128,17 @@ public final class IOUtils {
}
}
+ public static String readUTF8Fully(final InputStream in) throws
IOException {
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(in,
StandardCharsets.UTF_8));
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ }
+ return builder.toString();
+ }
+
// ------------------------------------------------------------------------
// Silent I/O cleanup / closing
// ------------------------------------------------------------------------
diff --git
a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionTimeExtractor.java
b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionTimeExtractor.java
index caf0e7a1f..60976e8b5 100644
---
a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionTimeExtractor.java
+++
b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionTimeExtractor.java
@@ -34,6 +34,8 @@ import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -90,7 +92,11 @@ public class PartitionTimeExtractor {
this.formatter = formatter;
}
- public LocalDateTime extract(List<String> partitionKeys, List<Object>
partitionValues) {
+ public LocalDateTime extract(LinkedHashMap<String, String> spec) {
+ return extract(new ArrayList<>(spec.keySet()), new
ArrayList<>(spec.values()));
+ }
+
+ public LocalDateTime extract(List<String> partitionKeys, List<?>
partitionValues) {
LocalDateTime dateTime = null;
try {
String timestampString;
diff --git
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
index e8dd0fde3..fb2bfe950 100644
---
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
+++
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
@@ -153,7 +153,7 @@ public class FlinkCdcMultiTableSink implements Serializable
{
// commit new files list even if they're empty.
// Otherwise we can't tell if the commit is successful after
// a restart.
- return (user, metricGroup) -> new StoreMultiCommitter(catalogLoader,
user, metricGroup);
+ return context -> new StoreMultiCommitter(catalogLoader, context);
}
protected CommittableStateManager<WrappedManifestCommittable>
createCommittableStateManager() {
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
index 90eb9ce32..e02b1c124 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
@@ -339,6 +339,41 @@ public class FlinkConnectorOptions {
.withDescription(
"Allow sink committer and writer operator to be
chained together");
+ public static final ConfigOption<Duration> PARTITION_IDLE_TIME_TO_DONE =
+ key("partition.idle-time-to-done")
+ .durationType()
+ .noDefaultValue()
+ .withDescription(
+ "Set a time duration when a partition has no new
data after this time duration, "
+ + "mark the done status to indicate that
the data is ready.");
+
+ public static final ConfigOption<Duration> PARTITION_TIME_INTERVAL =
+ key("partition.time-interval")
+ .durationType()
+ .noDefaultValue()
+ .withDescription(
+ "You can specify time interval for partition, for
example, "
+ + "daily partition is '1 d', hourly
partition is '1 h'.");
+
+ public static final ConfigOption<String> PARTITION_MARK_DONE_ACTION =
+ key("partition.mark-done-action")
+ .stringType()
+ .defaultValue("success-file")
+ .withDescription(
+ Description.builder()
+ .text(
+ "Action to mark a partition done
is to notify the downstream application that the partition"
+ + " has finished writing,
the partition is ready to be read.")
+ .linebreak()
+ .text("1. 'success-file': add '_success'
file to directory.")
+ .linebreak()
+ .text(
+ "2. 'done-partition': add
'xxx.done' partition to metastore.")
+ .linebreak()
+ .text(
+ "Both can be configured at the
same time: 'done-partition,success-file'.")
+ .build());
+
public static List<ConfigOption<?>> getOptions() {
final Field[] fields = FlinkConnectorOptions.class.getFields();
final List<ConfigOption<?>> list = new ArrayList<>(fields.length);
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
index d92417be7..8d08c2471 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/Committer.java
@@ -18,8 +18,11 @@
package org.apache.paimon.flink.sink;
+import org.apache.flink.api.common.state.OperatorStateStore;
import org.apache.flink.metrics.groups.OperatorMetricGroup;
+import javax.annotation.Nullable;
+
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
@@ -57,7 +60,55 @@ public interface Committer<CommitT, GlobalCommitT> extends
AutoCloseable {
/** Factory to create {@link Committer}. */
interface Factory<CommitT, GlobalCommitT> extends Serializable {
- Committer<CommitT, GlobalCommitT> create(
- String commitUser, OperatorMetricGroup metricGroup);
+ Committer<CommitT, GlobalCommitT> create(Context context);
+ }
+
+ /** Context to create {@link Committer}. */
+ interface Context {
+
+ String commitUser();
+
+ @Nullable
+ OperatorMetricGroup metricGroup();
+
+ boolean streamingCheckpointEnabled();
+
+ boolean isRestored();
+
+ OperatorStateStore stateStore();
+ }
+
+ static Context createContext(
+ String commitUser,
+ @Nullable OperatorMetricGroup metricGroup,
+ boolean streamingCheckpointEnabled,
+ boolean isRestored,
+ OperatorStateStore stateStore) {
+ return new Committer.Context() {
+ @Override
+ public String commitUser() {
+ return commitUser;
+ }
+
+ @Override
+ public OperatorMetricGroup metricGroup() {
+ return metricGroup;
+ }
+
+ @Override
+ public boolean streamingCheckpointEnabled() {
+ return streamingCheckpointEnabled;
+ }
+
+ @Override
+ public boolean isRestored() {
+ return isRestored;
+ }
+
+ @Override
+ public OperatorStateStore stateStore() {
+ return stateStore;
+ }
+ };
}
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
index facd3b398..3220512b0 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java
@@ -119,7 +119,14 @@ public class CommitterOperator<CommitT, GlobalCommitT>
extends AbstractStreamOpe
StateUtils.getSingleValueFromState(
context, "commit_user_state", String.class,
initialCommitUser);
// parallelism of commit operator is always 1, so commitUser will
never be null
- committer = committerFactory.create(commitUser, getMetricGroup());
+ committer =
+ committerFactory.create(
+ Committer.createContext(
+ commitUser,
+ getMetricGroup(),
+ streamingCheckpointEnabled,
+ context.isRestored(),
+ context.getOperatorStateStore()));
committableStateManager.initializeState(context, committer);
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CompactorSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CompactorSink.java
index 8e0882456..7dc3ab115 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CompactorSink.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CompactorSink.java
@@ -40,9 +40,8 @@ public class CompactorSink extends FlinkSink<RowData> {
}
@Override
- protected Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory(
- boolean streamingCheckpointEnabled) {
- return (user, metricGroup) -> new
StoreCommitter(table.newCommit(user), metricGroup);
+ protected Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory() {
+ return context -> new StoreCommitter(table,
table.newCommit(context.commitUser()), context);
}
@Override
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
index 92683e8f7..cf9bd487a 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
@@ -233,7 +233,7 @@ public abstract class FlinkSink<T> implements Serializable {
true,
options.get(SINK_COMMITTER_OPERATOR_CHAINING),
commitUser,
- createCommitterFactory(streamingCheckpointEnabled),
+ createCommitterFactory(),
createCommittableStateManager());
if (options.get(SINK_AUTO_TAG_FOR_SAVEPOINT)) {
committerOperator =
@@ -310,8 +310,7 @@ public abstract class FlinkSink<T> implements Serializable {
protected abstract OneInputStreamOperator<T, Committable>
createWriteOperator(
StoreSinkWrite.Provider writeProvider, String commitUser);
- protected abstract Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory(
- boolean streamingCheckpointEnabled);
+ protected abstract Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory();
protected abstract CommittableStateManager<ManifestCommittable>
createCommittableStateManager();
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkWriteSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkWriteSink.java
index b812c0491..0d6f245ba 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkWriteSink.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkWriteSink.java
@@ -40,18 +40,18 @@ public abstract class FlinkWriteSink<T> extends
FlinkSink<T> {
}
@Override
- protected Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory(
- boolean streamingCheckpointEnabled) {
+ protected Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory() {
// If checkpoint is enabled for streaming job, we have to
// commit new files list even if they're empty.
// Otherwise we can't tell if the commit is successful after
// a restart.
- return (user, metricGroup) ->
+ return context ->
new StoreCommitter(
- table.newCommit(user)
+ table,
+ table.newCommit(context.commitUser())
.withOverwrite(overwritePartition)
-
.ignoreEmptyCommit(!streamingCheckpointEnabled),
- metricGroup);
+
.ignoreEmptyCommit(!context.streamingCheckpointEnabled()),
+ context);
}
@Override
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesCompactorSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesCompactorSink.java
index f03b5b18b..cfc1ec1cd 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesCompactorSink.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/MultiTablesCompactorSink.java
@@ -153,8 +153,7 @@ public class MultiTablesCompactorSink implements
Serializable {
createCommitterFactory() {
Map<String, String> dynamicOptions = options.toMap();
dynamicOptions.put(CoreOptions.WRITE_ONLY.key(), "false");
- return (user, metricGroup) ->
- new StoreMultiCommitter(catalogLoader, user, metricGroup,
true, dynamicOptions);
+ return context -> new StoreMultiCommitter(catalogLoader, context,
true, dynamicOptions);
}
protected CommittableStateManager<WrappedManifestCommittable>
createCommittableStateManager() {
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
index 17c1d209a..922949cce 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreCommitter.java
@@ -20,15 +20,15 @@ package org.apache.paimon.flink.sink;
import org.apache.paimon.annotation.VisibleForTesting;
import org.apache.paimon.flink.metrics.FlinkMetricRegistry;
+import org.apache.paimon.flink.sink.partition.PartitionMarkDone;
import org.apache.paimon.io.DataFileMeta;
import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.sink.CommitMessage;
import org.apache.paimon.table.sink.CommitMessageImpl;
import org.apache.paimon.table.sink.TableCommit;
import org.apache.paimon.table.sink.TableCommitImpl;
-import org.apache.flink.metrics.groups.OperatorMetricGroup;
-
import javax.annotation.Nullable;
import java.io.IOException;
@@ -43,16 +43,28 @@ public class StoreCommitter implements
Committer<Committable, ManifestCommittabl
private final TableCommitImpl commit;
@Nullable private final CommitterMetrics committerMetrics;
+ @Nullable private final PartitionMarkDone partitionMarkDone;
- public StoreCommitter(TableCommit commit, @Nullable OperatorMetricGroup
metricGroup) {
+ public StoreCommitter(FileStoreTable table, TableCommit commit, Context
context) {
this.commit = (TableCommitImpl) commit;
- if (metricGroup != null) {
- this.commit.withMetricRegistry(new
FlinkMetricRegistry(metricGroup));
- this.committerMetrics = new
CommitterMetrics(metricGroup.getIOMetricGroup());
+ if (context.metricGroup() != null) {
+ this.commit.withMetricRegistry(new
FlinkMetricRegistry(context.metricGroup()));
+ this.committerMetrics = new
CommitterMetrics(context.metricGroup().getIOMetricGroup());
} else {
this.committerMetrics = null;
}
+
+ try {
+ this.partitionMarkDone =
+ PartitionMarkDone.create(
+ context.streamingCheckpointEnabled(),
+ context.isRestored(),
+ context.stateStore(),
+ table);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
@VisibleForTesting
@@ -99,15 +111,30 @@ public class StoreCommitter implements
Committer<Committable, ManifestCommittabl
throws IOException, InterruptedException {
commit.commitMultiple(committables, false);
calcNumBytesAndRecordsOut(committables);
+ if (partitionMarkDone != null) {
+ partitionMarkDone.notifyCommittable(committables);
+ }
}
@Override
public int filterAndCommit(List<ManifestCommittable> globalCommittables) {
- return commit.filterAndCommitMultiple(globalCommittables);
+ int committed = commit.filterAndCommitMultiple(globalCommittables);
+ if (partitionMarkDone != null) {
+ partitionMarkDone.notifyCommittable(globalCommittables);
+ }
+ return committed;
}
@Override
public Map<Long, List<Committable>>
groupByCheckpoint(Collection<Committable> committables) {
+ if (partitionMarkDone != null) {
+ try {
+ partitionMarkDone.snapshotState();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
Map<Long, List<Committable>> grouped = new HashMap<>();
for (Committable c : committables) {
grouped.computeIfAbsent(c.checkpointId(), k -> new
ArrayList<>()).add(c);
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
index ebe1fd645..efa2aefe3 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/StoreMultiCommitter.java
@@ -26,9 +26,6 @@ import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.sink.CommitMessage;
import org.apache.flink.api.java.tuple.Tuple2;
-import org.apache.flink.metrics.groups.OperatorMetricGroup;
-
-import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
@@ -48,8 +45,7 @@ public class StoreMultiCommitter
implements Committer<MultiTableCommittable,
WrappedManifestCommittable> {
private final Catalog catalog;
- private final String commitUser;
- @Nullable private final OperatorMetricGroup flinkMetricGroup;
+ private final Context context;
// To make the commit behavior consistent with that of Committer,
// StoreMultiCommitter manages multiple committers which are
@@ -60,22 +56,17 @@ public class StoreMultiCommitter
private final boolean ignoreEmptyCommit;
private final Map<String, String> dynamicOptions;
- public StoreMultiCommitter(
- Catalog.Loader catalogLoader,
- String commitUser,
- @Nullable OperatorMetricGroup flinkMetricGroup) {
- this(catalogLoader, commitUser, flinkMetricGroup, false,
Collections.emptyMap());
+ public StoreMultiCommitter(Catalog.Loader catalogLoader, Context context) {
+ this(catalogLoader, context, false, Collections.emptyMap());
}
public StoreMultiCommitter(
Catalog.Loader catalogLoader,
- String commitUser,
- @Nullable OperatorMetricGroup flinkMetricGroup,
+ Context context,
boolean ignoreEmptyCommit,
Map<String, String> dynamicOptions) {
this.catalog = catalogLoader.load();
- this.commitUser = commitUser;
- this.flinkMetricGroup = flinkMetricGroup;
+ this.context = context;
this.ignoreEmptyCommit = ignoreEmptyCommit;
this.dynamicOptions = dynamicOptions;
this.tableCommitters = new HashMap<>();
@@ -202,8 +193,10 @@ public class StoreMultiCommitter
}
committer =
new StoreCommitter(
-
table.newCommit(commitUser).ignoreEmptyCommit(ignoreEmptyCommit),
- flinkMetricGroup);
+ table,
+ table.newCommit(context.commitUser())
+ .ignoreEmptyCommit(ignoreEmptyCommit),
+ context);
tableCommitters.put(tableId, committer);
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/UnawareBucketCompactionSink.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/UnawareBucketCompactionSink.java
index cf825cec7..6785c53fb 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/UnawareBucketCompactionSink.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/UnawareBucketCompactionSink.java
@@ -48,9 +48,8 @@ public class UnawareBucketCompactionSink extends
FlinkSink<AppendOnlyCompactionT
}
@Override
- protected Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory(
- boolean streamingCheckpointEnabled) {
- return (s, metricGroup) -> new StoreCommitter(table.newCommit(s),
metricGroup);
+ protected Committer.Factory<Committable, ManifestCommittable>
createCommitterFactory() {
+ return context -> new StoreCommitter(table,
table.newCommit(context.commitUser()), context);
}
@Override
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/AddDonePartitionAction.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/AddDonePartitionAction.java
new file mode 100644
index 000000000..ea888c6d5
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/AddDonePartitionAction.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.paimon.flink.sink.partition;
+
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.metastore.MetastoreClient;
+
+import org.apache.paimon.shade.guava30.com.google.common.collect.Iterators;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+
+import static
org.apache.paimon.utils.PartitionPathUtils.extractPartitionSpecFromPath;
+
+/** A {@link PartitionMarkDoneAction} which add ".done" partition. */
+public class AddDonePartitionAction implements PartitionMarkDoneAction {
+
+ private final MetastoreClient metastoreClient;
+
+ public AddDonePartitionAction(MetastoreClient metastoreClient) {
+ this.metastoreClient = metastoreClient;
+ }
+
+ @Override
+ public void markDone(String partition) throws Exception {
+ LinkedHashMap<String, String> doneSpec =
extractPartitionSpecFromPath(new Path(partition));
+ Entry<String, String> lastField = tailEntry(doneSpec);
+ doneSpec.put(lastField.getKey(), lastField.getValue() + ".done");
+ metastoreClient.addPartition(doneSpec);
+ }
+
+ private Entry<String, String> tailEntry(LinkedHashMap<String, String>
partitionSpec) {
+ return Iterators.getLast(partitionSpec.entrySet().iterator());
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ metastoreClient.close();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDone.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDone.java
new file mode 100644
index 000000000..b4b4aa0bd
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDone.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink.sink.partition;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.manifest.ManifestCommittable;
+import org.apache.paimon.metastore.MetastoreClient;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.partition.PartitionTimeExtractor;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.utils.RowDataPartitionComputer;
+
+import org.apache.flink.api.common.state.ListState;
+import org.apache.flink.api.common.state.ListStateDescriptor;
+import org.apache.flink.api.common.state.OperatorStateStore;
+import org.apache.flink.api.common.typeutils.base.ListSerializer;
+import org.apache.flink.api.common.typeutils.base.StringSerializer;
+import org.apache.flink.table.utils.PartitionPathUtils;
+
+import javax.annotation.Nullable;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.apache.paimon.CoreOptions.METASTORE_PARTITIONED_TABLE;
+import static
org.apache.paimon.flink.FlinkConnectorOptions.PARTITION_IDLE_TIME_TO_DONE;
+import static
org.apache.paimon.flink.FlinkConnectorOptions.PARTITION_MARK_DONE_ACTION;
+import static
org.apache.paimon.flink.FlinkConnectorOptions.PARTITION_TIME_INTERVAL;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+import static org.apache.paimon.utils.Preconditions.checkNotNull;
+
+/** Mark partition done. */
+public class PartitionMarkDone implements Closeable {
+
+ private static final ListStateDescriptor<List<String>>
PENDING_PARTITIONS_STATE_DESC =
+ new ListStateDescriptor<>(
+ "mark-done-pending-partitions",
+ new ListSerializer<>(StringSerializer.INSTANCE));
+
+ private final RowDataPartitionComputer partitionComputer;
+ private final PartitionMarkDoneTrigger trigger;
+ private final List<PartitionMarkDoneAction> actions;
+
+ @Nullable
+ public static PartitionMarkDone create(
+ boolean isStreaming,
+ boolean isRestored,
+ OperatorStateStore stateStore,
+ FileStoreTable table)
+ throws Exception {
+ if (!isStreaming) {
+ return null;
+ }
+
+ List<String> partitionKeys = table.partitionKeys();
+ if (partitionKeys.isEmpty()) {
+ return null;
+ }
+
+ CoreOptions coreOptions = table.coreOptions();
+ Options options = coreOptions.toConfiguration();
+
+ Duration idleToDone = options.get(PARTITION_IDLE_TIME_TO_DONE);
+ if (idleToDone == null) {
+ return null;
+ }
+
+ MetastoreClient.Factory metastoreClientFactory =
+ table.catalogEnvironment().metastoreClientFactory();
+ checkNotNull(
+ metastoreClientFactory, "Cannot mark done partition for table
without metastore.");
+ checkArgument(
+ coreOptions.partitionedTableInMetastore(),
+ "Table should enable %s",
+ METASTORE_PARTITIONED_TABLE.key());
+
+ RowDataPartitionComputer partitionComputer =
+ new RowDataPartitionComputer(
+ coreOptions.partitionDefaultName(),
+ table.schema().logicalPartitionType(),
+ partitionKeys.toArray(new String[0]));
+
+ PartitionMarkDoneTrigger trigger =
+ new PartitionMarkDoneTrigger(
+ new PartitionMarkDoneTriggerState(isRestored,
stateStore),
+ new PartitionTimeExtractor(
+ coreOptions.partitionTimestampPattern(),
+ coreOptions.partitionTimestampFormatter()),
+ options.get(PARTITION_TIME_INTERVAL),
+ idleToDone);
+
+ List<PartitionMarkDoneAction> actions =
+
Arrays.asList(options.get(PARTITION_MARK_DONE_ACTION).split(",")).stream()
+ .map(
+ action -> {
+ switch (action) {
+ case "success-file":
+ return new
SuccessFileMarkDoneAction(
+ table.fileIO(),
table.location());
+ case "done-partition":
+ return new AddDonePartitionAction(
+
metastoreClientFactory.create());
+ default:
+ throw new
UnsupportedOperationException(action);
+ }
+ })
+ .collect(Collectors.toList());
+
+ return new PartitionMarkDone(partitionComputer, trigger, actions);
+ }
+
+ public PartitionMarkDone(
+ RowDataPartitionComputer partitionComputer,
+ PartitionMarkDoneTrigger trigger,
+ List<PartitionMarkDoneAction> actions) {
+ this.partitionComputer = partitionComputer;
+ this.trigger = trigger;
+ this.actions = actions;
+ }
+
+ public void notifyCommittable(List<ManifestCommittable> committables) {
+ Set<BinaryRow> partitions = new HashSet<>();
+ for (ManifestCommittable committable : committables) {
+ committable.fileCommittables().stream()
+ .map(CommitMessage::partition)
+ .forEach(partitions::add);
+ }
+
+ partitions.stream()
+ .map(partitionComputer::generatePartValues)
+ .map(PartitionPathUtils::generatePartitionPath)
+ .forEach(trigger::notifyPartition);
+
+ for (String partition : trigger.donePartitions()) {
+ try {
+ for (PartitionMarkDoneAction action : actions) {
+ action.markDone(partition);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public void snapshotState() throws Exception {
+ trigger.snapshotState();
+ }
+
+ @Override
+ public void close() throws IOException {
+ for (PartitionMarkDoneAction action : actions) {
+ action.close();
+ }
+ }
+
+ private static class PartitionMarkDoneTriggerState implements
PartitionMarkDoneTrigger.State {
+
+ private final boolean isRestored;
+ private final ListState<List<String>> pendingPartitionsState;
+
+ private PartitionMarkDoneTriggerState(boolean isRestored,
OperatorStateStore stateStore)
+ throws Exception {
+ this.isRestored = isRestored;
+ this.pendingPartitionsState =
stateStore.getListState(PENDING_PARTITIONS_STATE_DESC);
+ }
+
+ @Override
+ public List<String> restore() throws Exception {
+ List<String> pendingPartitions = new ArrayList<>();
+ if (isRestored) {
+
pendingPartitions.addAll(pendingPartitionsState.get().iterator().next());
+ }
+ return pendingPartitions;
+ }
+
+ @Override
+ public void update(List<String> partitions) throws Exception {
+
pendingPartitionsState.update(Collections.singletonList(partitions));
+ }
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneAction.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneAction.java
new file mode 100644
index 000000000..78d05ccc5
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneAction.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink.sink.partition;
+
+import java.io.Closeable;
+
+/** Action to mark partitions done. */
+public interface PartitionMarkDoneAction extends Closeable {
+
+ void markDone(String partition) throws Exception;
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneTrigger.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneTrigger.java
new file mode 100644
index 000000000..946d2b209
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneTrigger.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.paimon.flink.sink.partition;
+
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.partition.PartitionTimeExtractor;
+import org.apache.paimon.utils.StringUtils;
+
+import java.time.Duration;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import static
org.apache.paimon.utils.PartitionPathUtils.extractPartitionSpecFromPath;
+
+/** Trigger to mark partitions done. */
+public class PartitionMarkDoneTrigger {
+
+ private final State state;
+ private final PartitionTimeExtractor timeExtractor;
+ private final long timeInternal;
+ private final long idleTime;
+ private final Map<String, Long> pendingPartitions;
+
+ public PartitionMarkDoneTrigger(
+ State state,
+ PartitionTimeExtractor timeExtractor,
+ Duration timeInternal,
+ Duration idleTime)
+ throws Exception {
+ this(state, timeExtractor, timeInternal, idleTime,
System.currentTimeMillis());
+ }
+
+ PartitionMarkDoneTrigger(
+ State state,
+ PartitionTimeExtractor timeExtractor,
+ Duration timeInternal,
+ Duration idleTime,
+ long currentTimeMillis)
+ throws Exception {
+ this.state = state;
+ this.timeExtractor = timeExtractor;
+ this.timeInternal = timeInternal.toMillis();
+ this.idleTime = idleTime.toMillis();
+ this.pendingPartitions = new HashMap<>();
+ state.restore().forEach(p -> pendingPartitions.put(p,
currentTimeMillis));
+ }
+
+ public void notifyPartition(String partition) {
+ notifyPartition(partition, System.currentTimeMillis());
+ }
+
+ void notifyPartition(String partition, long currentTimeMillis) {
+ if (!StringUtils.isNullOrWhitespaceOnly(partition)) {
+ this.pendingPartitions.put(partition, currentTimeMillis);
+ }
+ }
+
+ public List<String> donePartitions() {
+ return donePartitions(System.currentTimeMillis());
+ }
+
+ public List<String> donePartitions(long currentTimeMillis) {
+ List<String> needDone = new ArrayList<>();
+ Iterator<Map.Entry<String, Long>> iter =
pendingPartitions.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry<String, Long> entry = iter.next();
+ String partition = entry.getKey();
+
+ long lastUpdateTime = entry.getValue();
+ long partitionStartTime =
+ timeExtractor
+ .extract(extractPartitionSpecFromPath(new
Path(partition)))
+ .atZone(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli();
+ long partitionEndTime = partitionStartTime + timeInternal;
+ lastUpdateTime = Math.max(lastUpdateTime, partitionEndTime);
+
+ if (currentTimeMillis - lastUpdateTime > idleTime) {
+ needDone.add(partition);
+ iter.remove();
+ }
+ }
+ return needDone;
+ }
+
+ public void snapshotState() throws Exception {
+ state.update(new ArrayList<>(pendingPartitions.keySet()));
+ }
+
+ /** State to store partitions. */
+ public interface State {
+
+ List<String> restore() throws Exception;
+
+ void update(List<String> partitions) throws Exception;
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/SuccessFile.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/SuccessFile.java
new file mode 100644
index 000000000..4d3656e6f
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/SuccessFile.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink.sink.partition;
+
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.utils.JsonSerdeUtil;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Objects;
+
+/** Json represents a success file. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class SuccessFile {
+
+ private static final String FIELD_CREATION_TIME = "creationTime";
+ private static final String FIELD_MODIFICATION_TIME = "modificationTime";
+
+ @JsonProperty(FIELD_CREATION_TIME)
+ private final long creationTime;
+
+ @JsonProperty(FIELD_MODIFICATION_TIME)
+ private final long modificationTime;
+
+ @JsonCreator
+ public SuccessFile(
+ @JsonProperty(FIELD_CREATION_TIME) long creationTime,
+ @JsonProperty(FIELD_MODIFICATION_TIME) long modificationTime) {
+ this.creationTime = creationTime;
+ this.modificationTime = modificationTime;
+ }
+
+ @JsonGetter(FIELD_CREATION_TIME)
+ public long creationTime() {
+ return creationTime;
+ }
+
+ @JsonGetter(FIELD_MODIFICATION_TIME)
+ public long modificationTime() {
+ return modificationTime;
+ }
+
+ public SuccessFile updateModificationTime(long modificationTime) {
+ return new SuccessFile(creationTime, modificationTime);
+ }
+
+ public String toJson() {
+ return JsonSerdeUtil.toJson(this);
+ }
+
+ public static SuccessFile fromJson(String json) {
+ return JsonSerdeUtil.fromJson(json, SuccessFile.class);
+ }
+
+ @Nullable
+ public static SuccessFile safelyFromPath(FileIO fileIO, Path path) throws
IOException {
+ try {
+ String json = fileIO.readFileUtf8(path);
+ return SuccessFile.fromJson(json);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SuccessFile that = (SuccessFile) o;
+ return creationTime == that.creationTime && modificationTime ==
that.modificationTime;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(creationTime, modificationTime);
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/SuccessFileMarkDoneAction.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/SuccessFileMarkDoneAction.java
new file mode 100644
index 000000000..ac97b6f14
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/partition/SuccessFileMarkDoneAction.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.paimon.flink.sink.partition;
+
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+
+/** A {@link PartitionMarkDoneAction} which create "_SUCCESS" file. */
+public class SuccessFileMarkDoneAction implements PartitionMarkDoneAction {
+
+ public static final String SUCCESS_FILE_NAME = "_SUCCESS";
+
+ private final FileIO fileIO;
+ private final Path tablePath;
+
+ public SuccessFileMarkDoneAction(FileIO fileIO, Path tablePath) {
+ this.fileIO = fileIO;
+ this.tablePath = tablePath;
+ }
+
+ @Override
+ public void markDone(String partition) throws Exception {
+ Path partitionPath = new Path(tablePath, partition);
+ Path successPath = new Path(partitionPath, SUCCESS_FILE_NAME);
+
+ long currentTime = System.currentTimeMillis();
+ SuccessFile successFile = SuccessFile.safelyFromPath(fileIO,
successPath);
+ if (successFile == null) {
+ successFile = new SuccessFile(currentTime, currentTime);
+ } else {
+ successFile = successFile.updateModificationTime(currentTime);
+ }
+ fileIO.overwriteFileUtf8(successPath, successFile.toJson());
+ }
+
+ @Override
+ public void close() {}
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/CommitterOperatorTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/CommitterOperatorTest.java
index 589eb773b..672ccfd39 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/CommitterOperatorTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/CommitterOperatorTest.java
@@ -561,7 +561,9 @@ public class CommitterOperatorTest extends
CommitterOperatorTestBase {
StreamTableCommit commit = table.newCommit(initialCommitUser);
OperatorMetricGroup metricGroup =
UnregisteredMetricsGroup.createOperatorMetricGroup();
- StoreCommitter committer = new StoreCommitter(commit, metricGroup);
+ StoreCommitter committer =
+ new StoreCommitter(
+ table, commit, Committer.createContext("",
metricGroup, true, false, null));
committer.commit(Collections.singletonList(manifestCommittable));
CommitterMetrics metrics = committer.getCommitterMetrics();
assertThat(metrics.getNumBytesOutCounter().getCount()).isEqualTo(285);
@@ -728,10 +730,13 @@ public class CommitterOperatorTest extends
CommitterOperatorTestBase {
true,
true,
commitUser == null ? initialCommitUser : commitUser,
- (user, metricGroup) ->
+ context ->
new StoreCommitter(
-
table.newStreamWriteBuilder().withCommitUser(user).newCommit(),
- metricGroup),
+ table,
+ table.newStreamWriteBuilder()
+ .withCommitUser(context.commitUser())
+ .newCommit(),
+ context),
committableStateManager);
}
@@ -745,10 +750,13 @@ public class CommitterOperatorTest extends
CommitterOperatorTestBase {
true,
true,
commitUser == null ? initialCommitUser : commitUser,
- (user, metricGroup) ->
+ context ->
new StoreCommitter(
-
table.newStreamWriteBuilder().withCommitUser(user).newCommit(),
- metricGroup),
+ table,
+ table.newStreamWriteBuilder()
+ .withCommitUser(context.commitUser())
+ .newCommit(),
+ context),
committableStateManager) {
@Override
public void initializeState(StateInitializationContext context)
throws Exception {
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/StoreMultiCommitterTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/StoreMultiCommitterTest.java
index 36d31ff41..94cfb3670 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/StoreMultiCommitterTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/StoreMultiCommitterTest.java
@@ -642,9 +642,7 @@ class StoreMultiCommitterTest {
false,
true,
initialCommitUser,
- (user, metricGroup) ->
- new StoreMultiCommitter(
- catalogLoader, initialCommitUser,
metricGroup),
+ context -> new StoreMultiCommitter(catalogLoader,
context),
new RestoreAndFailCommittableStateManager<>(
() ->
new VersionedSerializerWrapper<>(
@@ -660,9 +658,7 @@ class StoreMultiCommitterTest {
false,
true,
initialCommitUser,
- (user, metricGroup) ->
- new StoreMultiCommitter(
- catalogLoader, initialCommitUser,
metricGroup),
+ context -> new StoreMultiCommitter(catalogLoader,
context),
new
CommittableStateManager<WrappedManifestCommittable>() {
@Override
public void initializeState(
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/AddDonePartitionActionTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/AddDonePartitionActionTest.java
new file mode 100644
index 000000000..4fc31e41d
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/AddDonePartitionActionTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink.sink.partition;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.metastore.MetastoreClient;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.apache.paimon.utils.PartitionPathUtils.generatePartitionPath;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AddDonePartitionActionTest {
+
+ @Test
+ public void test() throws Exception {
+ AtomicBoolean closed = new AtomicBoolean(false);
+ Set<String> donePartitions = new HashSet<>();
+ MetastoreClient metastoreClient =
+ new MetastoreClient() {
+ @Override
+ public void addPartition(BinaryRow partition) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addPartition(LinkedHashMap<String, String>
partitionSpec) {
+
donePartitions.add(generatePartitionPath(partitionSpec));
+ }
+
+ @Override
+ public void deletePartition(LinkedHashMap<String, String>
partitionSpec) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() throws Exception {
+ closed.set(true);
+ }
+ };
+
+ AddDonePartitionAction action = new
AddDonePartitionAction(metastoreClient);
+
+ // test normal
+ action.markDone("dt=20201202");
+
assertThat(donePartitions).containsExactlyInAnyOrder("dt=20201202.done/");
+
+ // test multiple partition fields
+ action.markDone("dt=20201202/hour=02");
+ assertThat(donePartitions)
+ .containsExactlyInAnyOrder("dt=20201202.done/",
"dt=20201202/hour=02.done/");
+
+ action.close();
+ assertThat(closed).isTrue();
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneTriggerTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneTriggerTest.java
new file mode 100644
index 000000000..b3c636308
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/PartitionMarkDoneTriggerTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink.sink.partition;
+
+import org.apache.paimon.partition.PartitionTimeExtractor;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PartitionMarkDoneTriggerTest {
+
+ @Test
+ public void test() throws Exception {
+ List<String> pendingPartitions = new ArrayList<>();
+ PartitionMarkDoneTrigger.State state =
+ new PartitionMarkDoneTrigger.State() {
+ @Override
+ public List<String> restore() {
+ return new ArrayList<>(pendingPartitions);
+ }
+
+ @Override
+ public void update(List<String> partitions) {
+ pendingPartitions.clear();
+ pendingPartitions.addAll(partitions);
+ }
+ };
+
+ PartitionTimeExtractor extractor = new PartitionTimeExtractor("$dt",
"yyyy-MM-dd");
+ Duration timeInternal = Duration.ofDays(1);
+ Duration idleTime = Duration.ofMinutes(15);
+ PartitionMarkDoneTrigger trigger =
+ new PartitionMarkDoneTrigger(
+ state, extractor, timeInternal, idleTime,
toEpochMillis("2024-02-01"));
+
+ // test not reach partition end + idle time
+ trigger.notifyPartition("dt=2024-02-02", toEpochMillis("2024-02-01"));
+ List<String> partitions =
trigger.donePartitions(toEpochMillis("2024-02-03"));
+ assertThat(partitions).isEmpty();
+
+ // test state
+ assertThat(pendingPartitions).isEmpty();
+ trigger.snapshotState();
+ assertThat(pendingPartitions).containsOnly("dt=2024-02-02");
+
+ // test trigger
+ partitions = trigger.donePartitions(toEpochMillis("2024-02-03") +
idleTime.toMillis());
+ assertThat(partitions).isEmpty();
+ partitions = trigger.donePartitions(toEpochMillis("2024-02-03") +
idleTime.toMillis() + 1);
+ assertThat(partitions).containsOnly("dt=2024-02-02");
+
+ // test state
+ trigger.snapshotState();
+ assertThat(pendingPartitions).isEmpty();
+
+ // test refresh
+ trigger.notifyPartition("dt=2024-02-03", toEpochMillis("2024-02-03"));
+ trigger.notifyPartition("dt=2024-02-03", toEpochMillis("2024-02-04") +
idleTime.toMillis());
+ partitions = trigger.donePartitions(toEpochMillis("2024-02-04") +
idleTime.toMillis() + 1);
+ assertThat(partitions).isEmpty();
+ partitions =
+ trigger.donePartitions(toEpochMillis("2024-02-04") + 2 *
idleTime.toMillis() + 1);
+ assertThat(partitions).containsOnly("dt=2024-02-03");
+
+ // test restore
+ pendingPartitions.add("dt=2024-02-04");
+ trigger =
+ new PartitionMarkDoneTrigger(
+ state, extractor, timeInternal, idleTime,
toEpochMillis("2024-02-06"));
+ partitions = trigger.donePartitions(toEpochMillis("2024-02-06"));
+ assertThat(partitions).isEmpty();
+ partitions = trigger.donePartitions(toEpochMillis("2024-02-06") +
idleTime.toMillis() + 1);
+ assertThat(partitions).containsOnly("dt=2024-02-04");
+ }
+
+ private long toEpochMillis(String dt) {
+ return LocalDateTime.of(LocalDate.parse(dt), LocalTime.MIN)
+ .atZone(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli();
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/SuccessFileMarkDoneActionTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/SuccessFileMarkDoneActionTest.java
new file mode 100644
index 000000000..5a6818b25
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/partition/SuccessFileMarkDoneActionTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink.sink.partition;
+
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SuccessFileMarkDoneActionTest {
+
+ @TempDir java.nio.file.Path temp;
+
+ @Test
+ public void test() throws Exception {
+ LocalFileIO fileIO = new LocalFileIO();
+ Path path = new Path(temp.toUri());
+ SuccessFileMarkDoneAction action = new
SuccessFileMarkDoneAction(fileIO, path);
+ Path successPath = new Path(path, "dt=20240513/_SUCCESS");
+
+ action.markDone("dt=20240513");
+ SuccessFile successFile1 = SuccessFile.safelyFromPath(fileIO,
successPath);
+ assertThat(successFile1).isNotNull();
+
+ Thread.sleep(100);
+ action.markDone("dt=20240513");
+ SuccessFile successFile2 = SuccessFile.safelyFromPath(fileIO,
successPath);
+ assertThat(successFile2).isNotNull();
+
+ assertThat(successFile1.creationTime() ==
successFile2.creationTime()).isTrue();
+ assertThat(successFile1.modificationTime() <
successFile2.modificationTime()).isTrue();
+ }
+}
diff --git
a/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
b/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
index e478d44cd..5a915d200 100644
---
a/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
+++
b/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
@@ -27,14 +27,19 @@ import org.apache.paimon.hive.annotation.Minio;
import org.apache.paimon.hive.runner.PaimonEmbeddedHiveRunner;
import org.apache.paimon.privilege.NoPrivilegeException;
import org.apache.paimon.s3.MinioTestContainer;
+import org.apache.paimon.utils.IOUtils;
import com.klarna.hiverunner.HiveShell;
import com.klarna.hiverunner.annotations.HiveSQL;
+import org.apache.flink.core.fs.FSDataInputStream;
import org.apache.flink.core.fs.Path;
+import
org.apache.flink.streaming.api.environment.ExecutionCheckpointingOptions;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.TableException;
+import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.api.ValidationException;
+import org.apache.flink.table.api.config.ExecutionConfigOptions;
import org.apache.flink.table.api.internal.TableEnvironmentImpl;
import org.apache.flink.table.catalog.exceptions.DatabaseAlreadyExistException;
import org.apache.flink.table.catalog.exceptions.DatabaseNotEmptyException;
@@ -55,6 +60,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -80,6 +86,7 @@ public abstract class HiveCatalogITCaseBase {
protected String path;
protected TableEnvironment tEnv;
+ protected TableEnvironment sEnv;
private boolean locationInProperties;
@HiveSQL(files = {})
@@ -100,6 +107,10 @@ public abstract class HiveCatalogITCaseBase {
tEnv.executeSql("DROP DATABASE IF EXISTS test_db CASCADE");
tEnv.executeSql("CREATE DATABASE test_db").await();
tEnv.executeSql("USE test_db").await();
+
+ sEnv.executeSql("USE CATALOG my_hive").await();
+ sEnv.executeSql("USE test_db").await();
+
hiveShell.execute("USE test_db");
hiveShell.execute("CREATE TABLE hive_table ( a INT, b STRING )");
hiveShell.execute("INSERT INTO hive_table VALUES (100, 'Hive'), (200,
'Table')");
@@ -117,8 +128,15 @@ public abstract class HiveCatalogITCaseBase {
catalogProperties.putAll(minioTestContainer.getS3ConfigOptions());
}
- EnvironmentSettings settings =
EnvironmentSettings.newInstance().inBatchMode().build();
- tEnv = TableEnvironmentImpl.create(settings);
+ tEnv =
TableEnvironmentImpl.create(EnvironmentSettings.newInstance().inBatchMode().build());
+ sEnv =
+ TableEnvironmentImpl.create(
+
EnvironmentSettings.newInstance().inStreamingMode().build());
+ sEnv.getConfig()
+ .getConfiguration()
+ .set(ExecutionCheckpointingOptions.CHECKPOINTING_INTERVAL,
Duration.ofSeconds(1));
+
sEnv.getConfig().set(ExecutionConfigOptions.TABLE_EXEC_RESOURCE_DEFAULT_PARALLELISM,
1);
+
tEnv.executeSql(
String.join(
"\n",
@@ -132,6 +150,8 @@ public abstract class HiveCatalogITCaseBase {
.collect(Collectors.joining(",\n")),
")"))
.await();
+
+ sEnv.registerCatalog(catalogName, tEnv.getCatalog(catalogName).get());
}
private void after() {
@@ -1111,6 +1131,42 @@ public abstract class HiveCatalogITCaseBase {
assertNoPrivilege(() -> tEnv.executeSql("DROP TABLE t").await());
}
+ @Test
+ public void testMarkDone() throws Exception {
+ sEnv.executeSql(
+ "CREATE TABLE mark_done_t1 (a INT, dt STRING) WITH
('continuous.discovery-interval' = '1s')");
+ sEnv.executeSql(
+ "CREATE TABLE mark_done_t2 (a INT, dt STRING)
PARTITIONED BY (dt) WITH ("
+ + "'partition.timestamp-formatter'='yyyyMMdd',"
+ + "'partition.timestamp-pattern'='$dt',"
+ + "'partition.idle-time-to-done'='1 s',"
+ + "'partition.time-interval'='1 d',"
+ + "'metastore.partitioned-table'='true',"
+ +
"'partition.mark-done-action'='done-partition,success-file'"
+ + ")")
+ .await();
+
+ TableResult insertSql =
+ sEnv.executeSql("INSERT INTO mark_done_t2 SELECT * FROM
mark_done_t1");
+
+ tEnv.executeSql("INSERT INTO mark_done_t1 VALUES (5,
'20240501')").await();
+
+ Thread.sleep(10 * 1000);
+
+ assertThat(hiveShell.executeQuery("SHOW PARTITIONS mark_done_t2"))
+ .containsExactlyInAnyOrder("dt=20240501", "dt=20240501.done");
+
+ Path successFile = new Path(path,
"test_db.db/mark_done_t2/dt=20240501/_SUCCESS");
+ String successText;
+ try (FSDataInputStream in =
successFile.getFileSystem().open(successFile)) {
+ successText = IOUtils.readUTF8Fully(in);
+ }
+
+
assertThat(successText).contains("creationTime").contains("modificationTime");
+
+ insertSql.getJobClient().get().cancel();
+ }
+
private void assertNoPrivilege(Executable executable) {
Exception e = assertThrows(Exception.class, executable);
if (e.getCause() != null) {