noleader/README.md
kjuulh c334dba445
All checks were successful
continuous-integration/drone/push Build is passing
feat: do publish
2025-07-06 20:56:42 +02:00

3.5 KiB
Raw Permalink Blame History

noleader

A small, ergonomic Rust crate for leader election using NATS KeyValue (KV) as the distributed locking mechanism. Does not require a fixed set of nodes—any number of candidates can join or drop out dynamically.

Disclaimer

This library is still young and the API is subject to change.

Features

  • Distributed locking with TTL Uses NATS JetStream KV store to create a key with a TTL, ensuring only one leader at a time.
  • Automatic renewal with jitter Renews leadership every ~10 s (plus a random back-off) to avoid thunderingherd elections.
  • Graceful shutdown Integrates with tokio_util::sync::CancellationToken so you can cancel leadership and ongoing work cleanly.
  • Leader-only work loop do_while_leader runs your async closure as long as you hold leadership, cancelling it immediately upon relinquish.
  • No required cluster size Nodes can join or leave at any time—no minimum/maximum constraints.

Intended use-case

Noleader is not built for distributed consensus, or fast re-election procedures. It take upwards to a minute to get re-elected, state is the users responsibility to handle.

Noleader is pretty much just a distributed lock, intended for use-cases where the user wants to only have a single node scheduling work etc.

Good alternatives are:

  • Raft (for distributed consensus)
  • Relational databases (for locking)

Installation

[dependencies]
noleader = "0.1"

Then in your code:

use noleader::Leader;

Quick Example

use tokio_util::sync::CancellationToken;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Set up logging
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::from_default_env()
                .add_directive("noleader=debug".parse().unwrap())
                .add_directive("info".parse().unwrap()),
        )
        .init();

    let bucket = "my_bucket";
    let key    = "election_key";
    let client = async_nats::connect("localhost:4222").await?;

    // Create a new leader election instance
    let leader = Leader::new(bucket, key, client.clone());

    // Ensure the KV bucket exists
    leader.create_bucket().await?;

    // Attempts to acquire election loop, will call inner function if it wins, if it loses it will retry over again.
    // Will block until either the inner function returns and error, or the election processes crashes, intended to allow the application to properly restart
    leader
        .acquire_and_run({
            move |token| {
                let leader_id = leader_id.clone();

                async move {
                    loop {
                        if token.is_cancelled() {
                            return Ok(());
                        }

                        tracing::info!(leader_id, "do work as leader");
                        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
                    }
                }
            }
        })
        .await?;


    Ok(())
}

Examples

See the examples folder in ./crates/noleader/examples

Architecture

Noleader uses a simple election stealing

License

This crate is licensed under the same terms as the workspace (MIT or Apache-2.0). See LICENSE-MIT and LICENSE-APACHE.

Contribute

Issues and PRs are welcome! Repository: https://git.front.kjuulh.io/kjuulh/noleader Development happens publicly on the main branch—feel free to fork and send a merge request.