This is an automated email from the ASF dual-hosted git repository.

shunping pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 75d7e2c467f Process deletes before adds when committing OrderedList 
state updates. Otherwise we delete the newly added id from the idTracker. 
(#40659)
75d7e2c467f is described below

commit 75d7e2c467ff8e074f30cb439502ee6cba1374bf
Author: Andrew Crites <[email protected]>
AuthorDate: Mon Oct 19 12:42:14 2026 -0700

    Process deletes before adds when committing OrderedList state updates. 
Otherwise we delete the newly added id from the idTracker. (#40659)
---
 .../worker/windmill/state/WindmillOrderedList.java |  28 +++---
 .../windmill/state/WindmillStateInternalsTest.java | 105 +++++++++++++++++++++
 2 files changed, 119 insertions(+), 14 deletions(-)

diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillOrderedList.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillOrderedList.java
index 03652471a04..0b362d40a66 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillOrderedList.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillOrderedList.java
@@ -233,6 +233,20 @@ public class WindmillOrderedList<T> extends 
SimpleWindmillState implements Order
         cleared = false;
       }
 
+      if (!pendingDeletes.isEmpty()) {
+        for (Range<Instant> range : pendingDeletes.asRanges()) {
+          Windmill.TagSortedListDeleteRequest.Builder deletesBuilder =
+              updatesBuilder.addDeletesBuilder();
+          deletesBuilder.setRange(
+              Windmill.SortedListRange.newBuilder()
+                  
.setStart(WindmillTimeUtils.harnessToWindmillTimestamp(range.lowerEndpoint()))
+                  
.setLimit(WindmillTimeUtils.harnessToWindmillTimestamp(range.upperEndpoint())));
+          deletesBuilder.build();
+          idTracker.remove(range);
+        }
+        pendingDeletes.clear();
+      }
+
       if (!pendingAdds.isEmpty()) {
         // TODO(reuvenlax): Once we start caching data, we should remove this 
line. We have it
         // here now
@@ -261,20 +275,6 @@ public class WindmillOrderedList<T> extends 
SimpleWindmillState implements Order
         pendingAdds.clear();
         insertBuilder.build();
       }
-
-      if (!pendingDeletes.isEmpty()) {
-        for (Range<Instant> range : pendingDeletes.asRanges()) {
-          Windmill.TagSortedListDeleteRequest.Builder deletesBuilder =
-              updatesBuilder.addDeletesBuilder();
-          deletesBuilder.setRange(
-              Windmill.SortedListRange.newBuilder()
-                  
.setStart(WindmillTimeUtils.harnessToWindmillTimestamp(range.lowerEndpoint()))
-                  
.setLimit(WindmillTimeUtils.harnessToWindmillTimestamp(range.upperEndpoint())));
-          deletesBuilder.build();
-          idTracker.remove(range);
-        }
-        pendingDeletes.clear();
-      }
     } catch (ExecutionException | InterruptedException e) {
       throw new RuntimeException(e);
     }
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java
index 87b746089f1..7a68974f412 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java
@@ -2386,6 +2386,111 @@ public class WindmillStateInternalsTest {
     assertBuildable(commitBuilder);
   }
 
+  @Test
+  public void testOrderedListDeleteRangeThenAddPersist() throws Exception {
+    SettableFuture<Map<Range<Instant>, RangeSet<Long>>> orderedListFuture = 
SettableFuture.create();
+    orderedListFuture.set(null);
+    SettableFuture<Map<Range<Instant>, RangeSet<Instant>>> deletionsFuture =
+        SettableFuture.create();
+    deletionsFuture.set(null);
+    when(mockReader.valueFuture(
+            systemKey(NAMESPACE, "orderedList" + IdTracker.IDS_AVAILABLE_STR),
+            STATE_FAMILY,
+            IdTracker.IDS_AVAILABLE_CODER))
+        .thenReturn(orderedListFuture);
+    when(mockReader.valueFuture(
+            systemKey(NAMESPACE, "orderedList" + IdTracker.DELETIONS_STR),
+            STATE_FAMILY,
+            IdTracker.SUBRANGE_DELETIONS_CODER))
+        .thenReturn(deletionsFuture);
+
+    StateTag<OrderedListState<String>> addr =
+        StateTags.orderedList("orderedList", StringUtf8Coder.of());
+    OrderedListState<String> orderedListState = underTest.state(NAMESPACE, 
addr);
+
+    // Clear a sub-range [2ms, 4ms) and then add an element at 2ms within that 
cleared range.
+    orderedListState.clearRange(Instant.ofEpochMilli(2), 
Instant.ofEpochMilli(4));
+    orderedListState.add(TimestampedValue.of("hello", 
Instant.ofEpochMilli(2)));
+
+    Windmill.WorkItemCommitRequest.Builder commitBuilder =
+        Windmill.WorkItemCommitRequest.newBuilder();
+    underTest.persist(commitBuilder);
+
+    assertEquals(1, commitBuilder.getSortedListUpdatesCount());
+    TagSortedListUpdateRequest updates = commitBuilder.getSortedListUpdates(0);
+    assertEquals(1, updates.getDeletesCount());
+    assertEquals(2000, updates.getDeletes(0).getRange().getStart());
+    assertEquals(4000, updates.getDeletes(0).getRange().getLimit());
+    assertEquals(1, updates.getInsertsCount());
+    assertEquals(1, updates.getInserts(0).getEntriesCount());
+    assertEquals("hello", 
updates.getInserts(0).getEntries(0).getValue().toStringUtf8());
+    assertEquals(2000, updates.getInserts(0).getEntries(0).getSortKey());
+    assertEquals(IdTracker.NEW_RANGE_MIN_ID, 
updates.getInserts(0).getEntries(0).getId());
+    assertBuildable(commitBuilder);
+
+    // Verify that the sub-range deletion for [2ms, 3ms) was removed when 
"hello" was added at 2ms,
+    // so clearing the rest of the 5-minute bucket [0, 2ms) and [4ms, 5m) in 
the next work item
+    // does not wipe out id tracking for the bucket.
+    resetUnderTest();
+    orderedListState = underTest.state(NAMESPACE, addr);
+    orderedListState.clearRange(Instant.EPOCH, Instant.ofEpochMilli(2));
+    orderedListState.clearRange(Instant.ofEpochMilli(4), 
Instant.ofEpochSecond(300));
+
+    commitBuilder = Windmill.WorkItemCommitRequest.newBuilder();
+    underTest.persist(commitBuilder);
+    assertBuildable(commitBuilder);
+
+    // Adding another element at 2ms in a subsequent work item should get the 
next id (1), not 0.
+    resetUnderTest();
+    orderedListState = underTest.state(NAMESPACE, addr);
+    orderedListState.add(TimestampedValue.of("world", 
Instant.ofEpochMilli(2)));
+
+    commitBuilder = Windmill.WorkItemCommitRequest.newBuilder();
+    underTest.persist(commitBuilder);
+
+    updates = commitBuilder.getSortedListUpdates(0);
+    assertEquals(1, updates.getInsertsCount());
+    assertEquals(1, updates.getInserts(0).getEntriesCount());
+    assertEquals("world", 
updates.getInserts(0).getEntries(0).getValue().toStringUtf8());
+    assertEquals(2000, updates.getInserts(0).getEntries(0).getSortKey());
+    assertEquals(IdTracker.NEW_RANGE_MIN_ID + 1, 
updates.getInserts(0).getEntries(0).getId());
+    assertBuildable(commitBuilder);
+
+    // Now clear the entire 5-minute bucket [0, 5m) and add a new element in 
the same work item.
+    // The delete should be processed before the add, resetting the bucket's 
ids to start at
+    // NEW_RANGE_MIN_ID (0) for the new element while preserving its id 
tracking for future adds.
+    resetUnderTest();
+    orderedListState = underTest.state(NAMESPACE, addr);
+    orderedListState.clearRange(Instant.EPOCH, Instant.ofEpochSecond(300));
+    orderedListState.add(TimestampedValue.of("afterFullClear1", 
Instant.ofEpochMilli(2)));
+
+    commitBuilder = Windmill.WorkItemCommitRequest.newBuilder();
+    underTest.persist(commitBuilder);
+
+    updates = commitBuilder.getSortedListUpdates(0);
+    assertEquals(1, updates.getDeletesCount());
+    assertEquals(1, updates.getInsertsCount());
+    assertEquals(1, updates.getInserts(0).getEntriesCount());
+    assertEquals("afterFullClear1", 
updates.getInserts(0).getEntries(0).getValue().toStringUtf8());
+    assertEquals(IdTracker.NEW_RANGE_MIN_ID, 
updates.getInserts(0).getEntries(0).getId());
+    assertBuildable(commitBuilder);
+
+    // A subsequent add in the same bucket should receive NEW_RANGE_MIN_ID + 1.
+    resetUnderTest();
+    orderedListState = underTest.state(NAMESPACE, addr);
+    orderedListState.add(TimestampedValue.of("afterFullClear2", 
Instant.ofEpochMilli(2)));
+
+    commitBuilder = Windmill.WorkItemCommitRequest.newBuilder();
+    underTest.persist(commitBuilder);
+
+    updates = commitBuilder.getSortedListUpdates(0);
+    assertEquals(1, updates.getInsertsCount());
+    assertEquals(1, updates.getInserts(0).getEntriesCount());
+    assertEquals("afterFullClear2", 
updates.getInserts(0).getEntries(0).getValue().toStringUtf8());
+    assertEquals(IdTracker.NEW_RANGE_MIN_ID + 1, 
updates.getInserts(0).getEntries(0).getId());
+    assertBuildable(commitBuilder);
+  }
+
   @Test
   public void testOrderedListMergePendingAdds() {
     SettableFuture<Map<Range<Instant>, RangeSet<Long>>> orderedListFuture = 
SettableFuture.create();

Reply via email to