pub struct Rollout {
pub bucket_start: u8,
pub bucket_end: u8,
}Expand description
Half-open bucket range [bucket_start, bucket_end) over 0..100, giving this
workflow a slice of the traffic on its channel.
Compared against crate::Message::routing_bucket. The engine does not
derive the bucket: how a caller maps to one — a sticky hash of some request
identity, a per-message random draw, round-robin — is entirely the caller’s
policy and deliberately stays outside this crate.
Fields§
§bucket_start: u8Inclusive lower bound.
bucket_end: u8Exclusive upper bound. 100 means “up to and including bucket 99”.
Implementations§
Source§impl Rollout
impl Rollout
Sourcepub fn accepts(&self, bucket: u8) -> bool
pub fn accepts(&self, bucket: u8) -> bool
Whether this range serves bucket (0..=99).
[0, 100) accepts everything. An empty or inverted range
(bucket_end <= bucket_start) accepts nothing.
Sourcepub fn partition(percentages: &[u8]) -> Result<Vec<Self>, RolloutError>
pub fn partition(percentages: &[u8]) -> Result<Vec<Self>, RolloutError>
Turn an ordered percentage split into contiguous half-open ranges
covering exactly 0..100.
Entry i gets the range starting where entry i-1 ended, so the input
order is the traffic order. The percentages must sum to exactly 100:
less leaves buckets that match nothing, more pushes later entries past
the end of the bucket space where they can never match. The error names
which.
A 0 entry is allowed and yields an empty range, which accepts nothing
— the natural way to express a version that is staged but takes no
traffic.
use dataflow_rs::{Rollout, RolloutError};
let split = Rollout::partition(&[90, 10]).unwrap();
assert_eq!(split[0], Rollout { bucket_start: 0, bucket_end: 90 });
assert_eq!(split[1], Rollout { bucket_start: 90, bucket_end: 100 });
// Anything the engine can route lands in exactly one range.
for bucket in 0u8..=99 {
assert_eq!(split.iter().filter(|r| r.accepts(bucket)).count(), 1);
}
assert_eq!(Rollout::partition(&[90, 9]), Err(RolloutError::Under { total: 99 }));
assert_eq!(Rollout::partition(&[90, 11]), Err(RolloutError::Over { total: 101 }));Sourcepub fn validate_set<'a>(
rollouts: impl IntoIterator<Item = &'a Self>,
) -> Result<(), RolloutError>
pub fn validate_set<'a>( rollouts: impl IntoIterator<Item = &'a Self>, ) -> Result<(), RolloutError>
Check that a set of ranges — the versions of one logical workflow —
partitions 0..100: every bucket served, none served twice.
Both failures are silent in production otherwise. A gap blackholes a slice of traffic; an overlap makes which version answers depend on workflow ordering rather than on the rollout.
Ranges are checked individually first, so an inverted range or one reaching past bucket 100 is reported as itself rather than as whatever downstream gap it happens to produce. Coverage is then reported at the lowest affected bucket, so the diagnosis is deterministic.
use dataflow_rs::{Rollout, RolloutError};
let good = Rollout::partition(&[50, 50]).unwrap();
assert!(Rollout::validate_set(&good).is_ok());
// Order does not matter — this is a property of the set.
let reversed: Vec<_> = good.iter().rev().copied().collect();
assert!(Rollout::validate_set(&reversed).is_ok());
let gapped = [
Rollout { bucket_start: 0, bucket_end: 40 },
Rollout { bucket_start: 41, bucket_end: 100 },
];
assert_eq!(
Rollout::validate_set(&gapped),
Err(RolloutError::Gap { bucket: 40 }),
);