Manipulate zNodes in Apache ZooKeeper

Until now we've seen how to create zNodes. But creation is not the single thing that Apache ZooKeeper does.

In this article we cover 3 other available operations: edit, removal and bulk. Each of them is presented in separated part inside which we can find quick theoretical presentation and JUnit test cases.

Edit in Apache ZooKeeper

Edit of created zNodes groups permissions and date changes. These operations can be made with setACL(String, List<ACL>, int) and setData(String, byte[], int ). As you can see, the last parameter of these methods is an integer. It represents the version of zNode which we want to change. This parameter is a little bit confusing. We could think that we are allowed to modify old versions of zNodes. However, it's not the case, according to the ZooKeeper Javadoc and one of further presented test cases. To avoid memorizing current version of given node, we can pass -1 which matches any node's versions.

The version value is incremented on every modification. It can be observed through org.apache.zookeeper.data.Stat class, storing information about given zNode. It defines 2 types of versions: one representing data changes and another one representing ACL changes. In the case of zNode having some children, the version of parent is not impacted by changes made on children nodes.

We can see now corresponding test cases:

@Test
public void should_correctly_update_znode_content() throws KeeperException, InterruptedException {
  zooKeeper.create(ZNODE_CONTENT_UPDATE, "Home directory".getBytes(),   
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  Stat initialStat = zooKeeper.exists(ZNODE_CONTENT_UPDATE, false);
  zooKeeper.setData(ZNODE_CONTENT_UPDATE, "New home directory".getBytes(), ALL_VERSIONS);

  Stat newStat = zooKeeper.exists(ZNODE_CONTENT_UPDATE, false);

  assertChanges(initialStat, newStat);
  // Children and ACL weren't changed in this test case, so they should be equal
  assertThat(initialStat.getCversion()).isEqualTo(newStat.getCversion());
  assertThat(initialStat.getAversion()).isEqualTo(newStat.getAversion());
}

@Test(expected = KeeperException.BadVersionException.class)
public void should_fail_on_updating_only_old_version() throws KeeperException, InterruptedException {
  zooKeeper.create(ZNOODE_OLD_VERSION, "1".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  Stat initialStat = zooKeeper.exists(ZNOODE_OLD_VERSION, false);
  // This update increases zNode version
  zooKeeper.setData(ZNOODE_OLD_VERSION, "3".getBytes(), ALL_VERSIONS);
  // And this one should fail since we try to update a version which is not current
  zooKeeper.setData(ZNOODE_OLD_VERSION, "2".getBytes(), initialStat.getVersion());
}

@Test
public void should_correctly_update_children_node() throws KeeperException, InterruptedException {
  // create parent
  zooKeeper.create(ZNODE_CHILDREN_UPDATE, "Parent".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  Stat initialParentStat = zooKeeper.exists(ZNODE_CHILDREN_UPDATE, false);
  // create and update child
  zooKeeper.create(ZNODE_CHILDREN_UPDATE_CHILD, "Child".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  Stat initialChildStat = zooKeeper.exists(ZNODE_CHILDREN_UPDATE_CHILD, false);
  zooKeeper.setData(ZNODE_CHILDREN_UPDATE_CHILD, "New child".getBytes(), ALL_VERSIONS);

  // get new states after changes
  Stat newParentStat = zooKeeper.exists(ZNODE_CHILDREN_UPDATE, false);
  Stat newChildStat = zooKeeper.exists(ZNODE_CHILDREN_UPDATE_CHILD, false);

  assertChanges(initialChildStat, newChildStat);
  // Parent was not changed, so the version should be still the same
  assertThat(initialParentStat.getVersion()).isEqualTo(newParentStat.getVersion());
  assertThat(initialParentStat.getDataLength()).isEqualTo(newParentStat.getDataLength());
  assertThat(initialParentStat.getMtime()).isEqualTo(newParentStat.getMtime());
  assertThat(initialParentStat.getMzxid()).isEqualTo(newParentStat.getMzxid());
  assertThat(initialParentStat.getCzxid()).isEqualTo(newParentStat.getCzxid());
  assertThat(initialParentStat.getCtime()).isEqualTo(newParentStat.getCtime());
  // Unlike in previous test, children were changed, so the children version should be incremented
  assertThat(initialParentStat.getCversion()).isEqualTo(newParentStat.getCversion());
  // Children and ACL weren't changed in this test case, so they should be equal
  assertThat(initialParentStat.getAversion()).isEqualTo(newParentStat.getAversion());
}

@Test
public void should_correctly_detect_acl_changes() throws KeeperException, InterruptedException {
  zooKeeper.create(ZNODE_ACL_UPDATE, "ACL".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  Stat initialStat = zooKeeper.exists(ZNODE_ACL_UPDATE, false);
  zooKeeper.setACL(ZNODE_ACL_UPDATE, ZooDefs.Ids.READ_ACL_UNSAFE, ALL_VERSIONS);

  Stat newStat = zooKeeper.exists(ZNODE_ACL_UPDATE, false);

  assertThat(initialStat.getVersion()).isEqualTo(newStat.getVersion());
  assertThat(initialStat.getDataLength()).isEqualTo(newStat.getDataLength());
  assertThat(initialStat.getMtime()).isEqualTo(newStat.getMtime());
  assertThat(initialStat.getMzxid()).isEqualTo(newStat.getMzxid());
  assertThat(initialStat.getCzxid()).isEqualTo(newStat.getCzxid());
  assertThat(initialStat.getCtime()).isEqualTo(newStat.getCtime());
  assertThat(initialStat.getCversion()).isEqualTo(newStat.getCversion());
  // This time ACL was modified, so it should be reflected on this test
  assertThat(initialStat.getAversion()).isNotEqualTo(newStat.getAversion());

}

private static void assertChanges(Stat oldStat, Stat newStat) {
  // According to doc, version is bumped
  assertThat(oldStat.getVersion()).isNotEqualTo(newStat.getVersion());
  // New data should be defined too
  assertThat(oldStat.getDataLength()).isNotEqualTo(newStat.getDataLength());
  // Modification stats (last time and last id) should be different
  assertThat(oldStat.getMtime()).isNotEqualTo(newStat.getMtime());
  assertThat(oldStat.getMzxid()).isNotEqualTo(newStat.getMzxid());
  // But creation date should remain the same
  assertThat(oldStat.getCzxid()).isEqualTo(newStat.getCzxid());
  assertThat(oldStat.getCtime()).isEqualTo(newStat.getCtime());
}

Removal in Apache ZooKeeper

zNodes removal is quite easy operation. It needs a single call to delete(String, int) ZooKeeper's method. Here also, the last parameter represents version to delete. As in the case of edition, we can't remove other version than the current one.

The removal is a little bit complicated when zNode has children nodes. The delete method is not recursive, so we should first delete all children and only at the end their parent. However, there is simpler solution to do it. Instead of using delete method, we can pass by ZKUtil.deleteRecursive(ZooKeeper, String) utilitary method where the second parameter represents parent directory to delete.

We can see these assumptions in following test cases:

@Test
public void should_correctly_remove_znode_by_specific_version() throws KeeperException, InterruptedException {
  zooKeeper.create(HOME_TO_DELETE, "Home directory".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  int version = zooKeeper.exists(HOME_TO_DELETE, false).getVersion();

  zooKeeper.delete(HOME_TO_DELETE, version);

  assertThat(zooKeeper.exists(HOME_TO_DELETE, false)).isNull();
}

@Test(expected = KeeperException.BadVersionException.class)
public void should_correctly_remove_znode_by_specific_version_which_is_not_current() throws KeeperException, InterruptedException {
  String path = "/home_multi_versions";
  zooKeeper.create(path, "Home directory".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  int version = zooKeeper.exists(path, false).getVersion();
  zooKeeper.setData(path, new byte[0], ALL_VERSIONS);

  zooKeeper.delete(path, version);
}

@Test(expected = KeeperException.NotEmptyException.class)
public void should_fail_on_removing_znode_with_children_when_children_are_not_deleted_first() throws KeeperException, InterruptedException {
  createTree();

  zooKeeper.delete(HOME_WITH_CHILDREN, ALL_VERSIONS);
}

@Test
public void should_correctly_remove_znode_when_children_are_removed_before() throws KeeperException, InterruptedException {
  createTree();
  zooKeeper.delete(HOME_WITH_CHILDREN_1, ALL_VERSIONS);
  zooKeeper.delete(HOME_WITH_CHILDREN_2, ALL_VERSIONS);
  zooKeeper.delete(HOME_WITH_CHILDREN, ALL_VERSIONS);

  assertThat(zooKeeper.exists(HOME_WITH_CHILDREN, false)).isNull();
  assertThat(zooKeeper.exists(HOME_WITH_CHILDREN_1, false)).isNull();
  assertThat(zooKeeper.exists(HOME_WITH_CHILDREN_2, false)).isNull();
}

@Test
public void should_delete_recursively_with_zkutils() throws KeeperException, InterruptedException {
  createTree();

  ZKUtil.deleteRecursive(zooKeeper, HOME_WITH_CHILDREN);

  assertThat(zooKeeper.exists(HOME_WITH_CHILDREN, false)).isNull();
  assertThat(zooKeeper.exists(HOME_WITH_CHILDREN_1, false)).isNull();
  assertThat(zooKeeper.exists(HOME_WITH_CHILDREN_2, false)).isNull();
}

private void createTree() throws KeeperException, InterruptedException {
  zooKeeper.create(HOME_WITH_CHILDREN, "Root".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  zooKeeper.create(HOME_WITH_CHILDREN_1, "1".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  zooKeeper.create(HOME_WITH_CHILDREN_2, "2".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}

Bulk operations in Apache ZooKeeper

ZooKeeper class has also some methods to make multiple operations in a single call. This specific method is multi(Iterable<Op>). It uses an abstraction of operation represented by org.apache.zookeeper.Op class. This class contains some of factory methods which can be used to create Op instances corresponding to executed operation: create for zNode creation, delete for removal, setData for data definition and check for zNode check operation.

Bulk operations is all-or-nothing ones, ie. either all operations succeed or all fail. Operations results are represented by classes corresponding to operations categories: CreateResult for create, SetDataResult for data definiot, DeleteResult for delete and CheckResult for check. A special kind of object is ErrorResult which represents operation failure. The results are returned by multi() method.

Below you can find two cases showing bulk operations in Apache ZooKeeper:

@Test
public void should_correctly_execute_bulk_znodes_creation() throws KeeperException, InterruptedException {
  zooKeeper.create(NODE_2, "Content".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  Op createNode1 = Op.create(NODE_1, "content".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  Op removeNode2 = Op.delete(NODE_2, ALL_VERSIONS);

   /* 
    * {@code OpResult} is only an abstract class. It contains 
    * specific implementations for creation, delete,
    * data setting or version checking operations.
    */
  List<OpResult> results = 
    zooKeeper.multi(Lists.newArrayList(createNode1, removeNode2));

  assertThat(results).hasSize(2);
  assertThat(results).extracting("type").containsOnly(ZooDefs.OpCode.create, ZooDefs.OpCode.delete);
  assertThat(results.stream().map(result -> result.getClass().getCanonicalName()).collect(Collectors.toList())).containsOnly(
    "org.apache.zookeeper.OpResult.CreateResult", "org.apache.zookeeper.OpResult.DeleteResult"
  );
  assertThat(zooKeeper.exists(NODE_1, false)).isNotNull();
  assertThat(zooKeeper.exists(NODE_2, false)).isNull();
}

@Test
public void should_execute_only_some_valid_operations() throws KeeperException, InterruptedException {
  Op createNode1 = Op.create(NODE_1, "content".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  Op createNode2 = Op.create(NODE_1, "content".getBytes(), 
    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  Op removeNode3 = Op.delete(NODE_2, ALL_VERSIONS);

  try {
    zooKeeper.multi(Lists.newArrayList(createNode1, createNode2, removeNode3));
    fail("Should fail on executing bulk operation with 1 failure");
  } catch (KeeperException.NodeExistsException exception) {
    List<OpResult> results = exception.getResults();

    assertThat(results).hasSize(3);
    assertThat(results).extracting("type").containsOnly(ZooDefs.OpCode.error);
    assertThat(results.stream().map(result -> result.getClass().getCanonicalName()) 
              .collect(Collectors.toList())).containsOnly(
      "org.apache.zookeeper.OpResult.ErrorResult"
    );
  }

  assertThat(zooKeeper.exists(NODE_1, false)).isNull();
}

Edit, removal and bulk operations are 3 topics presented in this article oriented more in practice. Its first part shows two methods useful in zNode modification. The modification can apply on stored data or associated permission. In the part about delete we saw that delete is easy for file-looking zNode. In the case of parent-directory role, it's better to use utilitary deleteRecursive method. The last part illustrated the use of bulk operations through multi() method.

If you liked it, you should read:

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!