Skip to content

Commit 1322c52

Browse files
author
liubiao.leo
committed
[flink] Auto-tag savepoints on coordinator commit path
Mirror AutoTagForSavepointCommitterOperator on the JM-side commit path: writers mark the savepoint bit on their buffered committables, the coordinator rebuilds the pending-tag set from the replayed bit and creates savepoint-<id> tags after commit (dropping them on abort).
1 parent d9f23af commit 1322c52

13 files changed

Lines changed: 1053 additions & 42 deletions

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CoordinatorCommittingRowDataStoreWriteOperator.java

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@
3333
import org.apache.flink.api.common.typeutils.TypeSerializer;
3434
import org.apache.flink.api.common.typeutils.base.array.BytePrimitiveArraySerializer;
3535
import org.apache.flink.core.io.SimpleVersionedSerializerTypeSerializerProxy;
36+
import org.apache.flink.runtime.checkpoint.CheckpointOptions;
3637
import org.apache.flink.runtime.operators.coordination.OperatorEventGateway;
38+
import org.apache.flink.runtime.state.CheckpointStreamFactory;
3739
import org.apache.flink.runtime.state.StateInitializationContext;
3840
import org.apache.flink.runtime.state.StateSnapshotContext;
41+
import org.apache.flink.streaming.api.operators.OperatorSnapshotFutures;
3942
import org.apache.flink.streaming.api.operators.StreamOperatorParameters;
4043
import org.apache.flink.streaming.api.operators.util.SimpleVersionedListState;
4144
import org.apache.flink.streaming.api.watermark.Watermark;
@@ -158,23 +161,16 @@ public void snapshotState(StateSnapshotContext context) throws Exception {
158161
pendingCommittableState.addAll(new ArrayList<>(pendingCommittables.values()));
159162
}
160163

161-
@Override
162-
public void notifyCheckpointComplete(long checkpointId) throws Exception {
163-
super.notifyCheckpointComplete(checkpointId);
164-
// operator state already persisted these; we no longer need to replay them on the next
165-
// restore
166-
pendingCommittables.headMap(checkpointId, true).clear();
167-
}
168-
169164
@Override
170165
protected void emitCommittables(boolean waitCompaction, long checkpointId) throws IOException {
166+
// Runs before the checkpoint barrier (via prepareSnapshotPreBarrier) and at end-of-input.
167+
// Produces and buffers this checkpoint's committables and forwards them downstream, but does
168+
// NOT report to the coordinator yet: at this point a savepoint is indistinguishable from a
169+
// normal checkpoint. The report is deferred to snapshotState / endInput.
171170
List<Committable> committables = prepareCommit(waitCompaction, checkpointId);
172171
CheckpointCommittables entry =
173172
new CheckpointCommittables(
174173
checkpointId, committables, currentWatermark, currentIdle);
175-
// Emit an event per (subtask, checkpoint) regardless of whether committables is empty.
176-
operatorEventGateway.sendEventToCoordinator(
177-
CommittableEvent.create(checkpointId, entry, eventSerializer));
178174
// Always buffer the per-checkpoint entry so an empty barrier — even one that has not seen
179175
// a real watermark yet — survives restore. The coordinator relies on every subtask
180176
// having an entry for the checkpoint being aligned so its watermark min stays sound.
@@ -185,6 +181,48 @@ protected void emitCommittables(boolean waitCompaction, long checkpointId) throw
185181
committables.forEach(committable -> output.collect(new StreamRecord<>(committable)));
186182
}
187183

184+
@Override
185+
public OperatorSnapshotFutures snapshotState(
186+
long checkpointId,
187+
long timestamp,
188+
CheckpointOptions checkpointOptions,
189+
CheckpointStreamFactory storageLocation)
190+
throws Exception {
191+
// emitCommittables already ran in prepareSnapshotPreBarrier and buffered this checkpoint's
192+
// entry. The savepoint intent only becomes observable here, so stamp it on the buffered
193+
// entry before reporting so both the event and the persisted state carry it.
194+
if (checkpointOptions.getCheckpointType().isSavepoint()) {
195+
pendingCommittables.computeIfPresent(
196+
checkpointId, (id, entry) -> entry.withSavepoint(true));
197+
}
198+
reportToCoordinator(checkpointId);
199+
// super drives snapshotState(StateSnapshotContext) which persists the buffered pending map.
200+
return super.snapshotState(checkpointId, timestamp, checkpointOptions, storageLocation);
201+
}
202+
203+
@Override
204+
public void endInput() throws Exception {
205+
// endInput emits the Long.MAX_VALUE entry via emitCommittables but is not followed by a
206+
// snapshotState, so report it here. End-of-input is never a savepoint.
207+
super.endInput();
208+
reportToCoordinator(Long.MAX_VALUE);
209+
}
210+
211+
@Override
212+
public void notifyCheckpointComplete(long checkpointId) throws Exception {
213+
super.notifyCheckpointComplete(checkpointId);
214+
// operator state already persisted these; we no longer need to replay them on the next
215+
// restore
216+
pendingCommittables.headMap(checkpointId, true).clear();
217+
}
218+
219+
/** Sends the buffered entry for {@code checkpointId} to the coordinator, one per checkpoint. */
220+
private void reportToCoordinator(long checkpointId) throws IOException {
221+
operatorEventGateway.sendEventToCoordinator(
222+
CommittableEvent.create(
223+
checkpointId, pendingCommittables.get(checkpointId), eventSerializer));
224+
}
225+
188226
@Override
189227
public void processWatermark(Watermark mark) throws Exception {
190228
super.processWatermark(mark);

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -400,14 +400,6 @@ static void checkCoordinatorCommitPreconditions(
400400
+ PRECOMMIT_COMPACT.key()
401401
+ " = false.");
402402

403-
// The OperatorCoordinator cannot tell a savepoint from a normal checkpoint.
404-
// TODO support savepoint auto-tag.
405-
checkArgument(
406-
!options.get(SINK_AUTO_TAG_FOR_SAVEPOINT),
407-
"Could not enable coordinator commit because "
408-
+ SINK_AUTO_TAG_FOR_SAVEPOINT.key()
409-
+ " is enabled, which is not supported yet.");
410-
411403
// TODO concurrent checkpoints are not supported yet.
412404
checkArgument(
413405
checkpointConfig.getMaxConcurrentCheckpoints() == 1,

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/RowAppendTableSink.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,17 @@ private static class CoordinatorCommittingFactory extends RowDataStoreWriteOpera
110110
public OperatorCoordinator.Provider getCoordinatorProvider(
111111
String operatorName, OperatorID operatorID) {
112112
return new CommittingWriteOperatorCoordinator.Provider(
113-
operatorID, committerFactory, streamingCheckpointEnabled, initialCommitUser);
113+
operatorID,
114+
committerFactory,
115+
streamingCheckpointEnabled,
116+
initialCommitUser,
117+
new Options(table.options())
118+
.get(FlinkConnectorOptions.SINK_AUTO_TAG_FOR_SAVEPOINT),
119+
table::snapshotManager,
120+
table::tagManager,
121+
() -> table.store().newTagDeletion(),
122+
() -> table.store().createTagCallbacks(table),
123+
table.coreOptions().tagDefaultTimeRetained());
114124
}
115125

116126
@Override

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/coordinator/CheckpointCommittables.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,34 @@ public class CheckpointCommittables {
3535
// Idle bit is frozen at barrier time together with watermark; mirrors what Flink's
3636
// StatusWatermarkValve would have observed on the writer's input at the moment of the barrier.
3737
private final boolean idle;
38+
// Whether the checkpoint that produced these committables is a Flink savepoint. Only the writer
39+
// can observe this (via CheckpointOptions); it rides along so the JM-side coordinator, whose
40+
// OperatorCoordinator API carries no savepoint indicator, can auto-tag the resulting snapshot.
41+
private final boolean savepoint;
3842

3943
public CheckpointCommittables(
40-
long checkpointId, List<Committable> committables, long watermark, boolean idle) {
44+
long checkpointId,
45+
List<Committable> committables,
46+
long watermark,
47+
boolean idle,
48+
boolean savepoint) {
4149
this.checkpointId = checkpointId;
4250
this.committables = committables;
4351
this.watermark = watermark;
4452
this.idle = idle;
53+
this.savepoint = savepoint;
54+
}
55+
56+
// Convenience for callers that are not savepoint-aware yet.
57+
public CheckpointCommittables(
58+
long checkpointId, List<Committable> committables, long watermark, boolean idle) {
59+
this(checkpointId, committables, watermark, idle, false);
4560
}
4661

4762
// Convenience for callers that only need the pre-idle-aware shape (ACTIVE writer).
4863
public CheckpointCommittables(
4964
long checkpointId, List<Committable> committables, long watermark) {
50-
this(checkpointId, committables, watermark, false);
65+
this(checkpointId, committables, watermark, false, false);
5166
}
5267

5368
public long checkpointId() {
@@ -66,6 +81,15 @@ public boolean idle() {
6681
return idle;
6782
}
6883

84+
public boolean savepoint() {
85+
return savepoint;
86+
}
87+
88+
/** Returns a copy with the savepoint bit set; all other fields preserved. */
89+
public CheckpointCommittables withSavepoint(boolean savepoint) {
90+
return new CheckpointCommittables(checkpointId, committables, watermark, idle, savepoint);
91+
}
92+
6993
public int size() {
7094
return committables.size();
7195
}
@@ -77,7 +101,7 @@ public boolean isEmpty() {
77101
@Override
78102
public String toString() {
79103
return String.format(
80-
"CheckpointCommittables{checkpointId=%d, watermark=%d, idle=%s, committables=%s}",
81-
checkpointId, watermark, idle, committables);
104+
"CheckpointCommittables{checkpointId=%d, watermark=%d, idle=%s, savepoint=%s, committables=%s}",
105+
checkpointId, watermark, idle, savepoint, committables);
82106
}
83107
}

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/coordinator/CheckpointCommittablesSerializer.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public CheckpointCommittablesSerializer(CommittableSerializer committableSeriali
4343
public int getVersion() {
4444
// v1: checkpointId + watermark + committables
4545
// v2: v1 + idle bit (appended before the committable list to keep the ordering explicit)
46-
return 2;
46+
// v3: v2 + savepoint bit (appended after idle, still before the committable list)
47+
return 3;
4748
}
4849

4950
@Override
@@ -52,6 +53,7 @@ public byte[] serialize(CheckpointCommittables value) throws IOException {
5253
out.writeLong(value.checkpointId());
5354
out.writeLong(value.watermark());
5455
out.writeBoolean(value.idle());
56+
out.writeBoolean(value.savepoint());
5557
// Nested serializer version comes before the list so the reader can pick the right decoder
5658
// before touching any list bytes — mirrors ManifestCommittableSerializer's layout.
5759
out.writeInt(committableSerializer.getVersion());
@@ -67,7 +69,7 @@ public byte[] serialize(CheckpointCommittables value) throws IOException {
6769

6870
@Override
6971
public CheckpointCommittables deserialize(int version, byte[] serialized) throws IOException {
70-
if (version != 1 && version != 2) {
72+
if (version != 1 && version != 2 && version != 3) {
7173
throw new IOException("Unknown version " + version);
7274
}
7375
DataInputDeserializer in = new DataInputDeserializer(serialized);
@@ -76,6 +78,8 @@ public CheckpointCommittables deserialize(int version, byte[] serialized) throws
7678
// v1 payloads pre-date idle tracking; default to ACTIVE (idle=false), which reproduces
7779
// the pre-idle-aware behaviour: every subtask contributes to the min unconditionally.
7880
boolean idle = version >= 2 && in.readBoolean();
81+
// v1/v2 payloads pre-date savepoint tracking; default to false (a normal checkpoint).
82+
boolean savepoint = version >= 3 && in.readBoolean();
7983
int committableVersion = in.readInt();
8084
int count = in.readInt();
8185
List<Committable> committables = new ArrayList<>(count);
@@ -85,6 +89,6 @@ public CheckpointCommittables deserialize(int version, byte[] serialized) throws
8589
in.readFully(bytes);
8690
committables.add(committableSerializer.deserialize(committableVersion, bytes));
8791
}
88-
return new CheckpointCommittables(checkpointId, committables, watermark, idle);
92+
return new CheckpointCommittables(checkpointId, committables, watermark, idle, savepoint);
8993
}
9094
}

0 commit comments

Comments
 (0)