justalock-client 0.1.0

A distributed lock powered by the justalock service
Documentation
# justalock-client

[![Crates.io](https://img.shields.io/crates/v/justalock-client.svg)](https://crates.io/crates/justalock-client)
[![Documentation](https://docs.rs/justalock-client/badge.svg)](https://docs.rs/justalock-client)
[![License](https://img.shields.io/crates/l/justalock-client.svg)](LICENSE.txt)

A Rust client library for the [justalock](https://justalock.dev/) 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`:

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

## Quick Start

```rust
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:

```rust
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:

```rust
// 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:

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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:

```bash
# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Test specific scenarios
cargo test --test scenario_tests
```

## Development

### Building

```bash
cargo build
```

### Running Examples

```bash
cargo run --example basic_usage
```

### Documentation

```bash
cargo doc --open
```