justalock-client

Crates.iojustalock-client
lib.rsjustalock-client
version0.1.0
created_at2025-08-22 01:07:41.253857+00
updated_at2025-08-22 01:07:41.253857+00
descriptionA distributed lock powered by the justalock service
homepagehttps://justalock.dev/
repositoryhttps://github.com/goakley/justalock-clients/
max_upload_size
id1805763
size90,431
Glen Oakley (goakley)

documentation

https://docs.rs/justalock-client

README

justalock-client

Crates.io Documentation License

A Rust client library for the justalock distributed lock service. This library provides distributed locking functionality to coordinate work across multiple processes or services using Rust's async/await patterns.

Features

  • 🔒 Distributed Locking: Coordinate access to shared resources across multiple processes
  • 🔄 Automatic Lock Refresh: Keeps locks alive while your work is running
  • 🎯 Cancellation Support: Uses CancellationToken for graceful shutdown when locks are lost

Installation

Add this to your Cargo.toml:

[dependencies]
justalock-client = "0.1"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }

Quick Start

use justalock_client::Lock;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a lock with a unique identifier
    let lock = Lock::builder(42u128).build()?;

    // Execute work while holding the lock
    let result = lock.locked(|cancellation_token| async move {
        // Your critical work here - only runs when you have the lock
        println!("I have the lock! Doing important work...");

        // Check for cancellation (lock lost) during long operations
        for i in 1..=5 {
            if cancellation_token.is_cancelled() {
                return format!("Work cancelled at step {}", i);
            }

            // Simulate some work
            tokio::time::sleep(Duration::from_millis(200)).await;
            println!("Completed step {}", i);
        }

        "Work completed successfully!"
    }).await?;

    println!("Result: {}", result);
    Ok(())
}

Core Concepts

Lock Builder Pattern

The library uses a builder pattern for configuring locks:

use justalock_client::Lock;

let lock = Lock::builder(lock_id)
    .client_id(b"my-service-v1".to_vec())     // Identify this client
    .lifetime_seconds(30)                    // Lock expires after half a minute
    .build()?;

Lock IDs

The library supports multiple lock ID formats based on Rust primitives:

// Numeric IDs (recommended for performance)
let locku = Lock::builder(42u128);
let locki = Lock::builder(-123i128);

// Byte arrays for custom IDs
let locka = Lock::builder([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);

Cancellation Handling

The library provides a CancellationToken that signals when the lock is lost:

let result = lock.locked(|cancellation_token| async move {
    // Long-running work with cancellation checks
    for chunk in data_chunks {
        if cancellation_token.is_cancelled() {
            return Err("Lock lost during processing");
        }

        process_chunk(chunk).await?;
    }

    Ok("All chunks processed")
}).await?;

Client ID Best Practices

use std::env;

// Use consistent client IDs for the same service instance
let client_id = format!(
    "{}-{}-{}",
    env!("CARGO_PKG_NAME"),
    env!("CARGO_PKG_VERSION"),
    hostname::get().unwrap().to_string_lossy()
).into_bytes();

let lock = Lock::builder(lock_id)
    .client_id(client_id)
    .build()?;

Performance Notes

  • Lock instances are reusable - create once and call locked() multiple times
  • Automatic connection pooling - HTTP connections are reused efficiently
  • Background refresh - Lock renewals don't block your application code
  • Minimal allocations - Designed for high-throughput scenarios

Usage Examples

Basic Distributed Lock

use justalock_client::Lock;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let lock = Lock::builder("daily-report").build()?;

    let result = lock.locked(|_token| async move {
        println!("Generating daily report...");
        generate_report().await
    }).await?;

    println!("Report generated: {}", result);
    Ok(())
}

async fn generate_report() -> String {
    // Only one instance will generate the report
    "Report completed"
}

Service Coordination

use justalock_client::Lock;
use std::time::Duration;

// Service A - Database migration
async fn run_migration() -> Result<(), Box<dyn std::error::Error>> {
    let lock = Lock::builder("db-migration-v2")
        .client_id(b"migration-service".to_vec())
        .lifetime_seconds(1800) // 30 minutes
        .build()?;

    lock.locked(|token| async move {
        println!("Starting database migration...");

        let steps = ["backup", "schema_update", "data_migration", "verification"];
        for (i, step) in steps.iter().enumerate() {
            if token.is_cancelled() {
                return Err(format!("Migration cancelled at step: {}", step));
            }

            println!("Step {}: {}", i + 1, step);
            tokio::time::sleep(Duration::from_secs(30)).await; // Simulate work
        }

        println!("Migration completed successfully!");
        Ok(())
    }).await?
}

// Service B - Cache warming (waits for migration)
async fn warm_cache() -> Result<(), Box<dyn std::error::Error>> {
    let lock = Lock::builder("db-migration-v2") // Same lock ID
        .client_id(b"cache-service".to_vec())
        .lifetime_seconds(300)
        .build()?;

    lock.locked(|_token| async move {
        println!("Migration complete, warming cache...");
        // Cache warming logic
        Ok(())
    }).await?
}

Periodic Task Coordination

use justalock_client::Lock;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let lock = Lock::builder("hourly-cleanup")
        .client_id(format!("cleanup-{}", std::process::id()).as_bytes().to_vec())
        .lifetime_seconds(3600) // 1 hour max
        .build()?;

    loop {
        // Try to acquire the lock for cleanup
        match lock.clone().locked(|token| async move {
            println!("Starting cleanup...");

            // Cleanup tasks
            let tasks = ["temp_files", "old_logs", "expired_cache"];
            for task in tasks {
                if token.is_cancelled() {
                    return format!("Cleanup cancelled at: {}", task);
                }

                cleanup_task(task).await;
            }

            "Cleanup completed"
        }).await {
            Ok(result) => println!("Cleanup result: {}", result),
            Err(e) => println!("Cleanup failed: {:?}", e),
        }

        // Wait before next cleanup attempt
        tokio::time::sleep(Duration::from_secs(3600)).await;
    }
}

async fn cleanup_task(task: &str) {
    println!("Cleaning up: {}", task);
    tokio::time::sleep(Duration::from_millis(500)).await;
}

Error Handling

use justalock_client::{Error, Lock};

async fn robust_operation() -> Result<String, Box<dyn std::error::Error>> {
    let lock = Lock::builder("critical-operation").build()?;

    match lock.locked(|token| async move {
        // Your critical operation
        perform_critical_work(token).await
    }).await {
        Ok(result) => Ok(result),
        Err(Error::Data(msg)) => {
            eprintln!("Client error: {}", msg);
            Err("Configuration error".into())
        },
        Err(Error::Reqwest(e)) => {
            eprintln!("Network error: {}", e);
            Err("Network unavailable".into())
        },
    }
}

async fn perform_critical_work(
    token: tokio_util::sync::CancellationToken
) -> Result<String, &'static str> {
    for i in 1..=10 {
        if token.is_cancelled() {
            return Err("Operation cancelled");
        }

        // Simulate work
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    }

    Ok("Operation completed".to_string())
}

Testing

The library includes comprehensive tests with mocked API calls.

Run the test suite:

# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Test specific scenarios
cargo test --test scenario_tests

Development

Building

cargo build

Running Examples

cargo run --example basic_usage

Documentation

cargo doc --open
Commit count: 0

cargo fmt