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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/generated/flink_connector_configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@
<td>Boolean</td>
<td>Allow sink committer and writer operator to be chained together</td>
</tr>
<tr>
<td><h5>sink.coordinator-commit.enabled</h5></td>
<td style="word-wrap: break-word;">false</td>
<td>Boolean</td>
<td>If true, run the Paimon committer inside the Flink JobManager via an OperatorCoordinator. This decouples commit from any single TaskManager subtask so that region failover does not have to restart the whole pipeline. Only supports unaware-bucket append tables in streaming mode with checkpointing enabled; unsupported configurations fail during sink planning.</td>
</tr>
<tr>
<td><h5>sink.cross-partition.managed-memory</h5></td>
<td style="word-wrap: break-word;">256 mb</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,17 @@ public class FlinkConnectorOptions {
"Commit listener will be called after a successful commit. This option list custom commit "
+ "listener identifiers separated by comma.");

public static final ConfigOption<Boolean> SINK_COORDINATOR_COMMIT_ENABLED =
key("sink.coordinator-commit.enabled")
.booleanType()
.defaultValue(false)
.withDescription(
"If true, run the Paimon committer inside the Flink JobManager via an "
+ "OperatorCoordinator. This decouples commit from any single TaskManager "
+ "subtask so that region failover does not have to restart the whole pipeline. "
+ "Only supports unaware-bucket append tables in streaming mode with "
+ "checkpointing enabled; unsupported configurations fail during sink planning.");

public static final ConfigOption<Boolean> SINK_WRITER_COORDINATOR_ENABLED =
key("sink.writer-coordinator.enabled")
.booleanType()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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;

import org.apache.paimon.annotation.VisibleForTesting;
import org.apache.paimon.flink.sink.coordinator.CheckpointCommittables;
import org.apache.paimon.flink.sink.coordinator.CheckpointCommittablesSerializer;
import org.apache.paimon.flink.sink.coordinator.CommittableEvent;
import org.apache.paimon.flink.sink.coordinator.CommittingWriteOperatorCoordinator;
import org.apache.paimon.flink.sink.coordinator.RestoredCommittableEvent;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.sink.CommitMessageSerializer;
import org.apache.paimon.utils.Preconditions;

import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.api.common.typeutils.base.array.BytePrimitiveArraySerializer;
import org.apache.flink.core.io.SimpleVersionedSerializerTypeSerializerProxy;
import org.apache.flink.runtime.operators.coordination.OperatorEventGateway;
import org.apache.flink.runtime.state.StateInitializationContext;
import org.apache.flink.runtime.state.StateSnapshotContext;
import org.apache.flink.streaming.api.operators.StreamOperatorParameters;
import org.apache.flink.streaming.api.operators.util.SimpleVersionedListState;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.NavigableMap;
import java.util.TreeMap;

/**
* Write operator that hands committables to a JM-side {@link CommittingWriteOperatorCoordinator}
* which performs the commit.
*
* <p>This operator is stateful: it keeps an independent operator state that buffers the
* per-checkpoint committables which have not yet been acknowledged by the coordinator, so they
* survive a global failover and can be replayed on restore.
*/
public class CoordinatorCommittingRowDataStoreWriteOperator
extends StatelessRowDataStoreWriteOperator {

private static final long serialVersionUID = 1L;

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

@VisibleForTesting
static final String PENDING_COMMITTABLE_STATE_NAME = "pending_committable_state";

private final OperatorEventGateway operatorEventGateway;

/** Persisted buffer of pending checkpoints not yet acknowledged by the coordinator. */
private transient ListState<CheckpointCommittables> pendingCommittableState;

/** In-memory view of {@link #pendingCommittableState}, keyed by checkpoint id. */
private transient NavigableMap<Long, CheckpointCommittables> pendingCommittables;

/** Latest watermark observed on the input; forwarded on subsequent events. */
private transient long currentWatermark;

private transient CheckpointCommittablesSerializer stateSerializer;
private transient TypeSerializer<CheckpointCommittables> eventSerializer;

public CoordinatorCommittingRowDataStoreWriteOperator(
StreamOperatorParameters<Committable> parameters,
FileStoreTable table,
StoreSinkWrite.Provider storeSinkWriteProvider,
String initialCommitUser,
OperatorEventGateway operatorEventGateway) {
super(parameters, table, storeSinkWriteProvider, initialCommitUser);
this.operatorEventGateway = Preconditions.checkNotNull(operatorEventGateway);
}

@Override
public void initializeState(StateInitializationContext context) throws Exception {
super.initializeState(context);
stateSerializer =
new CheckpointCommittablesSerializer(
new CommittableSerializer(new CommitMessageSerializer()));
// Wire the same versioned serializer as a TypeSerializer for the event channel, so the
// payload version is embedded in the wire format instead of being carried as a separate
// event field.
eventSerializer =
new SimpleVersionedSerializerTypeSerializerProxy<>(
() ->
new CheckpointCommittablesSerializer(
new CommittableSerializer(new CommitMessageSerializer())));
pendingCommittableState =
new SimpleVersionedListState<>(
context.getOperatorStateStore()
.getListState(
new ListStateDescriptor<>(
PENDING_COMMITTABLE_STATE_NAME,
BytePrimitiveArraySerializer.INSTANCE)),
stateSerializer);
pendingCommittables = new TreeMap<>();
currentWatermark = Long.MIN_VALUE;

if (context.isRestored()) {
Preconditions.checkState(context.getRestoredCheckpointId().isPresent());
long restoredCheckpointId = context.getRestoredCheckpointId().getAsLong();

List<CheckpointCommittables> restored = new ArrayList<>();
for (CheckpointCommittables entry : pendingCommittableState.get()) {
restored.add(entry);
}
pendingCommittableState.clear();

LOG.info(
"Restore pending committables {} of checkpoint {}",
restored,
restoredCheckpointId);

// Contract: send exactly one RestoredCommittableEvent per subtask, even when the
// buffer is empty, so the coordinator can drive its own restore alignment.
operatorEventGateway.sendEventToCoordinator(
RestoredCommittableEvent.create(
restoredCheckpointId, restored, eventSerializer));
}
}

@Override
public void snapshotState(StateSnapshotContext context) throws Exception {
super.snapshotState(context);
pendingCommittableState.clear();
pendingCommittableState.addAll(new ArrayList<>(pendingCommittables.values()));
}

@Override
public void notifyCheckpointComplete(long checkpointId) throws Exception {
super.notifyCheckpointComplete(checkpointId);
// operator state already persisted these; we no longer need to replay them on the next
// restore
pendingCommittables.headMap(checkpointId, true).clear();
}

@Override
protected void emitCommittables(boolean waitCompaction, long checkpointId) throws IOException {
List<Committable> committables = prepareCommit(waitCompaction, checkpointId);
CheckpointCommittables entry =
new CheckpointCommittables(checkpointId, committables, currentWatermark);
// Emit an event per (subtask, checkpoint) regardless of whether committables is empty.
operatorEventGateway.sendEventToCoordinator(
CommittableEvent.create(checkpointId, entry, eventSerializer));
// Always buffer the per-checkpoint entry so an empty barrier — even one that has not seen
// a real watermark yet — survives restore. The coordinator relies on every subtask
// having an entry for the checkpoint being aligned so its watermark min stays sound.
pendingCommittables.put(checkpointId, entry);
// Still forward committables downstream even though the commit happens on the coordinator.
// The downstream is a DiscardingSink, but emitting keeps numRecordsOut observable and
// preserves the operator's IO metrics.
committables.forEach(committable -> output.collect(new StreamRecord<>(committable)));
}

@Override
public void processWatermark(Watermark mark) throws Exception {
super.processWatermark(mark);
// Skip Long.MAX_VALUE watermarks (batch or bounded stream end markers).
if (mark.getTimestamp() != Long.MAX_VALUE) {
currentWatermark = mark.getTimestamp();
}
}

@VisibleForTesting
NavigableMap<Long, CheckpointCommittables> getPendingCommittables() {
return pendingCommittables;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
package org.apache.paimon.flink.sink;

import org.apache.paimon.CoreOptions.TagCreationMode;
import org.apache.paimon.annotation.VisibleForTesting;
import org.apache.paimon.flink.compact.changelog.ChangelogCompactCoordinateOperator;
import org.apache.paimon.flink.compact.changelog.ChangelogCompactSortOperator;
import org.apache.paimon.flink.compact.changelog.ChangelogCompactWorkerOperator;
import org.apache.paimon.flink.compact.changelog.ChangelogTaskTypeInfo;
import org.apache.paimon.manifest.ManifestCommittable;
import org.apache.paimon.options.MemorySize;
import org.apache.paimon.options.Options;
import org.apache.paimon.table.BucketMode;
import org.apache.paimon.table.FileStoreTable;

import org.apache.flink.api.common.RuntimeExecutionMode;
Expand All @@ -50,6 +52,7 @@
import java.util.Queue;
import java.util.Set;

import static org.apache.paimon.CoreOptions.WRITE_ONLY;
import static org.apache.paimon.CoreOptions.createCommitUser;
import static org.apache.paimon.flink.FlinkConnectorOptions.END_INPUT_WATERMARK;
import static org.apache.paimon.flink.FlinkConnectorOptions.PRECOMMIT_COMPACT;
Expand Down Expand Up @@ -187,15 +190,39 @@ public DataStream<Committable> doWrite(

public DataStreamSink<?> doCommit(DataStream<Committable> written, String commitUser) {
StreamExecutionEnvironment env = written.getExecutionEnvironment();
ReadableConfig conf = env.getConfiguration();
CheckpointConfig checkpointConfig = env.getCheckpointConfig();
boolean streamingCheckpointEnabled =
isStreaming(written) && checkpointConfig.isCheckpointingEnabled();
if (streamingCheckpointEnabled) {
assertStreamingConfiguration(env);
}

if (coordinatorCommitEnabled()) {
return doCoordinatorCommit(written, checkpointConfig, streamingCheckpointEnabled);
}
return doOperatorCommit(written, commitUser, streamingCheckpointEnabled);
}

private DataStreamSink<?> doCoordinatorCommit(
DataStream<Committable> written,
CheckpointConfig checkpointConfig,
boolean streamingCheckpointEnabled) {
checkCoordinatorCommitPreconditions(table, checkpointConfig, streamingCheckpointEnabled);
// The commit runs inside the writer's OperatorCoordinator on the JobManager, so there
// is no global committer operator. Committables are still forwarded by the writer for
// observability and are discarded here.
return written.sinkTo(new PaimonDiscardingSink<>(table))
.name("end")
.setParallelism(written.getParallelism());
}

private DataStreamSink<?> doOperatorCommit(
DataStream<Committable> written,
String commitUser,
boolean streamingCheckpointEnabled) {
ReadableConfig conf = written.getExecutionEnvironment().getConfiguration();
Options options = Options.fromMap(table.options());

OneInputStreamOperatorFactory<Committable, Committable> committerOperator =
createCommitterOperatorFactory(
streamingCheckpointEnabled, commitUser, options.get(END_INPUT_WATERMARK));
Expand Down Expand Up @@ -310,6 +337,74 @@ protected abstract OneInputStreamOperatorFactory<T, Committable> createWriteOper

protected abstract CommittableStateManager<ManifestCommittable> createCommittableStateManager();

/**
* Whether the writer commits through its {@link
* org.apache.flink.runtime.operators.coordination.OperatorCoordinator} on the JobManager. When
* {@code true}, {@link #doCommit} skips the global committer operator. Sinks that wire a
* coordinated writer factory override this.
*/
protected boolean coordinatorCommitEnabled() {
return false;
}

/**
* Fails fast when coordinator commit is enabled together with a table property or runtime
* setting it cannot honor yet. Enabling coordinator commit is an explicit user choice, so a
* conflicting configuration is reported instead of silently falling back to the global
* committer.
*/
@VisibleForTesting
static void checkCoordinatorCommitPreconditions(
FileStoreTable table,
CheckpointConfig checkpointConfig,
boolean streamingCheckpointEnabled) {
Options options = Options.fromMap(table.options());

// Region failover only benefits streaming jobs. A batch job persists its shuffle data and
// has no region-failover concern, so coordinator commit brings no benefit and batch is not
// supported. The commit is driven by checkpoint completion, so checkpointing must be on.
checkArgument(
streamingCheckpointEnabled,
"Could not enable coordinator commit because it requires streaming mode with checkpointing enabled.");

// The checks below reject configurations that introduce an all-to-all shuffle. Such a
// shuffle places every operator into a single failover region, which defeats the region
// failover that coordinator commit is meant to enable.
checkArgument(
table.primaryKeys().isEmpty(),
"Could not enable coordinator commit because only append tables are supported, but the table has primary keys.");
checkArgument(
table.bucketMode() == BucketMode.BUCKET_UNAWARE,
"Could not enable coordinator commit because bucket mode is "
+ table.bucketMode()
+ ", only unaware bucket is supported.");
checkArgument(
table.coreOptions().writeOnly(),
"Could not enable coordinator commit because it requires "
+ WRITE_ONLY.key()
+ " = true.");
checkArgument(
!options.get(PRECOMMIT_COMPACT),
"Could not enable coordinator commit because it requires "
+ PRECOMMIT_COMPACT.key()
+ " = false.");

// The OperatorCoordinator cannot tell a savepoint from a normal checkpoint.
// TODO support savepoint auto-tag.
checkArgument(
!options.get(SINK_AUTO_TAG_FOR_SAVEPOINT),
"Could not enable coordinator commit because "
+ SINK_AUTO_TAG_FOR_SAVEPOINT.key()
+ " is enabled, which is not supported yet.");

// TODO concurrent checkpoints are not supported yet.
checkArgument(
checkpointConfig.getMaxConcurrentCheckpoints() == 1,
"Could not enable coordinator commit because execution.checkpointing.max-concurrent-checkpoints is "
+ checkpointConfig.getMaxConcurrentCheckpoints()
+ ", which is not supported yet.");
}

public static boolean isStreaming(DataStream<?> input) {
return isStreaming(input.getExecutionEnvironment());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void close() throws Exception {
}
}

private void emitCommittables(boolean waitCompaction, long checkpointId) throws IOException {
protected void emitCommittables(boolean waitCompaction, long checkpointId) throws IOException {
prepareCommit(waitCompaction, checkpointId)
.forEach(committable -> output.collect(new StreamRecord<>(committable)));
}
Expand Down
Loading
Loading