From 0315f6098c3e3f10b3170b488d339b5f6b6a4f5e Mon Sep 17 00:00:00 2001 From: oliviarla Date: Wed, 14 Oct 2026 16:51:14 +0900 Subject: [PATCH] FIX: Skip if nodeSet return null when already removed --- .../spy/memcached/ArcusKetamaNodeLocator.java | 13 ++-- .../spy/memcached/ArcusKetamaHashingTest.java | 71 +++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/spy/memcached/ArcusKetamaNodeLocator.java b/src/main/java/net/spy/memcached/ArcusKetamaNodeLocator.java index bb876c507..39defbf16 100644 --- a/src/main/java/net/spy/memcached/ArcusKetamaNodeLocator.java +++ b/src/main/java/net/spy/memcached/ArcusKetamaNodeLocator.java @@ -315,7 +315,10 @@ private void removeHash(MemcachedNode node) { for (int h = 0; h < 4; h++) { Long k = getKetamaHashPoint(digest, h); SortedSet nodeSet = ketamaNodes.get(k); - assert nodeSet != null; + if (nodeSet == null) { + // The hash point is duplicated in the node itself and already removed. + continue; + } nodeSet.remove(node); if (nodeSet.isEmpty()) { ketamaNodes.remove(k); @@ -384,12 +387,14 @@ private void removeHashOfAlter(MemcachedNode node) { } } else { nodeSet = ketamaNodes.get(k); - assert nodeSet != null; - boolean removed = nodeSet.remove(node); + if (nodeSet == null) { + // The hash point is duplicated in the node itself and already removed. + continue; + } + nodeSet.remove(node); if (nodeSet.isEmpty()) { ketamaNodes.remove(k); } - assert removed; } } } diff --git a/src/test/java/net/spy/memcached/ArcusKetamaHashingTest.java b/src/test/java/net/spy/memcached/ArcusKetamaHashingTest.java index 0c1a64df4..375b4fee6 100644 --- a/src/test/java/net/spy/memcached/ArcusKetamaHashingTest.java +++ b/src/test/java/net/spy/memcached/ArcusKetamaHashingTest.java @@ -2,6 +2,8 @@ import java.net.InetSocketAddress; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -13,6 +15,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; class ArcusKetamaHashingTest { @@ -60,6 +63,74 @@ void testHashCollision() { runThisManyNodes(stringNode1, stringNode2); } + @Test + void testRemoveNodeWithDuplicateHashPointInSelf() { + /* "127.0.7.1:12932" has a duplicate hash point in its own hash points. */ + List nodes = createNodes( + AddrUtil.getAddresses(Arrays.asList("127.0.0.1:11211", "127.0.0.2:11211"))); + MemcachedNode dupNode = new MockMemcachedNode( + new InetSocketAddress("127.0.7.1", 12932)); + + ArcusKetamaNodeLocator locator = + new ArcusKetamaNodeLocator(nodes, new ArcusKetamaNodeLocatorConfiguration()); + locator.update(Collections.singletonList(dupNode), + Collections.emptyList()); + assertTrue(locator.getAll().contains(dupNode)); + + locator.update(Collections.emptyList(), + Collections.singletonList(dupNode)); + assertFalse(locator.getAll().contains(dupNode)); + assertNoHashPointOf(locator, dupNode); + } + + @Test + void testRemoveJoiningAlterNodeWithDuplicateHashPointInSelf() { + /* "127.0.7.1:12932" has a duplicate hash point in its own hash points. */ + List nodes = createNodes( + AddrUtil.getAddresses(Arrays.asList("127.0.0.1:11211", "127.0.0.2:11211"))); + MemcachedNode dupNode = new MockMemcachedNode( + new InetSocketAddress("127.0.7.1", 12932)); + + ArcusKetamaNodeLocator locator = + new ArcusKetamaNodeLocator(nodes, new ArcusKetamaNodeLocatorConfiguration()); + locator.prepareMigration(Collections.singletonList(dupNode), MigrationType.JOIN); + assertTrue(locator.getAlterAll().contains(dupNode)); + + locator.updateAlter(Collections.emptyList(), + Collections.singletonList(dupNode)); + assertTrue(locator.getAlterAll().isEmpty()); + assertNoHashPointOf(locator, dupNode); + } + + @Test + void testRemoveLeavingAlterNodeWithDuplicateHashPointInSelf() { + /* "127.0.7.1:12932" has a duplicate hash point in its own hash points. */ + List nodes = createNodes( + AddrUtil.getAddresses(Arrays.asList("127.0.0.1:11211", "127.0.0.2:11211"))); + MemcachedNode dupNode = new MockMemcachedNode( + new InetSocketAddress("127.0.7.1", 12932)); + + ArcusKetamaNodeLocator locator = + new ArcusKetamaNodeLocator(nodes, new ArcusKetamaNodeLocatorConfiguration()); + locator.update(Collections.singletonList(dupNode), + Collections.emptyList()); + locator.prepareMigration(Collections.singletonList(dupNode), MigrationType.LEAVE); + assertTrue(locator.getAlterAll().contains(dupNode)); + + locator.updateAlter(Collections.emptyList(), + Collections.singletonList(dupNode)); + assertTrue(locator.getAlterAll().isEmpty()); + assertNoHashPointOf(locator, dupNode); + } + + private void assertNoHashPointOf(ArcusKetamaNodeLocator locator, MemcachedNode target) { + for (SortedSet nodeSet : locator.getKetamaNodes().values()) { + for (MemcachedNode node : nodeSet) { + assertNotSame(target, node); + } + } + } + /** * Simulate dropping from stringNode1 to stringNode2. * Ensure hashing is consistent between the the two scenarios.