#zookeeper #tokio #async #api-bindings

tokio-zookeeper

Asynchronous client library for interacting with Apache ZooKeeper

6 releases

0.2.1 Feb 13, 2023
0.2.0 Feb 10, 2023
0.1.3 Dec 4, 2018
0.1.2 Oct 10, 2018
0.1.1 Jul 30, 2018

#382 in Network programming

Download history 47/week @ 2023-11-20 1/week @ 2023-11-27 1/week @ 2023-12-04 153/week @ 2023-12-11 170/week @ 2023-12-18 126/week @ 2024-01-01 92/week @ 2024-01-08 15/week @ 2024-01-15 181/week @ 2024-01-22 128/week @ 2024-01-29 98/week @ 2024-02-05 23/week @ 2024-02-12 99/week @ 2024-02-19 80/week @ 2024-02-26 115/week @ 2024-03-04

320 downloads per month
Used in 3 crates (2 directly)

MIT/Apache

135KB
2.5K SLoC

tokio-zookeeper

Crates.io Documentation Build

This crate provides a client for interacting with Apache ZooKeeper, a highly reliable distributed service for maintaining configuration information, naming, providing distributed synchronization, and providing group services.

About ZooKeeper

The ZooKeeper Overview provides a thorough introduction to ZooKeeper, but we'll repeat the most important points here. At its heart, ZooKeeper is a hierarchical key-value store (that is, keys can have "sub-keys"), which additional mechanisms that guarantee consistent operation across client and server failures. Keys in ZooKeeper look like paths (e.g., /key/subkey), and every item along a path is called a "Znode". Each Znode (including those with children) can also have associated data, which can be queried and updated like in other key-value stores. Along with its data and children, each Znode stores meta-information such as access-control lists, modification timestamps, and a version number that allows clients to avoid stepping on each other's toes when accessing values (more on that later).

Operations

ZooKeeper's API consists of the same basic operations you would expect to find in a file-system: create for creating new Znodes, delete for removing them, exists for checking if a node exists, get_data and set_data for getting and setting a node's associated data respectively, and get_children for retrieving the children of a given node (i.e., its subkeys). For all of these operations, ZooKeeper gives strong guarantees about what happens when there are multiple clients interacting with the system, or even what happens in response to system and network failures.

Ephemeral nodes

When you create a Znode, you also specify a CreateMode. Nodes that are created with CreateMode::Persistent are the nodes we have discussed thus far. They remain in the server until you delete them. Nodes that are created with CreateMode::Ephemeral on the other hand are special. These ephemeral nodes are automatically deleted by the server when the client that created them disconnects. This can be handy for implementing lease-like mechanisms, and for detecting faults. Since they are automatically deleted, and nodes with children cannot be deleted directly, ephemeral nodes are not allowed to have children.

Watches

In addition to the methods above, ZooKeeper::exists, ZooKeeper::get_data, and ZooKeeper::get_children also support setting "watches" on a node. A watch is one-time trigger that causes a WatchedEvent to be sent to the client that set the watch when the state for which the watch was set changes. For example, for a watched get_data, a one-time notification will be sent the first time the data of the target node changes following when the response to the original get_data call was processed. You should see the "Watches" entry in the Programmer's Guide for details.

Getting started

To get ZooKeeper up and running, follow the official Getting Started Guide. In most Linux environments, the procedure for getting a basic setup working is usually just to install the zookeeper package and then run systemctl start zookeeper. ZooKeeper will then be running at 127.0.0.1:2181.

This implementation

This library is analogous to the asynchronous API offered by the official Java implementation, and for most operations the Java documentation should apply to the Rust implementation. If this is not the case, it is considered a bug, and we'd love a bug report with as much relevant information as you can offer.

Note that since this implementation is asynchronous, users of the client must take care to not re-order operations in their own code. There is some discussion of this in the official documentation of the Java bindings.

For more information on ZooKeeper, see the ZooKeeper Programmer's Guide and the Confluence ZooKeeper wiki. There is also a basic tutorial (that uses the Java client) here.

Interaction with Tokio

The futures in this crate expect to be running under a tokio::runtime::Runtime. In the common case, they would be executed by .awaiting them in a context that is executed via #[tokio::main] or #[tokio::test]. You can also explicitly create a tokio::runtime::Runtime and then use Runtime::block_on or Runtime::spawn.

A somewhat silly example

use tokio_zookeeper::*;
use futures::prelude::*;

let (zk, default_watcher) = ZooKeeper::connect(&"127.0.0.1:2181".parse().unwrap())
    .await
    .unwrap();

// let's first check if /example exists. the .watch() causes us to be notified
// the next time the "exists" status of /example changes after the call.
let stat = zk.watch().exists("/example").await.unwrap();
// initially, /example does not exist
assert_eq!(stat, None);
// so let's make it!
let path = zk
    .create(
        "/example",
        &b"Hello world"[..],
        Acl::open_unsafe(),
        CreateMode::Persistent,
    )
    .await
    .unwrap();
assert_eq!(path.as_deref(), Ok("/example"));

// does it exist now?
let stat = zk.watch().exists("/example").await.unwrap();
// looks like it!
// note that the creation above also triggered our "exists" watch!
assert_eq!(stat.unwrap().data_length as usize, b"Hello world".len());

// did the data get set correctly?
let res = zk.get_data("/example").await.unwrap();
let data = b"Hello world";
let res = res.unwrap();
assert_eq!(res.0, data);
assert_eq!(res.1.data_length as usize, data.len());

// let's update the data.
let stat = zk
    .set_data("/example", Some(res.1.version), &b"Bye world"[..])
    .await
    .unwrap();
assert_eq!(stat.unwrap().data_length as usize, "Bye world".len());

// create a child of /example
let path = zk
    .create(
        "/example/more",
        &b"Hello more"[..],
        Acl::open_unsafe(),
        CreateMode::Persistent,
    )
    .await
    .unwrap();
assert_eq!(path.as_deref(), Ok("/example/more"));

// it should be visible as a child of /example
let children = zk.get_children("/example").await.unwrap();
assert_eq!(children, Some(vec!["more".to_string()]));

// it is not legal to delete a node that has children directly
let res = zk.delete("/example", None).await.unwrap();
assert_eq!(res, Err(error::Delete::NotEmpty));
// instead we must delete the children first
let res = zk.delete("/example/more", None).await.unwrap();
assert_eq!(res, Ok(()));
let res = zk.delete("/example", None).await.unwrap();
assert_eq!(res, Ok(()));
// no /example should no longer exist!
let stat = zk.exists("/example").await.unwrap();
assert_eq!(stat, None);

// now let's check that the .watch().exists we did in the very
// beginning actually triggered!
let (event, _w) = default_watcher.into_future().await;
assert_eq!(
    event,
    Some(WatchedEvent {
        event_type: WatchedEventType::NodeCreated,
        keeper_state: KeeperState::SyncConnected,
        path: String::from("/example"),
    })
);

Thank you

This crate was originally developed by Jon Gjengset (@jonhoo) as part of his long-standing series of streams, starting at this video.

Dependencies

~4–14MB
~146K SLoC