Skip to main content

krabka_units/
lib.rs

1//! Dimensioned quantities for Krabka.
2//!
3//! A broker is full of numbers that mean nothing on their own. `max_bytes`,
4//! `session_timeout_ms`, and `throttle_bytes_per_sec` are all integers, so a
5//! function that takes two of them accepts them transposed and still compiles.
6//! A value that crosses a module boundary in the wrong unit is a runtime bug and
7//! not a compile error: seconds where milliseconds were expected, or bits where
8//! bytes were expected. A unit written into the name shows the intent. It does
9//! not enforce it.
10//!
11//! This crate gives those magnitudes real types, built on
12//! [`uom`](https://docs.rs/uom): [`ByteSize`], [`ByteRate`], [`Time`],
13//! [`Frequency`], and [`Ratio`]. Each one is a `uom` quantity, so unit
14//! conversion is a method call and not a hand-written multiplication. `uom` also
15//! checks arithmetic across dimensions:
16//!
17//! ```
18//! use krabka_units::prelude::*;
19//!
20//! let batch = mebibytes(8);
21//! let window = secs(2);
22//! let rate: ByteRate = (batch / window).into();
23//! assert_eq!(rate.bytes_per_sec_i64(), 4 * 1024 * 1024);
24//! ```
25//!
26//! # Magnitudes, not instants
27//!
28//! These types model *extents*: how many bytes, how long, how fast. They do not
29//! model *points*. A log offset, a leader epoch, or an epoch-milliseconds
30//! timestamp is an identifier or a coordinate and not a dimensioned magnitude, so
31//! it stays a newtype in `krabka-ids`. An absolute nanosecond timestamp also
32//! cannot round-trip through `f64` seconds, which is a second reason why instants
33//! do not belong here. The next section gives the detail.
34//!
35//! # Storage type
36//!
37//! Every quantity stores `f64` in SI base units: bytes, seconds, bytes/second,
38//! and hertz. One storage type across all five dimensions is what makes
39//! cross-dimension arithmetic usable, because `uom` only combines quantities that
40//! share it. `f64` is also the only choice wide enough for both ends of the range
41//! the broker needs. `uom`'s integer quantities store base units, so they cannot
42//! represent a sub-second duration at all.
43//!
44//! `f64` represents every integer below 2^53 exactly, which covers byte counts up
45//! to 8 PiB and durations up to 285 years of milliseconds, so conversions back to
46//! wire integers are exact over the whole range Kafka can express. Absolute
47//! nanosecond timestamps (~1.8 × 10^18) are the exception, and are excluded above.
48//!
49//! # Wire boundary
50//!
51//! The generated Kafka codec (`krabka-protocol`'s `generated` module) stays raw
52//! integers, because it must be byte-exact. Convert a value when it enters or
53//! leaves the hand-written domain layer. Use the extension traits in [`convert`]:
54//!
55//! ```
56//! use krabka_units::prelude::*;
57//!
58//! // Decoding a request: raw `int32` milliseconds in, `Time` out.
59//! let timeout = Time::from_millis(30_000);
60//! assert_eq!(timeout, secs(30));
61//!
62//! // Encoding a response: `Time` in, raw `int32` milliseconds out.
63//! assert_eq!(timeout.millis_i32(), 30_000);
64//! ```
65//!
66//! # Configuration
67//!
68//! [`parse`] reads the human forms operators write (`512MiB`, `30s`, `10MiB/s`)
69//! and [`fmt`] writes them back. [`serde_units`] wires both into
70//! `#[serde(with = ...)]`, so a config struct holds quantities and not integers.
71//!
72//! See the code style guide's "Newtypes for Domain Values" section for the
73//! sibling rule covering identifiers.
74
75use core::marker::PhantomData;
76
77use uom::si::f64 as si;
78
79pub mod convert;
80pub mod fmt;
81pub mod parse;
82pub mod serde_units;
83
84pub use uom;
85
86/// A count of bytes: a message size, a segment size, a buffer capacity, a quota
87/// balance.
88///
89/// This is `uom`'s information dimension with `f64` storage. The base unit is
90/// the byte, so [`ByteSizeExt::bytes_u64`](convert::ByteSizeExt::bytes_u64) is
91/// exact for every value the wire can carry. Construct with [`bytes`],
92/// [`kibibytes`], [`mebibytes`], or [`gibibytes`].
93pub type ByteSize = si::Information;
94
95/// A byte throughput: a producer quota, a replication rate, a measured
96/// send/receive rate.
97///
98/// Divide a [`ByteSize`] by a [`Time`] to get one. The `uom` product of two
99/// quantities erases the information *kind*, so the result needs `.into()`.
100pub type ByteRate = si::InformationRate;
101
102/// An extent of time: a timeout, a retention window, a backoff, a measured
103/// latency.
104///
105/// This is not an instant. See the crate docs. Construct with [`nanos`], [`micros`],
106/// [`millis`], [`secs`], [`minutes`], [`hours`], or [`days`], or convert from
107/// [`core::time::Duration`] with
108/// [`TimeExt::from_std`](convert::TimeExt::from_std).
109pub type Time = si::Time;
110
111/// An event rate: records per second, requests per second, polls per second.
112///
113/// Distinct from [`ByteRate`], which carries the information dimension. The
114/// reciprocal of a [`Time`] period.
115pub type Frequency = si::Frequency;
116
117/// A dimensionless fraction: a fill factor, a sampling probability, a percentage.
118///
119/// Multiplying a [`ByteSize`] by one of these yields a [`ByteSize`], so scaling a
120/// buffer by a fraction keeps its dimension.
121pub type Ratio = si::Ratio;
122
123/// Everything needed to name, build, and unwrap a quantity.
124///
125/// Glob-import this in modules that work with quantities:
126/// `use krabka_units::prelude::*;`.
127pub mod prelude {
128    pub use crate::{
129        ByteRate, ByteSize, Frequency, Ratio, Time, byte_rate, bytes, bytes_per_sec,
130        convert::{ByteRateExt, ByteSizeExt, FrequencyExt, RatioExt, StdDurationExt, TimeExt},
131        days, fraction, gibibytes, gibibytes_per_sec, hours, kibibytes, kibibytes_per_sec,
132        mebibytes, mebibytes_per_sec, micros, millis, minutes, nanos, per_sec, percent, secs,
133    };
134}
135
136/// Defines a `const fn` constructor that scales an integral magnitude into a
137/// quantity's base unit.
138///
139/// `u32` inputs keep the widening cast lossless and cover every magnitude worth
140/// writing as a literal. Use [`convert`]'s `from_*` constructors for values that
141/// need the full 64-bit range.
142macro_rules! scaled_ctor {
143    ($(#[$meta:meta])* $name:ident -> $quantity:ident, $numerator:expr, $denominator:expr) => {
144        $(#[$meta])*
145        #[must_use]
146        pub const fn $name(n: u32) -> $quantity {
147            $quantity {
148                dimension: PhantomData,
149                units: PhantomData,
150                value: n as f64 * $numerator / $denominator,
151            }
152        }
153    };
154}
155
156scaled_ctor!(
157    /// `n` bytes.
158    bytes -> ByteSize, 1.0, 1.0
159);
160scaled_ctor!(
161    /// `n` kibibytes (1 KiB = 1024 B).
162    kibibytes -> ByteSize, 1024.0, 1.0
163);
164scaled_ctor!(
165    /// `n` mebibytes (1 MiB = 1024 KiB).
166    mebibytes -> ByteSize, 1_048_576.0, 1.0
167);
168scaled_ctor!(
169    /// `n` gibibytes (1 GiB = 1024 MiB).
170    gibibytes -> ByteSize, 1_073_741_824.0, 1.0
171);
172
173scaled_ctor!(
174    /// `n` nanoseconds.
175    nanos -> Time, 1.0, 1e9
176);
177scaled_ctor!(
178    /// `n` microseconds.
179    micros -> Time, 1.0, 1e6
180);
181scaled_ctor!(
182    /// `n` milliseconds.
183    millis -> Time, 1.0, 1e3
184);
185scaled_ctor!(
186    /// `n` seconds.
187    secs -> Time, 1.0, 1.0
188);
189scaled_ctor!(
190    /// `n` minutes.
191    minutes -> Time, 60.0, 1.0
192);
193scaled_ctor!(
194    /// `n` hours.
195    hours -> Time, 3600.0, 1.0
196);
197scaled_ctor!(
198    /// `n` days.
199    days -> Time, 86_400.0, 1.0
200);
201
202scaled_ctor!(
203    /// `n` bytes per second.
204    bytes_per_sec -> ByteRate, 1.0, 1.0
205);
206scaled_ctor!(
207    /// `n` kibibytes per second.
208    kibibytes_per_sec -> ByteRate, 1024.0, 1.0
209);
210scaled_ctor!(
211    /// `n` mebibytes per second.
212    mebibytes_per_sec -> ByteRate, 1_048_576.0, 1.0
213);
214scaled_ctor!(
215    /// `n` gibibytes per second.
216    gibibytes_per_sec -> ByteRate, 1_073_741_824.0, 1.0
217);
218
219scaled_ctor!(
220    /// `n` events per second.
221    per_sec -> Frequency, 1.0, 1.0
222);
223scaled_ctor!(
224    /// `n` percent, as a dimensionless [`Ratio`] (`percent(25) == fraction(0.25)`).
225    percent -> Ratio, 1.0, 100.0
226);
227
228/// A byte rate given as a byte count over a time extent, without an intermediate
229/// division at the call site.
230///
231/// `byte_rate(mebibytes(8), secs(2))` reads as "8 MiB every 2 s".
232#[must_use]
233pub fn byte_rate(size: ByteSize, over: Time) -> ByteRate {
234    (size / over).into()
235}
236
237/// A dimensionless fraction, where `1.0` is the whole.
238#[must_use]
239pub const fn fraction(value: f64) -> Ratio {
240    Ratio {
241        dimension: PhantomData,
242        units: PhantomData,
243        value,
244    }
245}