mxm commented on code in PR #17630:
URL: https://github.com/apache/iceberg/pull/17630#discussion_r3801130387


##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertPlanner.java:
##########
@@ -257,6 +281,18 @@ public void processElement(StreamRecord<Trigger> element) 
throws Exception {
         return;
       }
 
+      // Resolving an eq delete consumes the index entries it matches, so a 
cycle that failed after
+      // its delete phase left the index without them. The cursor only 
advances once the committer's
+      // marker is on the target branch, so picking the same staging snapshot 
again means that cycle
+      // did not commit, and the target has not moved either, so nothing else 
rebuilds the index.
+      // This also covers a restore from a checkpoint taken mid-cycle, because 
the planned snapshot
+      // is part of the checkpointed state.
+      if (!rebuilt
+          && mainSnapshot != null
+          && Objects.equals(pendingStagingSnapshotId, 
nextToProcess.snapshotId())) {
+        rebuildIndex(mainSnapshot, true);
+      }

Review Comment:
   This feels a bit complicated. Can we move this logic into 
`ensureIndexCurrent` to avoid the indirection of via the `rebuilt` flag?



##########
docs/docs/flink-maintenance.md:
##########
@@ -113,8 +113,8 @@ Notes:
 
 **State and restart**
 
-- The PK-index worker keeps a keyed row-position index of the target branch in 
Flink state and **maintains it incrementally across checkpoints**: each trigger 
cycle only applies the commits added on the target branch since the last 
indexed snapshot, and the resulting index is persisted with the next Flink 
checkpoint. On failover, the worker resumes from the most recent checkpointed 
index rather than rebuilding from scratch. Its size scales with the number of 
live rows in the table for the configured equality columns; size Flink state 
backend, checkpoint storage, and TaskManager memory accordingly. **RocksDB is 
the preferred state backend** because the PK index can grow beyond the 
available heap and RocksDB spills to local disk instead of failing with an 
`OutOfMemoryError`.
-- Full (re)indexing from the target branch head is only triggered in a few 
cases: cold start with no checkpoint, external commits that advance the target 
past the currently-indexed snapshot (see below), or when the planner detects 
that the target branch has diverged from the indexed snapshot (rollback / 
replace-main / expired marker).
+- The PK-index worker keeps a keyed row-position index of the target branch in 
Flink state and **maintains it incrementally across checkpoints**: each trigger 
cycle only applies the commits added on the target branch since the last 
indexed snapshot, and the resulting index is persisted with the next Flink 
checkpoint. On failover, the worker resumes from the most recent checkpointed 
index rather than rebuilding from scratch, unless the checkpoint was taken 
while a conversion cycle was still in flight (see below). Its size scales with 
the number of live rows in the table for the configured equality columns; size 
Flink state backend, checkpoint storage, and TaskManager memory accordingly. 
**RocksDB is the preferred state backend** because the PK index can grow beyond 
the available heap and RocksDB spills to local disk instead of failing with an 
`OutOfMemoryError`.

Review Comment:
   "unless the checkpoint was taken while a conversion cycle was still in 
flight (see below)"
   
   This isn't really possible, unless unaligned checkpointing is used. The 
reason is that we emit all commands at once from the planner. Any checkpoint 
barriers will come afterwards.



##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertPlanner.java:
##########
@@ -297,22 +335,37 @@ private void ensureIndexCurrent(Snapshot mainSnapshot) {
           bootstrap ? "Bootstrapping" : "Reindexing",
           currentMainSnapshotId,
           eqFieldIds);
-      if (reindex) {
-        // Evict keyed entries the reindex will not re-add (e.g. data file 
removed by CoW).
-        output.collect(
-            CLEAR_BROADCAST_STREAM,
-            new StreamRecord<>(
-                IndexCommand.clearBeforeReindex(
-                    currentMainSnapshotId, mainSnapshot.sequenceNumber())));
-        reindexCounter.inc();
-      }
-
-      indexSnapshotId = currentMainSnapshotId;
-      indexedSequenceNumber = mainSnapshot.sequenceNumber();
-      emitMainDataReadCommands(mainSnapshot);
+      rebuildIndex(mainSnapshot, reindex);
     }
 
     lastMainSnapshotId = currentMainSnapshotId;
+    return bootstrap || reindex;
+  }
+
+  /**
+   * Re-emits every data row on {@code mainSnapshot} so the worker's index 
holds all their positions
+   * again, optionally preceded by a CLEAR_INDEX broadcast that evicts keyed 
entries the re-emission
+   * will not re-add (e.g. a PK whose data file was removed by a CoW commit). 
A bootstrap has no
+   * earlier index and so nothing to evict.
+   *
+   * <p>The worker detects stale state by comparing the generation stamped on 
the commands it
+   * receives with the one it stored, so every rebuild hands out a higher 
generation, including a
+   * rebuild while the target branch stands still.
+   */
+  private void rebuildIndex(Snapshot mainSnapshot, boolean evictStaleKeys) {
+    long generation = indexGeneration == null ? 1 : indexGeneration + 1;

Review Comment:
   Can we remove the null check and ensure `indexGeneration` is initialized 
properly from state?



##########
flink/v2.1/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestConvertEqualityDeletes.java:
##########
@@ -1342,6 +1342,131 @@ void testReaderErrorSkipsCommit() throws Exception {
     }
   }
 
+  @Test
+  void testDeleteResolvedBeforeFailureIsRetained() throws Exception {
+    Table table = createTableWithDelete(3);
+    insert(table, 1, "a");
+    insert(table, 2, "b");
+
+    // Two eq deletes in one commit. id=1's delete file stays readable so it 
resolves, while id=2's
+    // is removed so the cycle aborts only after id=1 has been resolved out of 
the index.
+    DeleteFile readableDelete = writeEqualityDelete(table, 1, "a");
+    DeleteFile missingDelete = writeEqualityDelete(table, 2, "b");

Review Comment:
   Should we also add an insert for the "a"/"b" here in addition to the delete?



##########
docs/docs/flink-maintenance.md:
##########
@@ -113,8 +113,8 @@ Notes:
 
 **State and restart**
 
-- The PK-index worker keeps a keyed row-position index of the target branch in 
Flink state and **maintains it incrementally across checkpoints**: each trigger 
cycle only applies the commits added on the target branch since the last 
indexed snapshot, and the resulting index is persisted with the next Flink 
checkpoint. On failover, the worker resumes from the most recent checkpointed 
index rather than rebuilding from scratch. Its size scales with the number of 
live rows in the table for the configured equality columns; size Flink state 
backend, checkpoint storage, and TaskManager memory accordingly. **RocksDB is 
the preferred state backend** because the PK index can grow beyond the 
available heap and RocksDB spills to local disk instead of failing with an 
`OutOfMemoryError`.
-- Full (re)indexing from the target branch head is only triggered in a few 
cases: cold start with no checkpoint, external commits that advance the target 
past the currently-indexed snapshot (see below), or when the planner detects 
that the target branch has diverged from the indexed snapshot (rollback / 
replace-main / expired marker).
+- The PK-index worker keeps a keyed row-position index of the target branch in 
Flink state and **maintains it incrementally across checkpoints**: each trigger 
cycle only applies the commits added on the target branch since the last 
indexed snapshot, and the resulting index is persisted with the next Flink 
checkpoint. On failover, the worker resumes from the most recent checkpointed 
index rather than rebuilding from scratch, unless the checkpoint was taken 
while a conversion cycle was still in flight (see below). Its size scales with 
the number of live rows in the table for the configured equality columns; size 
Flink state backend, checkpoint storage, and TaskManager memory accordingly. 
**RocksDB is the preferred state backend** because the PK index can grow beyond 
the available heap and RocksDB spills to local disk instead of failing with an 
`OutOfMemoryError`.
+- Full (re)indexing from the target branch head is only triggered in a few 
cases: cold start with no checkpoint, external commits that advance the target 
past the currently-indexed snapshot (see below), when the planner detects that 
the target branch has diverged from the indexed snapshot (rollback / 
replace-main / expired marker), or when a staging snapshot is planned again 
because the cycle that planned it did not commit. The last case covers both a 
failed cycle and a restore from a checkpoint taken mid-cycle: resolving an 
eq-delete consumes the index entries it matched, so those entries have to be 
restored before the delete is resolved again.

Review Comment:
   Maybe remove the last sentence, not sure we need that much detail here.



##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertPKIndex.java:
##########
@@ -149,15 +150,15 @@ public void open(OpenContext context) throws Exception {
   public void processElement(IndexCommand cmd, ReadOnlyContext ctx, 
Collector<DVPosition> out)
       throws Exception {
     try {
-      Long storedSequence = mainSequenceVersion.value();
-      if (!Objects.equals(storedSequence, cmd.mainSequenceNumber())) {
+      Long storedGeneration = indexGeneration.value();
+      if (!Objects.equals(storedGeneration, cmd.indexGeneration())) {
         LOG.info(
             "Main sequence changed from {} to {} (snapshot {}), clearing 
state",

Review Comment:
   ```suggestion
               "Index generation changed from {} to {} (snapshot {}), clearing 
state",
   ```



##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/operator/EqualityConvertPlanner.java:
##########
@@ -126,7 +130,10 @@ public class EqualityConvertPlanner extends 
AbstractStreamOperator<ReadCommand>
   private transient Long lastMainSnapshotId;
   private transient Long lastStagingSnapshotId;
   private transient Long indexSnapshotId;
-  private transient Long indexedSequenceNumber;
+  private transient Long indexGeneration;
+  // Staging snapshot the last emitted plan covered, checkpointed so it 
survives a restore taken
+  // mid-cycle. Selecting it again means that cycle never committed.
+  private transient Long pendingStagingSnapshotId;

Review Comment:
   Should we add a unit test to verify behavior regarding the new field?



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to