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

squah-confluent pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 3054383634f KAFKA-20292 [1/N]: Prepare to split 
TargetAssignmentBuilders (#22485)
3054383634f is described below

commit 3054383634fc46cff4af9d041287a512596f4160
Author: Sean Quah <[email protected]>
AuthorDate: Thu Jun 25 16:17:54 2026 +0100

    KAFKA-20292 [1/N]: Prepare to split TargetAssignmentBuilders (#22485)
    
    To accommodate asynchronous assignments, such as those from client-side
    assignors and assignors offloaded to background threads, we want to
    split the TargetAssignmentBuilders into two: one builder for building
    the target assignment and another for building the target assignment
    records. Client-side assignors will only use the second builder.
    
    Both builders require an up-to-date view of group members at the time
    they are run. In the non-offloaded case, this is the same view. However,
    the view needs to include the unwritten member operations from the
    ongoing heartbeat request. Currently the operations are applied within
    the TargetAssignmentBuilders. To avoid duplicating the logic once the
    TargetAssignmentBuilders are split, we would like to lift it out and
    pass the TargetAssignmentBuilders the updated view of members and
    assignments.
    
    Add an OverlayMap class, to be used to provide the updated views of
    members and assignments.
    
    Reviewers: Dongnuo Lyu <[email protected]>, David Jacot
     <[email protected]>
---
 .../kafka/coordinator/group/util/OverlayMap.java   | 193 +++++++++++
 .../coordinator/group/util/OverlayMapTest.java     | 359 +++++++++++++++++++++
 2 files changed, 552 insertions(+)

diff --git 
a/group-coordinator/src/main/java/org/apache/kafka/coordinator/group/util/OverlayMap.java
 
b/group-coordinator/src/main/java/org/apache/kafka/coordinator/group/util/OverlayMap.java
new file mode 100644
index 00000000000..23bdf5ca896
--- /dev/null
+++ 
b/group-coordinator/src/main/java/org/apache/kafka/coordinator/group/util/OverlayMap.java
@@ -0,0 +1,193 @@
+/*
+ * 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.kafka.coordinator.group.util;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A map which wraps an underlying base map and accepts incremental updates
+ * that overlay on top of it. This class expects the underlying base map to
+ * be immutable.
+ *
+ * <p>Null values are not supported.
+ *
+ * @param <K> The key type.
+ * @param <V> The value type.
+ */
+public class OverlayMap<K, V> extends AbstractMap<K, V> {
+    private final Map<K, V> base;
+
+    /** Entries whose keys are not in the base. */
+    private final Map<K, V> additions = new HashMap<>();
+
+    /** Entries whose keys are in the base, with a value that supersedes the 
base value. */
+    private final Map<K, V> replacements = new HashMap<>();
+
+    /** Keys that are in the base but have been removed. */
+    private final Set<Object> removals = new HashSet<>();
+
+    private Set<Entry<K, V>> entrySet;
+
+    public OverlayMap(Map<K, V> base) {
+        this.base = Objects.requireNonNull(base);
+    }
+
+    @Override
+    public int size() {
+        return base.size() + additions.size() - removals.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return additions.isEmpty() && removals.size() == base.size();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        if (additions.containsKey(key)) return true;
+        if (replacements.containsKey(key)) return true;
+        if (removals.contains(key)) return false;
+        return base.containsKey(key);
+    }
+
+    @Override
+    public V get(Object key) {
+        if (additions.containsKey(key)) return additions.get(key);
+        if (replacements.containsKey(key)) return replacements.get(key);
+        if (removals.contains(key)) return null;
+        return base.get(key);
+    }
+
+    @Override
+    public V put(K key, V value) {
+        Objects.requireNonNull(value);
+
+        if (additions.containsKey(key)) {
+            return additions.put(key, value);
+        }
+        if (replacements.containsKey(key)) {
+            return replacements.put(key, value);
+        }
+        if (removals.remove(key)) {
+            replacements.put(key, value);
+            return null;
+        }
+        if (base.containsKey(key)) {
+            replacements.put(key, value);
+            return base.get(key);
+        }
+        additions.put(key, value);
+        return null;
+    }
+
+    @Override
+    public V remove(Object key) {
+        if (additions.containsKey(key)) {
+            return additions.remove(key);
+        }
+        if (replacements.containsKey(key)) {
+            V prev = replacements.remove(key);
+            removals.add(key);
+            return prev;
+        }
+        if (removals.contains(key)) {
+            return null;
+        }
+        if (base.containsKey(key)) {
+            removals.add(key);
+            return base.get(key);
+        }
+        return null;
+    }
+
+    @Override
+    public void clear() {
+        additions.clear();
+        replacements.clear();
+        removals.clear();
+        removals.addAll(base.keySet());
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        if (entrySet != null) return entrySet;
+
+        entrySet = new AbstractSet<>() {
+            @Override
+            public Iterator<Entry<K, V>> iterator() {
+                return new Iterator<>() {
+                    private final Iterator<Entry<K, V>> baseIterator = 
base.entrySet().iterator();
+                    private final Iterator<Entry<K, V>> additionsIterator = 
additions.entrySet().iterator();
+                    private Entry<K, V> next = null;
+
+                    @Override
+                    public boolean hasNext() {
+                        if (next != null) return true;
+                        while (baseIterator.hasNext()) {
+                            Entry<K, V> entry = baseIterator.next();
+                            if (replacements.containsKey(entry.getKey())) {
+                                next = Map.entry(entry.getKey(), 
replacements.get(entry.getKey()));
+                            } else {
+                                next = entry;
+                            }
+                            if (removals.contains(entry.getKey())) continue;
+                            return true;
+                        }
+                        if (additionsIterator.hasNext()) {
+                            next = additionsIterator.next();
+                            return true;
+                        }
+                        next = null;
+                        return false;
+                    }
+
+                    @Override
+                    public Entry<K, V> next() {
+                        if (!hasNext()) throw new NoSuchElementException();
+                        Entry<K, V> result = next;
+                        next = null;
+                        return result;
+                    }
+                };
+            }
+
+            @Override
+            public int size() {
+                return OverlayMap.this.size();
+            }
+        };
+        return entrySet;
+    }
+
+    @Override
+    public String toString() {
+        return "OverlayMap(" +
+            "base=" + base +
+            ", additions=" + additions +
+            ", replacements=" + replacements +
+            ", removals=" + removals +
+            ')';
+    }
+}
diff --git 
a/group-coordinator/src/test/java/org/apache/kafka/coordinator/group/util/OverlayMapTest.java
 
b/group-coordinator/src/test/java/org/apache/kafka/coordinator/group/util/OverlayMapTest.java
new file mode 100644
index 00000000000..b8dbcfb892a
--- /dev/null
+++ 
b/group-coordinator/src/test/java/org/apache/kafka/coordinator/group/util/OverlayMapTest.java
@@ -0,0 +1,359 @@
+/*
+ * 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.kafka.coordinator.group.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OverlayMapTest {
+
+    // Test simple additions, replacements and removals. This is enough to 
populate the three
+    // internal fields of OverlayMap for use in the rest of the tests.
+
+    @Test
+    public void testPutNewKey() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "existing", "v1"
+        ));
+
+        assertNull(map.put("new", "x"));
+
+        assertEquals("x", map.get("new"));
+    }
+
+    @Test
+    public void testPutExistingKey() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "existing", "v1"
+        ));
+
+        assertEquals("v1", map.put("existing", "v2"));
+
+        assertEquals("v2", map.get("existing"));
+    }
+
+    @Test
+    public void testRemoveExistingKey() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "existing", "v1"
+        ));
+
+        assertEquals("v1", map.remove("existing"));
+
+        assertNull(map.get("existing"));
+    }
+
+    // Now test everything else.
+
+    @Test
+    public void testSize() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        assertEquals(3, map.size());
+
+        // Additions increase the size.
+        map.put("addition", "addition-value");
+        assertEquals(4, map.size());
+
+        // Replacements do not change the size.
+        map.put("replacement", "replacement-value-2");
+        assertEquals(4, map.size());
+
+        // Removals decrease the size.
+        map.remove("removal");
+        assertEquals(3, map.size());
+    }
+
+    @Test
+    public void testIsEmptyWithEmptyBase() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of());
+        assertTrue(map.isEmpty());
+
+        // Additions make it non-empty.
+        map.put("k", "v");
+        assertFalse(map.isEmpty());
+    }
+
+    @Test
+    public void testIsEmptyWhenAllBaseEntriesRemoved() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "k1", "v1",
+            "k2", "v2"
+        ));
+
+        map.remove("k1");
+        assertFalse(map.isEmpty());
+
+        map.remove("k2");
+        assertTrue(map.isEmpty());
+    }
+
+    @Test
+    public void testContainsKey() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        map.put("addition", "addition-value");
+        map.put("replacement", "replacement-value-2");
+        map.remove("removal");
+
+        assertTrue(map.containsKey("addition"));
+        assertTrue(map.containsKey("replacement"));
+        assertFalse(map.containsKey("removal"));
+        assertTrue(map.containsKey("base"));
+        assertFalse(map.containsKey("unknown"));
+    }
+
+    @Test
+    public void testGet() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        map.put("addition", "addition-value");
+        map.put("replacement", "replacement-value-2");
+        map.remove("removal");
+
+        assertEquals("addition-value", map.get("addition"));
+        assertEquals("replacement-value-2", map.get("replacement"));
+        assertNull(map.get("removal"));
+        assertEquals("base-value", map.get("base"));
+        assertNull(map.get("unknown"));
+    }
+
+    @Test
+    public void testPut() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        map.put("addition", "addition-value");
+        map.put("replacement", "replacement-value-2");
+        map.remove("removal");
+
+        assertEquals(3, map.size());
+
+        // Replace a new value.
+        assertEquals("addition-value", map.put("addition", 
"addition-value-2"));
+        assertEquals(3, map.size());
+        assertTrue(map.containsKey("addition"));
+        assertEquals("addition-value-2", map.get("addition"));
+        assertEquals(Map.of(
+            "addition", "addition-value-2",
+            "replacement", "replacement-value-2",
+            "base", "base-value"
+        ), map);
+
+        // Replace an already-replaced value.
+        assertEquals("replacement-value-2", map.put("replacement", 
"replacement-value-3"));
+        assertEquals(3, map.size());
+        assertTrue(map.containsKey("replacement"));
+        assertEquals("replacement-value-3", map.get("replacement"));
+        assertEquals(Map.of(
+            "addition", "addition-value-2",
+            "replacement", "replacement-value-3",
+            "base", "base-value"
+        ), map);
+
+        // Replace a removed value.
+        assertNull(map.put("removal", "removal-value-2"));
+        assertEquals(4, map.size());
+        assertTrue(map.containsKey("removal"));
+        assertEquals("removal-value-2", map.get("removal"));
+        assertEquals(Map.of(
+            "addition", "addition-value-2",
+            "replacement", "replacement-value-3",
+            "removal", "removal-value-2",
+            "base", "base-value"
+        ), map);
+
+        // Put over a base value and put over an unknown value are already 
test separately in
+        // testPutExistingKey and testPutNewKey.
+    }
+
+    @Test
+    public void testRemove() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        map.put("addition", "addition-value");
+        map.put("replacement", "replacement-value-2");
+        map.remove("removal");
+
+        assertEquals(3, map.size());
+
+        // Remove a new value.
+        assertEquals("addition-value", map.remove("addition"));
+        assertEquals(2, map.size());
+        assertFalse(map.containsKey("addition"));
+        assertNull(map.get("addition"));
+        assertEquals(Map.of(
+            "replacement", "replacement-value-2",
+            "base", "base-value"
+        ), map);
+
+        // Remove a replaced value.
+        assertEquals("replacement-value-2", map.remove("replacement"));
+        assertEquals(1, map.size());
+        assertFalse(map.containsKey("replacement"));
+        assertNull(map.get("replacement"));
+        assertEquals(Map.of(
+            "base", "base-value"
+        ), map);
+
+        // Remove a removed value.
+        assertNull(map.remove("removal"));
+        assertEquals(1, map.size());
+        assertFalse(map.containsKey("removal"));
+        assertNull(map.get("removal"));
+        assertEquals(Map.of(
+            "base", "base-value"
+        ), map);
+
+        // Remove an unknown value.
+        assertNull(map.remove("unknown"));
+        assertEquals(1, map.size());
+        assertFalse(map.containsKey("unknown"));
+        assertNull(map.get("unknown"));
+        assertEquals(Map.of(
+            "base", "base-value"
+        ), map);
+
+        // Removing a base value is already tested separately in 
testRemoveExistingKey.
+    }
+
+    @Test
+    public void testClear() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        map.put("addition", "addition-value");
+        map.put("replacement", "replacement-value-2");
+        map.remove("removal");
+        map.clear();
+
+        assertEquals(0, map.size());
+        assertTrue(map.isEmpty());
+        assertFalse(map.containsKey("addition"));
+        assertFalse(map.containsKey("replacement"));
+        assertFalse(map.containsKey("base"));
+        assertNull(map.get("addition"));
+        assertNull(map.get("base"));
+    }
+
+    @Test
+    public void testEntrySet() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        map.put("addition", "addition-value");
+        map.put("replacement", "replacement-value-2");
+        map.remove("removal");
+
+        assertEquals(
+            Map.of(
+                "addition", "addition-value",
+                "replacement", "replacement-value-2",
+                "base", "base-value"
+            ),
+            new HashMap<>(map)
+        );
+    }
+
+    @Test
+    public void testEntrySetIterator() {
+        Map<String, String> base = new LinkedHashMap<>();
+        base.put("base", "base-value");
+        base.put("replacement", "replacement-value");
+        base.put("removal", "removal-value");
+
+        OverlayMap<String, String> map = new OverlayMap<>(base);
+        map.put("addition", "addition-value");
+        map.put("replacement", "replacement-value-2");
+        map.remove("removal");
+
+        Iterator<Map.Entry<String, String>> iterator = 
map.entrySet().iterator();
+
+        assertTrue(iterator.hasNext());
+        assertEquals(Map.entry("base", "base-value"), iterator.next());
+
+        assertTrue(iterator.hasNext());
+        assertEquals(Map.entry("replacement", "replacement-value-2"), 
iterator.next());
+
+        assertTrue(iterator.hasNext());
+        assertEquals(Map.entry("addition", "addition-value"), iterator.next());
+
+        assertFalse(iterator.hasNext());
+        assertThrows(NoSuchElementException.class, iterator::next);
+    }
+
+    @Test
+    public void testEntrySetIteratorOverEmptyMap() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of());
+
+        Iterator<Map.Entry<String, String>> iterator = 
map.entrySet().iterator();
+
+        assertFalse(iterator.hasNext());
+        assertThrows(NoSuchElementException.class, iterator::next);
+    }
+
+    @Test
+    public void testEntrySetSize() {
+        OverlayMap<String, String> map = new OverlayMap<>(Map.of(
+            "base", "base-value",
+            "replacement", "replacement-value",
+            "removal", "removal-value"
+        ));
+        assertEquals(3, map.entrySet().size());
+
+        // Additions increase the size.
+        map.put("addition", "addition-value");
+        assertEquals(4, map.entrySet().size());
+
+        // Replacements do not change the size.
+        map.put("replacement", "replacement-value-2");
+        assertEquals(4, map.entrySet().size());
+
+        // Removals decrease the size.
+        map.remove("removal");
+        assertEquals(3, map.entrySet().size());
+    }
+}

Reply via email to