-
Notifications
You must be signed in to change notification settings - Fork 16
Vary lading.yaml configs under Antithesis #1922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: blt/introducing_lading_mvp_scenario
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| [package] | ||
| name = "harness" | ||
| version = "0.1.0" | ||
| edition = "2024" | ||
| license = "MIT" | ||
| publish = false | ||
| description = "Shared Antithesis harness for lading scenarios: per-timeline config sampling." | ||
|
|
||
| [lib] | ||
| doctest = false | ||
|
|
||
| [[bin]] | ||
| name = "first_sample_config" | ||
| path = "src/bin/first_sample_config.rs" | ||
|
|
||
| [lints] | ||
| workspace = true | ||
|
|
||
| [dependencies] | ||
| lading = { path = "../../../lading" } | ||
| lading-payload = { path = "../../../lading_payload" } | ||
| lading-antithesis = { workspace = true, features = ["antithesis"] } | ||
| antithesis_sdk = { workspace = true, features = ["full", "rand_v0_10"] } | ||
| byte-unit = { workspace = true, features = ["std"] } | ||
| rand = { workspace = true, features = ["thread_rng", "std_rng"] } | ||
| serde_yaml = { workspace = true } | ||
| anyhow = { workspace = true } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| //! Antithesis `first_` command: sample this timeline's lading config and release | ||
| //! the blocked system-under-test. | ||
| //! | ||
| //! Runs once per timeline after `setup_complete`, so the `AntithesisRng` draws | ||
| //! are post-snapshot decisions Antithesis branches: each timeline boots lading | ||
| //! under its own sampled config. Writes the config to the shared volume, tags | ||
| //! the sample for triage, then writes the `ready` sentinel last so the config is | ||
| //! always present before the SUT unblocks. | ||
|
|
||
| use std::path::PathBuf; | ||
|
|
||
| use anyhow::Context; | ||
|
|
||
| fn main() -> anyhow::Result<()> { | ||
| lading_antithesis::init(); | ||
|
|
||
| let dir: PathBuf = | ||
| std::env::var_os("CONFIG_DIR").map_or_else(|| PathBuf::from("/shared"), PathBuf::from); | ||
| std::fs::create_dir_all(&dir) | ||
| .with_context(|| format!("create config dir {}", dir.display()))?; | ||
|
|
||
| // Draw structured choices from AntithesisRng so Antithesis branches each pick | ||
| // and explores the config menu across timelines. UnwrapErr adapts the SDK's | ||
| // fallible RNG to rand's infallible RngCore. | ||
| let mut rng = rand::rand_core::UnwrapErr(antithesis_sdk::random::AntithesisRng); | ||
| let cfg = harness::config::sample(&mut rng); | ||
| let variant = harness::config::variant_label(&cfg.variant); | ||
| let yaml = harness::config::to_yaml(&cfg).context("serialize sampled config")?; | ||
|
|
||
| let config_path = dir.join("lading.yaml"); | ||
| std::fs::write(&config_path, yaml.as_bytes()) | ||
| .with_context(|| format!("write {}", config_path.display()))?; | ||
|
|
||
| // Per-timeline anchor: counting these in triage shows how many distinct | ||
| // variants the run explored. | ||
| lading_antithesis::reachable!("first_sample_config sampled a config", { "variant": variant }); | ||
|
|
||
| let ready = dir.join("ready"); | ||
| std::fs::write(&ready, b"ready\n") | ||
| .with_context(|| format!("write sentinel {}", ready.display()))?; | ||
|
|
||
| Ok(()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| //! Per-timeline lading config sampling. | ||
| //! | ||
| //! The sampler builds lading's real [`tcp::Config`] so the value menu is exactly | ||
| //! the config schema, then serializes it under the `generator: [{ tcp: … }]` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This ellipsis, the second ellipsis in the AGENTS.md reference: AGENTS.md:L151-L158 Useful? React with 👍 / 👎. |
||
| //! shape lading parses. The transport is fixed to TCP against the sink oracle, | ||
| //! which counts bytes and so catches any payload variant; only the free axes | ||
| //! (payload variant, rate, connections, block sizes, seed) vary. Transport | ||
| //! variation waits on a multi-protocol sink. | ||
|
|
||
| use lading::generator::tcp; | ||
| use rand::seq::IndexedRandom; | ||
| use rand::{Rng, RngExt}; | ||
|
|
||
| /// Fixed address of the sink oracle every sampled config targets. | ||
| const SINK_ADDR: &str = "sink:9000"; | ||
|
|
||
| /// Payload variants the TCP sink catches with no decoding. All are fieldless, so | ||
| /// they need no extra configuration to sample. | ||
| fn variant_menu() -> [lading_payload::Config; 6] { | ||
| [ | ||
| lading_payload::Config::Ascii, | ||
| lading_payload::Config::Syslog5424, | ||
| lading_payload::Config::Json, | ||
| lading_payload::Config::Fluent, | ||
| lading_payload::Config::ApacheCommon, | ||
| lading_payload::Config::DatadogLog, | ||
| ] | ||
| } | ||
|
|
||
| /// Sample a lading TCP generator config for one timeline. | ||
| /// | ||
| /// Draws the payload variant, throughput, parallel connections, and seed from | ||
| /// `rng`; the transport and target stay fixed to the sink. | ||
| #[must_use] | ||
| pub fn sample<R: Rng>(rng: &mut R) -> tcp::Config { | ||
| // Structured choices come from the caller's rng -- AntithesisRng in | ||
| // production -- so Antithesis branches each pick and sweeps the menu. | ||
| let variant = variant_menu() | ||
| .choose(rng) | ||
| .cloned() | ||
| .unwrap_or(lading_payload::Config::Ascii); | ||
|
|
||
| let bps_mib = [1_u64, 5, 10, 50, 100].choose(rng).copied().unwrap_or(10); | ||
| let bytes_per_second_bytes = bps_mib * 1024 * 1024; | ||
| let bytes_per_second = Some(byte_unit::Byte::from_u64(bytes_per_second_bytes)); | ||
|
|
||
| let parallel_connections = rng.random_range(1..=8_u16); | ||
|
|
||
| // Cap the block size at the smallest per-connection throttle capacity. lading | ||
| // divides `bytes_per_second` evenly across `parallel_connections`; a block | ||
| // larger than a worker's divided capacity is rejected by the throttle, and | ||
| // the TCP worker then busy-spins discarding it with no backoff. Keeping | ||
| // `maximum_block_size <= bytes_per_second / parallel_connections` guarantees | ||
| // every block fits, so no timeline degrades into a discard spin. | ||
| let per_connection_capacity = bytes_per_second_bytes / u64::from(parallel_connections); | ||
| let maximum_block_size = | ||
| byte_unit::Byte::from_u64(per_connection_capacity.clamp(1, 1024 * 1024)); | ||
|
|
||
| // lading seeds its own payload PRNG from `seed`. The Antithesis docs warn | ||
| // against seeding your own RNG from SDK randomness, so draw the seed from | ||
| // system entropy rather than the (Antithesis) `rng`. This makes payload byte | ||
| // content opaque to Antithesis and effectively fixed across timelines, which | ||
| // is fine: the sink asserts on bytes received, not on content. | ||
| let mut seed = [0u8; 32]; | ||
| rand::rng().fill_bytes(&mut seed); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L86-L92 Useful? React with 👍 / 👎. |
||
|
|
||
| tcp::Config { | ||
| seed, | ||
| addr: SINK_ADDR.to_string(), | ||
| variant, | ||
| bytes_per_second, | ||
| maximum_block_size, | ||
| maximum_prebuild_cache_size_bytes: byte_unit::Byte::from_u64(8 * 1024 * 1024), | ||
| parallel_connections, | ||
| throttle: None, | ||
| } | ||
| } | ||
|
|
||
| /// Serialize a sampled `tcp::Config` into the top-level `generator: [{ tcp: … }]` | ||
| /// YAML that lading consumes. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns an error if serialization fails. | ||
| pub fn to_yaml(cfg: &tcp::Config) -> Result<String, serde_yaml::Error> { | ||
| let mut tcp_item = serde_yaml::Mapping::new(); | ||
| tcp_item.insert(serde_yaml::Value::from("tcp"), serde_yaml::to_value(cfg)?); | ||
| let generators = serde_yaml::Value::Sequence(vec![serde_yaml::Value::Mapping(tcp_item)]); | ||
| let mut top = serde_yaml::Mapping::new(); | ||
| top.insert(serde_yaml::Value::from("generator"), generators); | ||
| serde_yaml::to_string(&serde_yaml::Value::Mapping(top)) | ||
| } | ||
|
|
||
| /// Short, stable label for a payload variant, for tagging the Antithesis sample. | ||
| #[must_use] | ||
| pub fn variant_label(variant: &lading_payload::Config) -> &'static str { | ||
| match variant { | ||
| lading_payload::Config::Ascii => "ascii", | ||
| lading_payload::Config::Syslog5424 => "syslog5424", | ||
| lading_payload::Config::Json => "json", | ||
| lading_payload::Config::Fluent => "fluent", | ||
| lading_payload::Config::ApacheCommon => "apache_common", | ||
| lading_payload::Config::DatadogLog => "datadog_log", | ||
| _ => "other", | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{sample, to_yaml}; | ||
| use rand::SeedableRng; | ||
| use rand::rngs::StdRng; | ||
|
|
||
| #[test] | ||
| fn sampled_config_deserializes_as_valid_lading_config() { | ||
|
Comment on lines
+114
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These three randomized-invariant checks are ordinary unit tests over a hard-coded set of 256 seeds, so they neither use the repository's required property-test strategy nor provide shrinking when serialization or block-capacity invariants fail. Express the sampled-config invariants as AGENTS.md reference: AGENTS.md:L121-L132 Useful? React with 👍 / 👎. |
||
| // The load-bearing invariant: whatever we sample must be a config lading | ||
| // actually accepts. Sweep many seeds so the whole menu is exercised. | ||
| for s in 0..256_u64 { | ||
| let mut rng = StdRng::seed_from_u64(s); | ||
| let cfg = sample(&mut rng); | ||
| let yaml = to_yaml(&cfg).expect("serialize sampled config"); | ||
| let parsed: Result<lading::config::Config, _> = serde_yaml::from_str(&yaml); | ||
| assert!( | ||
| parsed.is_ok(), | ||
| "seed {s} produced a config lading rejects: {err:?}\n{yaml}", | ||
| err = parsed.err() | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn sampled_config_holds_invariants() { | ||
| for s in 0..256_u64 { | ||
| let mut rng = StdRng::seed_from_u64(s); | ||
| let cfg = sample(&mut rng); | ||
| assert_eq!(cfg.addr, "sink:9000"); | ||
| assert!((1..=8).contains(&cfg.parallel_connections)); | ||
| assert!(cfg.bytes_per_second.is_some()); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn block_size_never_exceeds_per_connection_capacity() { | ||
| // Regression: lading divides bytes_per_second across parallel_connections, | ||
| // and a block larger than a worker's divided capacity busy-spins on | ||
| // discard. maximum_block_size must fit the smallest per-connection share. | ||
| for s in 0..256_u64 { | ||
| let mut rng = StdRng::seed_from_u64(s); | ||
| let cfg = sample(&mut rng); | ||
| let bps = cfg.bytes_per_second.expect("bytes_per_second set").as_u64(); | ||
| let per_connection = bps / u64::from(cfg.parallel_connections); | ||
| assert!( | ||
| cfg.maximum_block_size.as_u64() <= per_connection, | ||
| "seed {s}: block {block} exceeds per-connection capacity {per_connection}", | ||
| block = cfg.maximum_block_size.as_u64() | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| //! Shared Antithesis harness for lading scenarios. | ||
| //! | ||
| //! Holds the per-timeline config-variation mechanism every scenario reuses: | ||
| //! [`config::sample`] draws a lading generator config from a value menu, and the | ||
| //! `first_sample_config` command serializes it to the shared volume the | ||
| //! system-under-test boots from. The menu is built from lading's own | ||
| //! `tcp::Config`, so it cannot drift from the real config schema. | ||
|
|
||
| pub mod config; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This context string and the two later write-error contexts use anonymous
{}placeholders, contrary to the repository's named-format-argument convention. Give each displayed path a named argument so these newly added diagnostics follow the required formatting style.AGENTS.md reference: AGENTS.md:L111-L116
Useful? React with 👍 / 👎.