amliuyong commented on issue #9236:
URL: https://github.com/apache/paimon/issues/9236#issuecomment-6486795099

   We hit the runtime case described above ("The same completeness invariant 
must also hold at runtime ...") in a normal run, without any failover. Adding a 
reproduction here rather than opening a new issue.
   
   **Observed** with Paimon 2.0.0 and Flink 1.20.4: a Flink job writing a 
bounded `datagen` source (3000 rows) into an append table (`bucket = -1`, 3 
partitions), streaming mode, checkpoint interval 3 s, sink parallelism 2. All 
3000 rows were written to data files, but only 1506 were committed. The job 
finished normally, with no exception or warning. The 1494 rows that one writer 
flushed at end of input are in data files that no snapshot references. 
Snapshots: identifier 1 (904 rows), then, both created when checkpoint 2 
completed, identifier 2 (2 rows) and identifier `Long.MAX_VALUE` (600 rows, 
only the other writer's end-of-input files).
   
   **Sequence.** `CommitterOperator` is the same in 2.0.0, 2.1.0 and master 
(071e9ee):
   
   1. Writer A reaches end of input and emits its committables with checkpoint 
id `Long.MAX_VALUE`.
   2. Checkpoint N: the committer's `snapshotState()` polls A's END_INPUT 
committables into state. Writer B is still running.
   3. Writer B reaches end of input after barrier N and emits its END_INPUT 
committables. EndOfData then reaches the committer, and `endInput()` sets 
`endInput = true` and returns without polling, because streaming checkpointing 
is enabled 
([L180-L193](https://github.com/apache/paimon/blob/071e9eee2bb34f66afc0f293144f993109098667/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java#L180-L193)).
   4. Only then does the committer handle `notifyCheckpointComplete(N)`, 
because its previous commit was slow. As `endInput` is true, it commits up to 
`END_INPUT_CHECKPOINT_ID` 
([L198](https://github.com/apache/paimon/blob/071e9eee2bb34f66afc0f293144f993109098667/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java#L198)),
 which creates the `Long.MAX_VALUE` snapshot with A's data only.
   5. Checkpoint N+1 polls B's committables. On its completion they go through 
`filterAndCommit` 
([L211-L219](https://github.com/apache/paimon/blob/071e9eee2bb34f66afc0f293144f993109098667/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/CommitterOperator.java#L211-L219)),
 and `FileStoreCommitImpl#filterCommitted` drops them, because this commit user 
already has a snapshot with identifier `Long.MAX_VALUE` 
([L320](https://github.com/apache/paimon/blob/071e9eee2bb34f66afc0f293144f993109098667/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java#L320)).
   
   **Harness test.** On master 071e9ee the test below fails with `expected: 
["1, 10", "2, 20", "3, 30"] but was: ["1, 10", "2, 20"]`. With #9284 applied to 
the same commit it passes, and so do the other tests in `CommitterOperatorTest` 
and `AutoTagForSavepointCommitterOperatorTest`. #9284 has no test for this 
no-failover path yet; feel free to use this one.
   
   <details>
   
<summary><code>CommitterOperatorTest#testEndInputOfWritersFinishingInDifferentCheckpoints</code></summary>
   
   ```java
       @Test
       public void testEndInputOfWritersFinishingInDifferentCheckpoints() 
throws Exception {
           FileStoreTable table =
                   createFileStoreTable(
                           options -> {
                               options.setString("bucket", "-1");
                               options.remove("bucket-key");
                           });
           OneInputStreamOperatorTestHarness<Committable, Committable> 
testHarness =
                   createRecoverableTestHarness(table);
           testHarness.open();
   
           // two writer subtasks of a bounded streaming job
           StreamWriteBuilder streamWriteBuilder =
                   
table.newStreamWriteBuilder().withCommitUser(initialCommitUser);
           StreamTableWrite write1 = streamWriteBuilder.newWrite();
           StreamTableWrite write2 = streamWriteBuilder.newWrite();
           long timestamp = 1;
   
           // writer 1 reaches end of input before checkpoint 1
           write1.write(GenericRow.of(1, 10L));
           for (CommitMessage committable : write1.prepareCommit(true, 
Long.MAX_VALUE)) {
               testHarness.processElement(new Committable(Long.MAX_VALUE, 
committable), timestamp++);
           }
           // writer 2 is still running at checkpoint 1
           write2.write(GenericRow.of(2, 20L));
           for (CommitMessage committable : write2.prepareCommit(false, 1)) {
               testHarness.processElement(new Committable(1, committable), 
timestamp++);
           }
           testHarness.snapshot(1, timestamp++);
   
           // writer 2 reaches end of input after checkpoint 1 is snapshotted, 
but before the
           // committer is notified that checkpoint 1 is complete
           write2.write(GenericRow.of(3, 30L));
           for (CommitMessage committable : write2.prepareCommit(true, 
Long.MAX_VALUE)) {
               testHarness.processElement(new Committable(Long.MAX_VALUE, 
committable), timestamp++);
           }
           testHarness.endInput();
           testHarness.notifyOfCompletedCheckpoint(1);
   
           // the final checkpoint, taken after end of input
           testHarness.snapshot(2, timestamp++);
           testHarness.notifyOfCompletedCheckpoint(2);
   
           // the end-of-input data of both writers is committed in one 
END_INPUT snapshot
           assertResults(table, "1, 10", "2, 20", "3, 30");
           Snapshot latest = table.snapshotManager().latestSnapshot();
           assertThat(latest.commitIdentifier()).isEqualTo(Long.MAX_VALUE);
           assertThat(latest.deltaRecordCount()).isEqualTo(2L);
   
           write1.close();
           write2.close();
           testHarness.close();
       }
   ```
   
   </details>
   
   We also reproduced it end to end on 2.0.0 with a Flink MiniCluster, 
`FlinkSinkBuilder` and sink parallelism 2, with `write-only` both true and 
false. We forced the timing: one source subtask finishes after its second 
checkpoint, and a commit callback sleeps in the first commit. In 20 of 20 runs, 
one writer's end-of-input data was lost, and the job still finished normally.
   
   **Not affected:** batch mode, where `endInput()` polls and commits at once; 
sink parallelism 1; unbounded input.
   
   **Workarounds** until a fix is released: run bounded writes in batch mode 
(`execution.runtime-mode = batch`), or set `sink.parallelism = 1` for streaming 
writes of bounded input.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to