# qubit-budget
[](https://github.com/qubit-ltd/rs-budget/actions/workflows/ci.yml)
[](https://qubit-ltd.github.io/rs-budget/coverage/)
[](https://crates.io/crates/qubit-budget)
[](https://www.rust-lang.org)
[](LICENSE)
[](README.zh_CN.md)
`qubit-budget` helps Rust libraries and services put explicit, finite limits on
work: bytes read, nodes visited, output produced, open resources, or elapsed
time. It keeps the accounting separate from parsing and I/O, so callers can
reject oversized work with structured errors and predictable state changes.
## Installation
```toml
[dependencies]
qubit-budget = "0.5"
```
The crate has no default features. Enable an integration only when it is
needed:
```toml
[dependencies]
qubit-budget = { version = "0.5", features = ["json"] }
```
Available features: `json`, `big-integer`, `big-decimal` (which enables
`big-integer`), and `time`. The minimum supported Rust version is 1.94.
## Quick Start
Suppose one response may contain no more than eight bytes. Charge each accepted
chunk to one budget. A failed charge does not change that budget:
```rust
use qubit_budget::ResourceBudget;
let mut response = ResourceBudget::new("response-bytes", 8_u64);
response.try_consume(5).expect("the first chunk fits");
let error = response
.try_consume(4)
.expect_err("only three bytes remain");
assert_eq!(error.resource(), &"response-bytes");
assert_eq!(error.limit(), 8);
assert_eq!(error.remaining(), 3);
assert_eq!(error.requested(), 4);
assert_eq!(response.used(), 5);
```
This is the basic pattern: choose a meaningful resource name, configure a
limit at the boundary you own, and let the caller decide how to handle a typed
error.
## Choose the right primitive
| Check one value, such as nesting depth | `ResourceLimit` | No state changes | Reports the observed value and limit |
| Spend an allowance that cannot return | `ResourceBudget` | Reduces `remaining` | Budget is unchanged |
| Reuse capacity that callers return | `ResourcePool` | Acquire or release changes the pool | Pool is unchanged |
| Tie reusable capacity to an owned lifetime | `ManagedResourcePool` | Returns an RAII permit | Pool is unchanged |
`ResourceBudget` is not cloneable: copying it would duplicate a finite
allowance. `ResourcePool` is only in-memory accounting; it does not wait,
synchronize access, or enforce fairness. `ManagedResourcePool` is a cloneable,
synchronized handle whose permits return capacity on Drop; it does not wait or
enforce fairness.
## What it provides
- Atomic charges for one budget and all-or-nothing charges for a group.
- Checked conversion of native `usize` and `u64` measurements.
- Structured errors for exceeded limits, insufficient budget, conversion, and
invalid pool release.
- Reusable structure limits for depth, nodes, container sizes, and key bytes.
- Transactional string rendering: bytes are charged only after a complete,
valid UTF-8 string is produced.
- Optional limits for JSON, strings, big integers, big decimals, durations,
and clock-backed deadlines.
With `json`, an attempt distinguishes immediate I/O accounting from
transactional value accounting. Accepted input and writer output remain
charged; staged JSON value usage is published only by `commit`. The user guide
explains this boundary with a complete decode scenario.
## JSON transaction boundary
Use a transaction to stage measurements for one complete value. It publishes
them only after the surrounding operation succeeds:
```rust
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueLimits;
let mut budget = JsonValueLimits::<JsonResource, usize>::builder()
.max_nodes(8)
.max_string_bytes(16)
.build()
.budget();
let mut transaction = budget.transaction();
transaction.try_admit(JsonMeasurement::String {
depth: 1,
bytes: 5,
})?;
transaction.commit()?;
# Ok::<(), qubit_budget::MeasuredBudgetError<qubit_budget::json::JsonResource, usize>>(())
```
Raw and normalized input remain immediate charges. A transaction publishes only
staged value usage, so callers call `commit` after the complete value succeeds.
A rejected value admission poisons that transaction and prevents publication;
already accepted I/O and output remain accounted. The user guide and design
document define the complete atomicity matrix and recovery boundary.
## What it does not do
This crate does not parse JSON, perform I/O, choose application limit values,
wait for pool capacity, or define recovery policy. For JSON
parsing and Serde integration, use an adapter such as
[`qubit-json`](https://crates.io/crates/qubit-json).
## Learn More
- [User guide](doc/user_guide.md): a scenario-led explanation of JSON
accounting, errors, transactions, and troubleshooting.
- [Design document](doc/design.md): invariants, state transitions, and feature
boundaries.
- [API documentation](https://docs.rs/qubit-budget)
## 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-budget](https://github.com/qubit-ltd/rs-budget)