# Qubit Progress
[](https://github.com/qubit-ltd/rs-progress/actions/workflows/ci.yml)
[](https://qubit-ltd.github.io/rs-progress/coverage/)
[](https://crates.io/crates/qubit-progress)
[](https://www.rust-lang.org)
[](LICENSE)
[](README.zh_CN.md)
`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
```toml
[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.
```rust
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](doc/user_guide.md#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.
```rust
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](doc/user_guide.md) for the lifecycle model, validation rules, scheduling, automatic reporting, reporters, and error handling. API-level details are available on [docs.rs](https://docs.rs/qubit-progress).
## Testing
```bash
# 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](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](https://github.com/qubit-ltd/rs-progress)