qubit-progress 0.8.3

Generic progress reporting abstractions for Qubit Rust libraries
Documentation

Qubit Progress

Rust CI Coverage Crates.io Rust License 中文文档

qubit-progress is a lifecycle-safe progress-reporting library for one long-running operation. It sends complete, immutable events to a Reporter: each event contains the operation phase, elapsed time, stable metric metadata, and the latest dynamic counts.

Why this crate?

Progress reporting is often coupled to a terminal progress bar or scattered across a copy loop as ad-hoc counters. That makes it difficult to send the same state to logs, JSON, a UI, or telemetry; it also makes consumers reconstruct state from deltas. Threaded work adds another problem: the code that owns the operation and the code that changes the counters are usually different.

This crate separates the two kinds of state. Configure each Metric—its stable ID, display name, and optional total—once at startup. Progress then owns the dynamic counts; cloneable metric handles apply validated lifecycle transitions and every event reads one internally consistent snapshot. Use MetricDelta for one atomic batch of additive lifecycle changes, and OperationAttributes for immutable string correlation metadata shared by every event. With multiple metrics, snapshots are consistent per metric rather than a globally atomic cross-metric view while the operation is running. Its consuming terminal methods permit at most one terminal event and prevent later reports in safe Rust; finish() requires zero active work and satisfied known totals, while finish_unchecked() is available for intentionally incomplete successful outcomes. Dropping or unwinding before a terminal call can still abandon an operation.

Event delivery is at-most-once: the crate returns reporter failures to the caller and does not automatically retry them. Applications that retry at a higher level must account for a reporter that accepted an event before returning an error, which can make a retry duplicate the event; idempotent reporter sinks should use the event's operation_id and sequence when deduplicating.

Installation

[dependencies]
qubit-progress = "0.8"

Enable serde to serialize and deserialize event data, json-lines for JsonLinesReporter (which includes serde), or log for LogReporter.

Basic use: copy a directory of files

Suppose an import command copies a known list of files. The operation declares the number of files once. Before each copy it marks one file active; after success it marks that file succeeded and reports the latest counts. TextReporter writes one complete line per event to standard error, but the same Progress code works with a custom reporter.

use std::{fs, io};
use qubit_progress::{Metric, Progress, TextReporter};

fn copy_files(files: &[(&str, &str)]) -> Result<(), Box<dyn std::error::Error>> {
    let reporter = TextReporter::new(io::stderr());
    let mut progress = Progress::builder(&reporter)
        .attribute("job_id", "import-42")
        .metric(Metric::new("files", "Files").total(files.len() as u64))
        .start()?;

    let copy_result = (|| -> Result<(), Box<dyn std::error::Error>> {
        let files_metric = progress.metric("files").expect("configured metric must exist");
        for (source, destination) in files {
            files_metric.start(1)?;
            fs::copy(source, destination)?;
            files_metric.succeed(1)?;
            progress.report()?;
        }
        Ok(())
    })();

    match copy_result {
        Ok(()) => {
            progress.finish()?;
            Ok(())
        }
        Err(error) => {
            progress.fail()?;
            Err(error)
        }
    }
}

Started, every Running event, and Succeeded all carry the configured total. The application never repeats that fixed configuration, and a reporter never needs a previous event to understand the current state.

This example shows the success path. On failure or cancellation, send the matching terminal event before returning; see Close every operation.

Worker-thread use: automatically report shared copy state

For parallel or worker-driven work, let Progress own one scoped background reporter thread for the whole operation. Every worker updates a cloneable metric handle and shares the cloneable ProgressNotifier; the reporter thread coalesces notifications and delivers events. Call stop() before the terminal event: the scoped exclusive borrow prevents manual reporting or termination while the background reporter is active.

use std::{thread, time::Duration};
use qubit_progress::{Metric, Progress, TextReporter};

let reporter = TextReporter::new(std::io::stderr());
let mut progress = Progress::builder(&reporter)
    .interval(Duration::from_secs(1))
    .metric(Metric::new("files", "Files").total(2))
    .start()?;
let files_metric = progress.metric("files").expect("configured metric must exist");

thread::scope(|scope| -> Result<(), qubit_progress::AutoReporterError> {
    let auto = progress.spawn_auto_reporter(scope);

    let worker = scope.spawn(move || {
        // This worker copies two files as one batch, so each transition updates two files.
        files_metric.start(2).expect("metric update must succeed");
        // Copy both files here.
        files_metric.succeed(2).expect("metric update must succeed");
    });
    worker.join().expect("copy worker panicked");
    auto.stop()?;
    Ok(())
})?;

progress.finish()?;
# Ok::<(), Box<dyn std::error::Error>>(())

Without .interval(...), the default is Duration::ZERO: workers call notify() after changes, and repeated notifications coalesce into at most one pending report. This example sets a positive interval, so the background reporter sends periodic heartbeats instead; notify() is a no-op and workers avoid notification synchronization work. The background reporter wakes only for its timer or stop(). Relative deadlines accept even Duration::MAX; that value simply has no future automatic due deadline.

Next steps

Read the user guide for the lifecycle model, validation rules, scheduling, automatic reporting, reporters, and error handling. API-level details are available on docs.rs.

Testing

# Run tests with the default feature set
cargo test

# Run tests with all declared features
cargo test --all-features

# Project CI checks
./ci-check.sh

# Check code coverage
./coverage.sh

License

Copyright (c) 2025 - 2026. Haixing Hu. All rights reserved.

Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.

Contributing

Contributions are welcome. Please follow the Rust API guidelines, keep public API documentation and tests current, and run ./align-ci.sh to format code and ./ci-check.sh to satisfy CI requirements before submitting a pull request.

Author

Haixing Hu - Qubit Co. Ltd.

Repository: https://github.com/qubit-ltd/rs-progress