This is an automated email from the ASF dual-hosted git repository.
jxue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/helix.git
The following commit(s) were added to refs/heads/master by this push:
new 0da9b4245 Delete expected version (#2759)
0da9b4245 is described below
commit 0da9b42452b80db64e4698d091f21fe421086e5e
Author: Grant Paláu Spencer <[email protected]>
AuthorDate: Tue Mar 19 15:51:29 2024 -0700
Delete expected version (#2759)
Add delete with expected version API to zk client and data accessor
---
.../java/org/apache/helix/BaseDataAccessor.java | 11 +++++++
.../helix/manager/zk/ZkBaseDataAccessor.java | 21 ++++++++++++
.../helix/manager/zk/ZkCacheBaseDataAccessor.java | 25 +++++++++++++++
.../helix/store/zk/AutoFallbackPropertyStore.java | 8 +++++
.../helix/manager/zk/TestZkBaseDataAccessor.java | 37 ++++++++++++++++++++++
.../apache/helix/mock/MockBaseDataAccessor.java | 18 +++++++++++
.../zookeeper/api/client/RealmAwareZkClient.java | 2 ++
.../zookeeper/impl/client/DedicatedZkClient.java | 7 +++-
.../zookeeper/impl/client/FederatedZkClient.java | 7 +++-
.../zookeeper/impl/client/SharedZkClient.java | 7 +++-
.../helix/zookeeper/zkclient/IZkConnection.java | 2 ++
.../apache/helix/zookeeper/zkclient/ZkClient.java | 14 +++++++-
.../helix/zookeeper/zkclient/ZkConnection.java | 5 +++
.../client/RealmAwareZkClientFactoryTestBase.java | 24 ++++++++++++++
.../impl/client/TestFederatedZkClient.java | 26 ++++++++++++++-
15 files changed, 209 insertions(+), 5 deletions(-)
diff --git a/helix-core/src/main/java/org/apache/helix/BaseDataAccessor.java
b/helix-core/src/main/java/org/apache/helix/BaseDataAccessor.java
index 162ffb05d..4fafb4271 100644
--- a/helix-core/src/main/java/org/apache/helix/BaseDataAccessor.java
+++ b/helix-core/src/main/java/org/apache/helix/BaseDataAccessor.java
@@ -112,6 +112,17 @@ public interface BaseDataAccessor<T> {
*/
boolean remove(String path, int options);
+ /**
+ * This will remove the ZNode, if the ZNode's version matches the provided
expectedVersion. This
+ * operation will fail if the node has any children.
+ * @param path Path to the ZNode to update
+ * @param options Set the type of ZNode see the valid values in {@link
AccessOption}
+ * @param expectedVersion the expected version of the node to be removed, -1
means match any
+ * version
+ * @return true if the removal succeeded, false otherwise
+ */
+ boolean removeWithExpectedVersion(String path, int options, int
expectedVersion);
+
/**
* Use it when creating children under a parent node. This will use async
api for better
* performance. If the child already exists it will return false.
diff --git
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java
index e074a22e6..06ea97fb9 100644
---
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java
+++
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java
@@ -788,6 +788,27 @@ public class ZkBaseDataAccessor<T> implements
BaseDataAccessor<T> {
return true;
}
+ /**
+ * Sync remove with expected version. It tries to remove the ZNode if the
ZNode's version matches
+ * the provided expectedVersion. This operation will FAIL if the node has
any children. Node does
+ * not exist is regarded as success. If expectedVersion is set to -1, then
the ZNode version
+ * match is not enforced.
+ */
+ @Override
+ public boolean removeWithExpectedVersion(String path, int options, int
expectedVersion) {
+ try {
+ // operation will not throw exception when path successfully deleted or
does not exist
+ // despite real error, operation will throw exception when path not
empty, and in this
+ // case, we do NOT try to delete recursively
+ _zkClient.delete(path, expectedVersion);
+ } catch (ZkException zkException) {
+ LOG.error("Failed to delete {} with opts {} and expected version {}.",
path, options,
+ expectedVersion, zkException);
+ return false;
+ }
+ return true;
+ }
+
/**
* async create. give up on error other than NONODE
*/
diff --git
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
index 6a635a5da..2be1a4f76 100644
---
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
+++
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
@@ -356,6 +356,31 @@ public class ZkCacheBaseDataAccessor<T> implements
HelixPropertyStore<T> {
return _baseAccessor.remove(serverPath, options);
}
+ @Override
+ public boolean removeWithExpectedVersion(String path, int options, int
expectedVersion) {
+ String clientPath = path;
+ String serverPath = prependChroot(clientPath);
+
+ Cache<T> cache = getCache(serverPath);
+ if (cache != null) {
+ try {
+ cache.lockWrite();
+
+ boolean success = _baseAccessor.removeWithExpectedVersion(serverPath,
options, expectedVersion);
+ if (success) {
+ cache.purgeRecursive(serverPath);
+ }
+
+ return success;
+ } finally {
+ cache.unlockWrite();
+ }
+ }
+
+ // no cache
+ return _baseAccessor.removeWithExpectedVersion(serverPath, options,
expectedVersion);
+ }
+
@Override
public T get(String path, Stat stat, int options) {
String clientPath = path;
diff --git
a/helix-core/src/main/java/org/apache/helix/store/zk/AutoFallbackPropertyStore.java
b/helix-core/src/main/java/org/apache/helix/store/zk/AutoFallbackPropertyStore.java
index c23bdca90..ce82206a6 100644
---
a/helix-core/src/main/java/org/apache/helix/store/zk/AutoFallbackPropertyStore.java
+++
b/helix-core/src/main/java/org/apache/helix/store/zk/AutoFallbackPropertyStore.java
@@ -95,6 +95,14 @@ public class AutoFallbackPropertyStore<T> extends
ZkHelixPropertyStore<T> {
return super.remove(path, options);
}
+ @Override
+ public boolean removeWithExpectedVersion(String path, int options, int
expectedVersion) {
+ if (_fallbackStore != null) {
+ _fallbackStore.removeWithExpectedVersion(path, options, expectedVersion);
+ }
+ return super.removeWithExpectedVersion(path, options, expectedVersion);
+ }
+
@Override
public T get(String path, Stat stat, int options) {
if (_fallbackStore == null) {
diff --git
a/helix-core/src/test/java/org/apache/helix/manager/zk/TestZkBaseDataAccessor.java
b/helix-core/src/test/java/org/apache/helix/manager/zk/TestZkBaseDataAccessor.java
index bf4f342dd..56337f547 100644
---
a/helix-core/src/test/java/org/apache/helix/manager/zk/TestZkBaseDataAccessor.java
+++
b/helix-core/src/test/java/org/apache/helix/manager/zk/TestZkBaseDataAccessor.java
@@ -543,6 +543,43 @@ public class TestZkBaseDataAccessor extends ZkUnitTestBase
{
System.out.println("END " + testName + " at " + new
Date(System.currentTimeMillis()));
}
+ /**
+ * Test that remove with expected version will fail on version mismatch.
Succeed on version match.
+ */
+ @Test
+ public void testRemoveWithExpectedVersion() {
+ String className = TestHelper.getTestClassName();
+ String methodName = TestHelper.getTestMethodName();
+ String testName = className + "_" + methodName;
+
+ System.out.println("START " + testName + " at " + new
Date(System.currentTimeMillis()));
+
+ String path = String.format("/%s/%s", _rootPath, "msg_0");
+ ZNRecord record = new ZNRecord("msg_0");
+ ZkBaseDataAccessor<ZNRecord> accessor = new
ZkBaseDataAccessor<ZNRecord>(_gZkClient);
+
+ // Create node
+ Assert.assertTrue(accessor.create(path, record, AccessOption.PERSISTENT));
+
+ // Create child node
+ String childPath = path + "/child";
+ Assert.assertTrue(accessor.create(childPath, record,
AccessOption.PERSISTENT));
+
+ // Delete parent with correct expected version. Should fail due to having
a child
+ int currentVersion = accessor.getStat(path, 0).getVersion();
+ Assert.assertFalse(accessor.removeWithExpectedVersion(path, 0,
currentVersion));
+
+ // Remove Child
+ Assert.assertTrue(accessor.removeWithExpectedVersion(childPath, 0, -1));
+
+ // Delete childless node with wrong expected version. Should fail due to
version mismatch
+ Assert.assertFalse(accessor.removeWithExpectedVersion(path, 0,
currentVersion+100));
+
+ // Delete childless node with correct expected version. Shoudl succeed
+ Assert.assertTrue(accessor.removeWithExpectedVersion(path, 0,
currentVersion));
+
+ }
+
@Test
public void testDeleteNodeWithChildren() {
String root = _rootPath;
diff --git
a/helix-core/src/test/java/org/apache/helix/mock/MockBaseDataAccessor.java
b/helix-core/src/test/java/org/apache/helix/mock/MockBaseDataAccessor.java
index 1567b98cc..da5418d00 100644
--- a/helix-core/src/test/java/org/apache/helix/mock/MockBaseDataAccessor.java
+++ b/helix-core/src/test/java/org/apache/helix/mock/MockBaseDataAccessor.java
@@ -111,6 +111,24 @@ public class MockBaseDataAccessor implements
BaseDataAccessor<ZNRecord> {
return true;
}
+ @Override
+ public boolean removeWithExpectedVersion(String path, int options, int
expectedVersion) {
+ if (expectedVersion != -1) {
+ ZNode node = _recordMap.get(path);
+ if (node != null && node.getStat().getVersion() != expectedVersion) {
+ return false;
+ }
+ }
+
+ _recordMap.remove(path);
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return true;
+ }
+
@Override
public boolean[] createChildren(List<String> paths, List<ZNRecord> records,
int options) {
diff --git
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/api/client/RealmAwareZkClient.java
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/api/client/RealmAwareZkClient.java
index ac0240d5f..e24cc6e9c 100644
---
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/api/client/RealmAwareZkClient.java
+++
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/api/client/RealmAwareZkClient.java
@@ -254,6 +254,8 @@ public interface RealmAwareZkClient {
boolean delete(final String path);
+ boolean delete(final String path, final int expectedVersion);
+
<T extends Object> T readData(String path);
<T extends Object> T readData(String path, boolean
returnNullIfPathNotExists);
diff --git
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/DedicatedZkClient.java
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/DedicatedZkClient.java
index 8dba7d81e..a1a06f48d 100644
---
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/DedicatedZkClient.java
+++
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/DedicatedZkClient.java
@@ -385,8 +385,13 @@ public class DedicatedZkClient implements
RealmAwareZkClient {
@Override
public boolean delete(String path) {
+ return delete(path, -1);
+ }
+
+ @Override
+ public boolean delete(String path, int expectedVersion) {
checkIfPathContainsShardingKey(path);
- return _rawZkClient.delete(path);
+ return _rawZkClient.delete(path, expectedVersion);
}
@Override
diff --git
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/FederatedZkClient.java
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/FederatedZkClient.java
index 8069985e3..219ad9703 100644
---
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/FederatedZkClient.java
+++
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/FederatedZkClient.java
@@ -372,7 +372,12 @@ public class FederatedZkClient implements
RealmAwareZkClient {
@Override
public boolean delete(String path) {
- return getZkClient(path).delete(path);
+ return delete(path, -1);
+ }
+
+ @Override
+ public boolean delete(String path, int expectedVersion) {
+ return getZkClient(path).delete(path, expectedVersion);
}
@Override
diff --git
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/SharedZkClient.java
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/SharedZkClient.java
index 093538fa6..367628868 100644
---
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/SharedZkClient.java
+++
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/impl/client/SharedZkClient.java
@@ -414,8 +414,13 @@ public class SharedZkClient implements RealmAwareZkClient {
@Override
public boolean delete(String path) {
+ return delete(path, -1);
+ }
+
+ @Override
+ public boolean delete(String path, int expectedVersion) {
checkIfPathContainsShardingKey(path);
- return _innerSharedZkClient.delete(path);
+ return _innerSharedZkClient.delete(path, expectedVersion);
}
@Override
diff --git
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java
index 43eefad26..30dee9468 100644
---
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java
+++
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/IZkConnection.java
@@ -45,6 +45,8 @@ public interface IZkConnection {
public void delete(String path) throws InterruptedException,
KeeperException;
+ public void delete (String path, int expectedVersion) throws
InterruptedException, KeeperException;
+
boolean exists(final String path, final boolean watch) throws
KeeperException, InterruptedException;
List<String> getChildren(final String path, final boolean watch) throws
KeeperException, InterruptedException;
diff --git
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java
index 110587e09..9d21e7604 100644
---
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java
+++
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java
@@ -2158,6 +2158,18 @@ public class ZkClient implements Watcher {
* @return true if path is successfully deleted, false if path does not exist
*/
public boolean delete(final String path) {
+ return delete(path, -1);
+ }
+
+ /**
+ * Delete the given path. Path should not have any children or the deletion
will fail.
+ * This function will throw an exception if we fail to delete an existing
path.
+ * @param path Path of the znode to delete
+ * @param expectedVersion The expected version of the node to be removed, -1
means match any
+ * version. ZK
+ * @return true if the path is successfully deleted, false if path does not
exist
+ */
+ public boolean delete(final String path, final int expectedVersion) {
long startT = System.currentTimeMillis();
boolean success;
try {
@@ -2166,7 +2178,7 @@ public class ZkClient implements Watcher {
@Override
public Object call() throws Exception {
- getConnection().delete(path);
+ getConnection().delete(path, expectedVersion);
return null;
}
});
diff --git
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java
index 376409231..07d01df61 100644
---
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java
+++
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkConnection.java
@@ -145,6 +145,11 @@ public class ZkConnection implements IZkConnection {
_zk.delete(path, -1);
}
+ @Override
+ public void delete(String path, int expectedVersion) throws
InterruptedException, KeeperException {
+ _zk.delete(path, expectedVersion);
+ }
+
@Override
public boolean exists(String path, boolean watch) throws KeeperException,
InterruptedException {
return _zk.exists(path, watch) != null;
diff --git
a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/RealmAwareZkClientFactoryTestBase.java
b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/RealmAwareZkClientFactoryTestBase.java
index 29c2df0f8..bae31de40 100644
---
a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/RealmAwareZkClientFactoryTestBase.java
+++
b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/RealmAwareZkClientFactoryTestBase.java
@@ -26,6 +26,7 @@ import
org.apache.helix.msdcommon.exception.InvalidRoutingDataException;
import org.apache.helix.zookeeper.api.client.RealmAwareZkClient;
import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.helix.zookeeper.datamodel.serializer.ZNRecordSerializer;
+import org.apache.helix.zookeeper.zkclient.exception.ZkBadVersionException;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -238,4 +239,27 @@ public abstract class RealmAwareZkClientFactoryTestBase
extends RealmAwareZkClie
Assert.assertTrue(_realmAwareZkClient.delete(TEST_VALID_PATH));
Assert.assertFalse(_realmAwareZkClient.exists(TEST_VALID_PATH));
}
+
+ /**
+ * Test that delete() with expected version fails on version mismatch.
Succeeds on version match.
+ */
+ @Test(dependsOnMethods = "testDelete")
+ public void testDeleteExpectedVersion() {
+ // Create a ZNode for testing
+ _realmAwareZkClient.createPersistent(TEST_VALID_PATH, true);
+ Assert.assertTrue(_realmAwareZkClient.exists(TEST_VALID_PATH));
+ int expectedVersion =
_realmAwareZkClient.getStat(TEST_VALID_PATH).getVersion();
+
+ // Test delete with invalid version, should fail to delete
+ try {
+ _realmAwareZkClient.delete(TEST_VALID_PATH, expectedVersion + 100);
+ Assert.fail("Should have thrown bad version exception");
+ } catch (ZkBadVersionException expectedException) {
+ // Expected exception, continue
+ }
+
+ // Assert delete with expected version successful
+ Assert.assertTrue(_realmAwareZkClient.delete(TEST_VALID_PATH,
expectedVersion));
+ Assert.assertFalse(_realmAwareZkClient.exists(TEST_VALID_PATH));
+ }
}
diff --git
a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestFederatedZkClient.java
b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestFederatedZkClient.java
index 30335efb1..3fea6a8bf 100644
---
a/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestFederatedZkClient.java
+++
b/zookeeper-api/src/test/java/org/apache/helix/zookeeper/impl/client/TestFederatedZkClient.java
@@ -40,6 +40,7 @@ import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.helix.zookeeper.datamodel.serializer.ZNRecordSerializer;
import org.apache.helix.zookeeper.routing.RoutingDataManager;
import org.apache.helix.zookeeper.zkclient.IZkStateListener;
+import org.apache.helix.zookeeper.zkclient.exception.ZkBadVersionException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Watcher;
import org.testng.Assert;
@@ -289,10 +290,33 @@ public class TestFederatedZkClient extends
RealmAwareZkClientTestBase {
Assert.assertFalse(_realmAwareZkClient.exists(TEST_REALM_ONE_VALID_PATH));
}
+ /**
+ * Tests that delete() works on version match and fails on version mismatch
+ */
+ @Test(dependsOnMethods = "testDelete")
+ public void testDeleteExpectedVersion() {
+ // Create a ZNode for testing
+ _realmAwareZkClient.createPersistent(TEST_VALID_PATH, true);
+ Assert.assertTrue(_realmAwareZkClient.exists(TEST_VALID_PATH));
+ int expectedVersion =
_realmAwareZkClient.getStat(TEST_VALID_PATH).getVersion();
+
+ // Test delete with invalid version, should fail to delete
+ try {
+ _realmAwareZkClient.delete(TEST_VALID_PATH, expectedVersion + 100);
+ Assert.fail("Should hvae thrown bad version exception");
+ } catch (ZkBadVersionException expectedException) {
+ // Expected exception, continue
+ }
+
+ // Assert delete with expected version successful
+ Assert.assertTrue(_realmAwareZkClient.delete(TEST_VALID_PATH,
expectedVersion));
+ Assert.assertFalse(_realmAwareZkClient.exists(TEST_VALID_PATH));
+ }
+
/*
* Tests that multi-realm feature.
*/
- @Test(dependsOnMethods = "testDelete")
+ @Test(dependsOnMethods = "testDeleteExpectedVersion")
public void testMultiRealmCRUD() {
ZNRecord realmOneZnRecord = new ZNRecord("realmOne");
realmOneZnRecord.setSimpleField("realmOne", "Value");