Skip to content

Commit 34de63f

Browse files
committed
chore(antithesis): Bound contexts in runs
This commit introduces a new protocol to the intake node to allow for context bounding in runs. The purposes are twofold. First, while it's interesting to explore timelines in which there are too many contexts for either lane to handle these are less relevant to me _now_, so I've capped total contexts at 1M. Second, in order for timeseries equality to _work_ I need to be certain that I have at least _some_ timeseries with more than one point emitted into them. Previously drivers were emitting essentially random contexts and it was very unlikely for more than one point to be emitted per context. Oops. Drivers now request N contexts from the intake service for each sub-kind of dogstatsd they emit, meaning all contexts are sourced from a single spot in a topology and not from pure randomness.
1 parent e4ff7fa commit 34de63f

27 files changed

Lines changed: 1173 additions & 353 deletions

Cargo.lock

Lines changed: 4 additions & 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ itoa = { workspace = true }
2020
libc = { workspace = true }
2121
num-traits = { workspace = true }
2222
rand = { workspace = true }
23+
reqwest = { workspace = true, features = ["blocking"] }
2324
ryu = { workspace = true }
2425
serde = { workspace = true }
2526
serde_json = { workspace = true }
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 671f360ca24ca5add047c2f93acf185116ebfa9c663637a430a15fa6abc56151 # shrinks to seed = 8436120490560261333, kind = Metric

test/antithesis/harness/src/bin/first_sample_config/main.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use antithesis_sdk::prelude::*;
1414
use antithesis_sdk::random::AntithesisRng;
1515
use anyhow::Context;
1616
use clap::Parser;
17-
use harness::config::DatadogConfig;
17+
use harness::config::{ContextSourceConfig, DatadogConfig};
1818
use rand::rand_core::UnwrapErr;
1919
use serde_json::json;
2020

@@ -67,6 +67,15 @@ fn main() -> anyhow::Result<()> {
6767
fs::write(&driver_path, config.driver_config(&mut rng).to_yaml()?.as_bytes())
6868
.with_context(|| format!("write driver config {}", driver_path.display()))?;
6969

70+
// Per-kind context-pool caps for this timeline, read lazily by the intake's shared pool. Sampled
71+
// here so the pool's cardinality varies per timeline like every other sampled knob.
72+
let context_source_path = cli.config_dir.join("context_source.yaml");
73+
fs::write(
74+
&context_source_path,
75+
ContextSourceConfig::sample(&mut rng).to_yaml()?.as_bytes(),
76+
)
77+
.with_context(|| format!("write context source config {}", context_source_path.display()))?;
78+
7079
// Per-timeline anchor: counting these in triage tells us how many distinct
7180
// configs the run sampled.
7281
let details = serde_json::to_value(&config).unwrap_or_else(|e| json!({ "serialize_error": e.to_string() }));

test/antithesis/harness/src/config.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ cloud_provider_metadata: []
186186
/// Upper bound on datagrams one driver invocation ships in a timeline.
187187
const MAX_DATAGRAMS: usize = 10_000;
188188

189+
/// Upper bound on the working set one driver invocation fetches from the shared context pool.
190+
const MAX_WORKING_SET: u64 = 1_024;
191+
189192
/// Config a load generator reads to shape its output to this timeline's SUT.
190193
/// `first_sample_config` samples it beside `datadog.yaml` from one draw, so the
191194
/// generator and the SUT are driven together.
@@ -197,6 +200,9 @@ pub struct DriverConfig {
197200
pub payload_byte_limit: usize,
198201
/// Datagrams a driver invocation ships this timeline.
199202
pub datagram_count: usize,
203+
/// Distinct contexts a driver invocation fetches from the shared pool as its working set, each
204+
/// boundary-biased log-uniform in `1..=MAX_WORKING_SET`.
205+
pub context_count: usize,
200206
}
201207

202208
impl DriverConfig {
@@ -211,6 +217,7 @@ impl DriverConfig {
211217
Self {
212218
payload_byte_limit,
213219
datagram_count: rng.random_range(0..=MAX_DATAGRAMS),
220+
context_count: usize::try_from(Probe::new(1, MAX_WORKING_SET).sample(rng)).unwrap_or(usize::MAX),
214221
}
215222
}
216223

@@ -237,6 +244,61 @@ impl DriverConfig {
237244
}
238245
}
239246

247+
/// Upper bound on the distinct contexts a shared pool holds per kind.
248+
const MAX_CONTEXTS_PER_KIND: u64 = 1_000_000;
249+
250+
/// The per-kind caps a timeline's shared context pool fills to before it recurs existing contexts.
251+
/// `first_sample_config` samples this beside `datadog.yaml` so cardinality varies per timeline. Each
252+
/// cap is drawn independently, so the distinct-context count per kind varies at random.
253+
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
254+
pub struct ContextSourceConfig {
255+
/// Distinct metric contexts the pool holds.
256+
pub metric_contexts: usize,
257+
/// Distinct event contexts the pool holds.
258+
pub event_contexts: usize,
259+
/// Distinct service-check contexts the pool holds.
260+
pub service_check_contexts: usize,
261+
}
262+
263+
impl ContextSourceConfig {
264+
/// Sample the per-kind caps, each boundary-biased log-uniform in `1..=MAX_CONTEXTS_PER_KIND`.
265+
#[must_use]
266+
pub fn sample<R: Rng + ?Sized>(rng: &mut R) -> Self {
267+
Self {
268+
metric_contexts: sample_cap(rng),
269+
event_contexts: sample_cap(rng),
270+
service_check_contexts: sample_cap(rng),
271+
}
272+
}
273+
274+
/// Render `self` as a `context_source.yaml` string.
275+
///
276+
/// # Errors
277+
///
278+
/// Returns an error if serialization fails.
279+
pub fn to_yaml(&self) -> anyhow::Result<String> {
280+
serde_yaml::to_string(self).context("serialize context_source.yaml")
281+
}
282+
283+
/// Read the context-source config from the `context_source.yaml` that `first_sample_config` wrote
284+
/// to `config_dir`.
285+
///
286+
/// # Errors
287+
///
288+
/// Returns an error if the config is unreadable or is not valid YAML.
289+
pub fn read(config_dir: &Path) -> anyhow::Result<Self> {
290+
let path = config_dir.join("context_source.yaml");
291+
let yaml = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
292+
serde_yaml::from_str(&yaml).with_context(|| format!("parse context source config from {}", path.display()))
293+
}
294+
}
295+
296+
/// A single per-kind cap. The Probe max is well within `usize` on every supported target, so the
297+
/// saturating conversion is unreachable in practice.
298+
fn sample_cap<R: Rng + ?Sized>(rng: &mut R) -> usize {
299+
usize::try_from(Probe::new(1, MAX_CONTEXTS_PER_KIND).sample(rng)).unwrap_or(usize::MAX)
300+
}
301+
240302
#[cfg(test)]
241303
mod tests {
242304
use std::convert::Infallible;

0 commit comments

Comments
 (0)