Skip to content

Commit 51945fe

Browse files
committed
chore(antithesis): dogstatsd generation with byte limit
This commit updates the harness driver to build dogstatsd to a byte limit, in the manner of datadog/lading. I have been unable to investigate SMPTNG-7611 well owing to the error log emission by Datadog Agent. I've added property tests to assert the payload limit is obeyed, accepting that this means making the dogstatsd generator pure with regard to Rng and may not use antithesis SDK's random_* directly.
1 parent 4b011df commit 51945fe

11 files changed

Lines changed: 226 additions & 113 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/antithesis/harness/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ serde = { workspace = true }
2424
serde_json = { workspace = true }
2525
serde_yaml = { workspace = true }
2626

27+
[dev-dependencies]
28+
proptest = { workspace = true }
29+
2730
[lints.clippy]
2831
all = "deny"
2932
complexity = "deny"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 377cba92b14295b3fe5c6f2738a398ee042be8e35f529398ecf9e275e6b0a1da # shrinks to seed = 0

test/antithesis/harness/src/driver.rs

Lines changed: 33 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,104 +15,69 @@ use std::sync::mpsc::sync_channel;
1515
use std::thread::{self, sleep};
1616
use std::time::{Duration, Instant};
1717

18-
use antithesis_sdk::random::{random_choice, AntithesisRng};
18+
use antithesis_sdk::random::AntithesisRng;
1919
use anyhow::Context;
20-
use rand::{rand_core::UnwrapErr, RngExt};
20+
use rand::seq::IndexedRandom;
21+
use rand::RngExt;
2122

2223
use crate::payload::dogstatsd;
24+
pub use crate::payload::dogstatsd::Batch;
2325

2426
const SEND_RETRY_BUDGET: Duration = Duration::from_secs(5);
2527
const SEND_RETRY_BACKOFF: Duration = Duration::from_millis(1);
2628

27-
/// Per-batch composition: 50% clean, 25% feral, 25% mixed.
28-
#[derive(Clone, Copy, Debug)]
29-
pub enum Batch {
30-
/// Every line clean.
31-
Clean,
32-
/// Every line feral.
33-
Feral,
34-
/// A per-line clean-or-feral mix.
35-
Mixed,
36-
}
37-
38-
impl Batch {
39-
/// Sample a batch composition: half clean, a quarter feral, a quarter mixed.
40-
#[must_use]
41-
pub fn sample() -> Self {
42-
match random_choice(&[Batch::Clean, Batch::Clean, Batch::Feral, Batch::Mixed]) {
43-
Some(Batch::Feral) => Batch::Feral,
44-
Some(Batch::Mixed) => Batch::Mixed,
45-
_ => Batch::Clean,
46-
}
47-
}
48-
49-
/// The vibe for one line drawn from this batch.
50-
fn vibe(self) -> dogstatsd::Vibe {
51-
match self {
52-
Batch::Clean => dogstatsd::Vibe::Clean,
53-
Batch::Feral => dogstatsd::Vibe::Feral,
54-
Batch::Mixed => dogstatsd::sample_vibe(),
55-
}
29+
/// Sample a line composition: half clean, a quarter feral, a quarter mixed.
30+
#[must_use]
31+
pub fn sample() -> Batch {
32+
let mut rng = AntithesisRng;
33+
match [Batch::Clean, Batch::Clean, Batch::Feral, Batch::Mixed].choose(&mut rng) {
34+
Some(Batch::Feral) => Batch::Feral,
35+
Some(Batch::Mixed) => Batch::Mixed,
36+
_ => Batch::Clean,
5637
}
5738
}
5839

59-
/// A generated dogstatsd line queued for the sockets.
60-
enum Line {
61-
/// A single-value line.
62-
Single { bytes: Vec<u8> },
63-
/// A multi-value `:`-packed metric.
64-
Multi {
65-
/// The encoded line.
66-
bytes: Vec<u8>,
67-
/// The number of values in the packed run.
68-
count: usize,
69-
},
70-
}
71-
72-
impl Line {
73-
/// The encoded bytes to ship over a socket.
74-
fn bytes(&self) -> &[u8] {
75-
match self {
76-
Line::Single { bytes } | Line::Multi { bytes, .. } => bytes,
77-
}
78-
}
40+
/// A generated payload queued for the sockets: the packed bytes and what they hold.
41+
struct Datagram {
42+
/// The `\n`-packed payload bytes to ship over a socket.
43+
bytes: Vec<u8>,
44+
/// The lines and largest packed run in `bytes`.
45+
payload: dogstatsd::Payload,
7946
}
8047

8148
/// What a driver run shipped, for anchoring assertions.
8249
#[derive(Clone, Debug)]
8350
pub struct Stats {
84-
/// Lines pulled from the channel, whether or not any send succeeded.
51+
/// Payloads pulled from the channel, whether or not any send succeeded.
8552
pub received: usize,
86-
/// Successful sends per socket, indexed as the sockets were passed to [`run`].
53+
/// Lines delivered per socket, summed across payloads, indexed as the sockets
54+
/// were passed to [`run`].
8755
pub sent: Vec<usize>,
8856
/// Largest packed run that reached each socket, indexed likewise. Zero when
8957
/// no multi-value line reached that socket.
9058
pub max_packed: Vec<usize>,
9159
}
9260

93-
/// Drive one batch of sampled `DogStatsD` lines to every socket, blocking through
94-
/// backpressure so on success `sent[i] == received` for all `i`.
61+
/// Drive a batch of sampled `DogStatsD` payloads to every socket, blocking
62+
/// through backpressure so every payload reaches every socket.
9563
///
9664
/// # Errors
9765
///
98-
/// Errors if a line cannot be delivered within the retry budget, or a worker panics.
66+
/// Errors if a payload cannot be delivered within the retry budget, or a worker panics.
9967
pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> anyhow::Result<Stats> {
10068
let count = {
101-
let mut rng = UnwrapErr(AntithesisRng);
69+
let mut rng = AntithesisRng;
10270
rng.random_range(0..=10_000u64)
10371
};
10472

105-
let (tx, rx) = sync_channel::<Line>(2024);
73+
let (tx, rx) = sync_channel::<Datagram>(2024);
10674

10775
let producer = thread::spawn(move || {
108-
let mut rng = UnwrapErr(AntithesisRng);
76+
let mut rng = AntithesisRng;
10977
for _ in 0..count {
11078
let mut bytes = Vec::new();
111-
let line = match dogstatsd::send(&mut rng, &mut bytes, batch.vibe()) {
112-
None => Line::Single { bytes },
113-
Some(count) => Line::Multi { bytes, count },
114-
};
115-
if tx.send(line).is_err() {
79+
let payload = dogstatsd::write_payload(&mut rng, &mut bytes, batch, dogstatsd::PAYLOAD_BYTE_LIMIT);
80+
if tx.send(Datagram { bytes, payload }).is_err() {
11681
break;
11782
}
11883
}
@@ -122,14 +87,12 @@ pub fn run(batch: Batch, sockets: Vec<UnixDatagram>) -> anyhow::Result<Stats> {
12287
let mut received = 0usize;
12388
let mut sent = vec![0usize; sockets.len()];
12489
let mut max_packed = vec![0usize; sockets.len()];
125-
while let Ok(line) = rx.recv() {
90+
while let Ok(datagram) = rx.recv() {
12691
received += 1;
12792
for (i, socket) in sockets.iter().enumerate() {
128-
deliver(socket, line.bytes())?;
129-
sent[i] += 1;
130-
if let Line::Multi { count, .. } = &line {
131-
max_packed[i] = max_packed[i].max(*count);
132-
}
93+
deliver(socket, &datagram.bytes)?;
94+
sent[i] += datagram.payload.lines;
95+
max_packed[i] = max_packed[i].max(datagram.payload.max_packed);
13396
}
13497
}
13598
Ok(Stats {

test/antithesis/harness/src/payload/dogstatsd.rs

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,26 @@ fn choose_message<R: Rng + ?Sized>(rng: &mut R) -> Message {
101101
}
102102
}
103103

104-
/// Write one `DogStatsD` message of a sampled type to `buf` at the given vibe.
104+
/// The `dogstatsd_buffer_size` default, via Datadog Agent.
105+
pub const PAYLOAD_BYTE_LIMIT: usize = 8_192;
106+
107+
/// What a generated payload holds, for anchoring assertions.
108+
#[derive(Clone, Copy, Debug, Default)]
109+
pub struct Payload {
110+
/// Lines packed into the buffer.
111+
pub lines: usize,
112+
/// Largest packed multi-value run among those lines. Zero when none.
113+
pub max_packed: usize,
114+
}
115+
116+
/// Append one `DogStatsD` line of a sampled type to `buf`, capping the appended
117+
/// bytes at `limit`. A non-empty line is always `\n`-terminated.
105118
///
106119
/// Returns the packed value count when a multi-value metric was emitted, else
107120
/// `None`.
108-
pub fn send<R: Rng + ?Sized>(rng: &mut R, buf: &mut Vec<u8>, vibe: Vibe) -> Option<usize> {
109-
buf.clear();
110-
match choose_message(rng) {
121+
pub fn write_line<R: Rng + ?Sized>(rng: &mut R, buf: &mut Vec<u8>, vibe: Vibe, limit: usize) -> Option<usize> {
122+
let start = buf.len();
123+
let packed = match choose_message(rng) {
111124
Message::Event => {
112125
events::write(rng, buf, vibe);
113126
None
@@ -117,5 +130,120 @@ pub fn send<R: Rng + ?Sized>(rng: &mut R, buf: &mut Vec<u8>, vibe: Vibe) -> Opti
117130
None
118131
}
119132
Message::Metric => metrics::write(rng, buf, vibe),
133+
};
134+
if buf.len() - start > limit {
135+
buf.truncate(start + limit);
136+
// Restore the terminator the truncation dropped. Lines carry no interior
137+
// newline, so the result still holds exactly one.
138+
if buf.len() > start {
139+
if let Some(last) = buf.last_mut() {
140+
*last = b'\n';
141+
}
142+
}
143+
}
144+
packed
145+
}
146+
147+
/// Per-run line composition: every line clean, every line feral, or a per-line
148+
/// clean-or-feral mix.
149+
#[derive(Clone, Copy, Debug)]
150+
pub enum Batch {
151+
/// Every line clean.
152+
Clean,
153+
/// Every line feral.
154+
Feral,
155+
/// Each line independently clean or feral.
156+
Mixed,
157+
}
158+
159+
impl Batch {
160+
/// The vibe for one line of this batch, sampled per call so `Mixed` interleaves.
161+
fn vibe<R: Rng + ?Sized>(self, rng: &mut R) -> Vibe {
162+
match self {
163+
Batch::Clean => Vibe::Clean,
164+
Batch::Feral => Vibe::Feral,
165+
Batch::Mixed => sample_vibe(rng),
166+
}
167+
}
168+
}
169+
170+
/// Fill `buf` with `\n`-terminated lines, packing whole lines straight into it
171+
/// and drawing the `limit` budget down as each lands. When a line overruns the
172+
/// remaining budget it is rolled back and packing stops, so `buf` holds only
173+
/// whole lines and never exceeds `limit`. Each line's vibe is sampled from
174+
/// `batch`, so a `Mixed` payload interleaves clean and feral lines. Clears `buf`
175+
/// first.
176+
pub fn write_payload<R: Rng + ?Sized>(rng: &mut R, buf: &mut Vec<u8>, batch: Batch, limit: usize) -> Payload {
177+
buf.clear();
178+
let mut payload = Payload::default();
179+
loop {
180+
let vibe = batch.vibe(rng);
181+
let start = buf.len();
182+
let packed = write_line(rng, buf, vibe, limit);
183+
if buf.len() > limit {
184+
// The line overran the remaining budget. Roll it back and stop.
185+
buf.truncate(start);
186+
break;
187+
}
188+
if buf.len() == start {
189+
// Nothing written — the budget is spent.
190+
break;
191+
}
192+
payload.lines += 1;
193+
if let Some(count) = packed {
194+
payload.max_packed = payload.max_packed.max(count);
195+
}
196+
}
197+
payload
198+
}
199+
200+
#[cfg(test)]
201+
mod test {
202+
use proptest::prelude::*;
203+
use rand::rngs::SmallRng;
204+
use rand::SeedableRng;
205+
206+
use super::{write_line, write_payload, Batch, Vibe};
207+
208+
fn any_vibe() -> impl Strategy<Value = Vibe> {
209+
prop_oneof![Just(Vibe::Clean), Just(Vibe::Feral)]
210+
}
211+
212+
fn any_batch() -> impl Strategy<Value = Batch> {
213+
prop_oneof![Just(Batch::Clean), Just(Batch::Feral), Just(Batch::Mixed)]
214+
}
215+
216+
/// Lines carry no interior newline and each is `\n`-terminated, so the line
217+
/// count equals the newline count.
218+
#[allow(clippy::naive_bytecount)]
219+
fn newline_count(buf: &[u8]) -> usize {
220+
buf.iter().filter(|&&b| b == b'\n').count()
221+
}
222+
223+
proptest! {
224+
#[test]
225+
fn write_line_stays_within_its_limit(seed: u64, limit: u16, vibe in any_vibe()) {
226+
let mut rng = SmallRng::seed_from_u64(seed);
227+
let limit = usize::from(limit);
228+
let mut buf = Vec::new();
229+
write_line(&mut rng, &mut buf, vibe, limit);
230+
231+
prop_assert!(buf.len() <= limit);
232+
if !buf.is_empty() {
233+
prop_assert_eq!(buf[buf.len() - 1], b'\n');
234+
prop_assert_eq!(newline_count(&buf), 1);
235+
}
236+
}
237+
238+
#[test]
239+
fn write_payload_stays_within_its_limit(seed: u64, limit: u16, batch in any_batch()) {
240+
let mut rng = SmallRng::seed_from_u64(seed);
241+
let limit = usize::from(limit);
242+
let mut buf = Vec::new();
243+
let payload = write_payload(&mut rng, &mut buf, batch, limit);
244+
245+
prop_assert!(buf.len() <= limit);
246+
prop_assert_eq!(newline_count(&buf), payload.lines);
247+
}
120248
}
121249
}

0 commit comments

Comments
 (0)