| Crates.io | noleader |
| lib.rs | noleader |
| version | 0.1.2 |
| created_at | 2025-07-04 11:28:54.806944+00 |
| updated_at | 2025-07-04 11:28:54.806944+00 |
| description | A small leader election package using NATS keyvalue store as the distributed locking mechanism. Does not require a min / max set of nodes |
| homepage | |
| repository | https://git.front.kjuulh.io/kjuulh/noleader |
| max_upload_size | |
| id | 1737834 |
| size | 69,587 |
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.
This library is still young and the API is subject to change.
tokio_util::sync::CancellationToken so you can cancel leadership and ongoing work cleanly.do_while_leader runs your async closure as long as you hold leadership, cancelling it immediately upon relinquish.Noleader is not built for distributed consensus, or fast re-election produces. It take upwards to a minute to get reelected, state is the users responsibility to handle.
Noleader is pretty much just a distributed lock, intended for use-cases where the use wants to only have a single node scheduling work etc.
Good alternatives are:
[dependencies]
noleader = "0.1"
Then in your code:
use noleader::Leader;
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(())
}
Leader::new(bucket: &str, key: &str, client: async_nats::Client) -> Leader
Create a new election participant.create_bucket(&self) -> anyhow::Result<()>
Ensures the KV bucket exists (no-op if already created).start(&self, token: CancellationToken) -> anyhow::Result<()>
Begins the background leader-election loop; renews TTL on success or retries on failure.do_while_leader<F, Fut>(&self, f: F) -> anyhow::Result<()>
Runs your closure as long as you hold leadership; cancels immediately on loss.leader_id(&self) -> Uuid
Returns your unique candidate ID.is_leader(&self) -> Status
Returns Status::Leader or Status::Candidate, taking shutdown into account.pub enum Status {
Leader,
Candidate,
}
This crate is licensed under the same terms as the workspace (MIT or Apache-2.0). See LICENSE-MIT and LICENSE-APACHE.
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.