From 2905d45ee71a470a9ed7867951221c3952a2dd06 Mon Sep 17 00:00:00 2001 From: "Geoffrey M. Oxberry" Date: Wed, 20 May 2026 15:51:27 -0700 Subject: [PATCH] feat(payload): adopt Probability for TimestampConfig::probability Change the public `TimestampConfig::probability` field from `f32` to the `Probability` alias of `BoundedProbability<{ f32::to_bits(0.0) }>`. The `try_from` impl enforces the finite + `[0.0, 1.0]` invariant at deserialize time, so the matching check in `TimestampConfig::valid()` is removed. The new type threads through `MemberGenerator::new` and `MetricGenerator::new`. At the comparison sites in `metric::timestamp`, `.get()` extracts the inner `f32` so RNG sequences and bit-exact output are preserved. --- lading_payload/src/dogstatsd.rs | 53 ++++++++++++-------------- lading_payload/src/dogstatsd/metric.rs | 14 ++++--- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/lading_payload/src/dogstatsd.rs b/lading_payload/src/dogstatsd.rs index 72d994fc2..0d42d5e65 100644 --- a/lading_payload/src/dogstatsd.rs +++ b/lading_payload/src/dogstatsd.rs @@ -9,7 +9,7 @@ use tracing::{debug, warn}; use crate::{ Serialize, common::{ - config::ConfRange, + config::{ConfRange, Probability}, strings, strings::{random_strings_with_length, random_strings_with_length_range}, }, @@ -226,15 +226,15 @@ pub struct TimestampConfig { /// /// The `DogStatsD` protocol only supports this field for count and gauge metrics. pub range: ConfRange, - /// Probability between 0 and 1 that a generated count or gauge metric includes `|T`. - pub probability: f32, + /// Probability that a generated count or gauge metric includes `|T`. + pub probability: Probability, } impl Default for TimestampConfig { fn default() -> Self { Self { range: ConfRange::Constant(1), - probability: 0.0, + probability: Probability::try_new(0.0).expect("0.0 is in [0.0, 1.0]"), } } } @@ -245,13 +245,7 @@ impl TimestampConfig { if !range_valid { return Result::Err(format!("Timestamp range is invalid: {reason}")); } - if !self.probability.is_finite() || self.probability < 0.0 || self.probability > 1.0 { - return Result::Err(format!( - "Timestamp probability {} must be finite and in range [0.0, 1.0]", - self.probability - )); - } - if self.probability > 0.0 && self.range.start() == 0 { + if self.probability.get() > 0.0 && self.range.start() == 0 { return Result::Err( "Timestamp range start value cannot be 0 when timestamps are enabled".to_string(), ); @@ -450,7 +444,7 @@ impl MemberGenerator { external_data: Vec, cardinality: Vec, timestamp_range: ConfRange, - timestamp_probability: f32, + timestamp_probability: Probability, mut rng: &mut R, ) -> Result where @@ -833,12 +827,16 @@ impl DogStatsD { #[cfg(test)] mod test { - use super::{ConfRange, Config, KindWeights, MetricWeights, TimestampConfig}; + use super::{ConfRange, Config, KindWeights, MetricWeights, Probability, TimestampConfig}; use proptest::prelude::*; use rand::{SeedableRng, rngs::SmallRng}; use crate::{DogStatsD, Serialize}; + fn prob(value: f32) -> Probability { + Probability::try_new(value).expect("value must be in [0.0, 1.0]") + } + // Generate a batch of raw DogStatsD bytes using the given config and seed. fn generate_bytes(config: &Config, seed: u64) -> Vec { let mut rng = SmallRng::seed_from_u64(seed); @@ -942,7 +940,7 @@ mod test { metric_weights: MetricWeights::new(100, 100, 0, 0, 0, 0), timestamp: Box::new(TimestampConfig { range: ConfRange::Constant(1_656_581_400), - probability: 1.0, + probability: prob(1.0), }), ..Default::default() }; @@ -956,7 +954,7 @@ mod test { metric_weights: MetricWeights::new(100, 100, 0, 0, 0, 0), timestamp: Box::new(TimestampConfig { range: ConfRange::Constant(1_656_581_400), - probability: 0.0, + probability: prob(0.0), }), ..Default::default() }; @@ -974,7 +972,7 @@ mod test { metric_weights: MetricWeights::new(100, 100, 0, 0, 0, 0), timestamp: Box::new(TimestampConfig { range: ConfRange::Inclusive { min: 10, max: 20 }, - probability: 1.0, + probability: prob(1.0), }), ..Default::default() }; @@ -1001,7 +999,7 @@ mod test { metric_weights: MetricWeights::new(0, 0, 100, 0, 0, 0), timestamp: Box::new(TimestampConfig { range: ConfRange::Constant(1_656_581_400), - probability: 1.0, + probability: prob(1.0), }), ..Default::default() }; @@ -1013,15 +1011,14 @@ mod test { } #[test] - fn timestamp_probability_must_be_valid() { - let config = Config { - timestamp: Box::new(TimestampConfig { - probability: 2.0, - ..Default::default() - }), - ..Default::default() - }; - assert!(config.valid().is_err()); + fn timestamp_probability_rejects_out_of_range_on_deserialize() { + let yaml = "range: !constant 1\nprobability: 2.0\n"; + let err = serde_yaml::from_str::(yaml) + .expect_err("probability 2.0 must be rejected at deserialize time"); + assert!( + err.to_string().contains("exceeds 1.0"), + "unexpected error: {err}" + ); } #[test] @@ -1029,7 +1026,7 @@ mod test { let config = Config { timestamp: Box::new(TimestampConfig { range: ConfRange::Constant(0), - probability: 1.0, + probability: prob(1.0), }), ..Default::default() }; @@ -1064,7 +1061,7 @@ mod test { metric_weights: MetricWeights::new(100, 100, 0, 0, 0, 0), timestamp: Box::new(TimestampConfig { range: ConfRange::Inclusive { min: 10, max: 20 }, - probability: 0.5, + probability: prob(0.5), }), ..Default::default() }; diff --git a/lading_payload/src/dogstatsd/metric.rs b/lading_payload/src/dogstatsd/metric.rs index 71d604460..6dc18162c 100644 --- a/lading_payload/src/dogstatsd/metric.rs +++ b/lading_payload/src/dogstatsd/metric.rs @@ -8,7 +8,11 @@ use rand::{ seq::IteratorRandom, }; -use crate::{Error, Generator, common::strings, dogstatsd::metric::template::Template}; +use crate::{ + Error, Generator, + common::{config::Probability, strings}, + dogstatsd::metric::template::Template, +}; use tracing::debug; use self::strings::{Pool, choose_or_not_ref}; @@ -26,7 +30,7 @@ pub(crate) struct MetricGenerator { pub(crate) external_data: Vec, pub(crate) cardinality: Vec, pub(crate) timestamp_range: ConfRange, - pub(crate) timestamp_probability: f32, + pub(crate) timestamp_probability: Probability, pub(crate) templates: Vec, pub(crate) multivalue_count: ConfRange, pub(crate) multivalue_pack_probability: f32, @@ -54,7 +58,7 @@ impl MetricGenerator { external_data: Vec, cardinality: Vec, timestamp_range: ConfRange, - timestamp_probability: f32, + timestamp_probability: Probability, tags_generator: &mut common::tags::Generator, pools: &StringPools, value_conf: ValueConf, @@ -109,12 +113,12 @@ impl MetricGenerator { where R: Rng + ?Sized, { - if self.timestamp_probability == 0.0 { + if self.timestamp_probability.get() == 0.0 { return None; } let prob: f32 = StandardUniform.sample(rng); - if prob < self.timestamp_probability { + if prob < self.timestamp_probability.get() { Some(self.timestamp_range.sample(rng)) } else { None