From 8a6d7236efc8d2791b20ce0f693e2eed8bc836e0 Mon Sep 17 00:00:00 2001 From: andrussal Date: Thu, 18 Dec 2025 14:53:08 +0100 Subject: [PATCH] workflows: add try_* builder APIs for URLs and rates --- .../workflows/src/builder/mod.rs | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/testing-framework/workflows/src/builder/mod.rs b/testing-framework/workflows/src/builder/mod.rs index d6f667d..036a91d 100644 --- a/testing-framework/workflows/src/builder/mod.rs +++ b/testing-framework/workflows/src/builder/mod.rs @@ -13,6 +13,18 @@ use crate::{ workloads::{chaos::RandomRestartWorkload, da, transaction}, }; +#[derive(Debug, thiserror::Error)] +pub enum BuilderInputError { + #[error("{field} must be non-zero")] + ZeroValue { field: &'static str }, + #[error("invalid url for {field}: '{value}': {message}")] + InvalidUrl { + field: &'static str, + value: String, + message: String, + }, +} + macro_rules! non_zero_rate_fn { ($name:ident, $message:literal) => { const fn $name(rate: u64) -> NonZeroU64 { @@ -106,6 +118,13 @@ pub trait ObservabilityBuilderExt: Sized { /// Convenience wrapper that parses a URL string (panics if invalid). fn with_metrics_query_url_str(self, url: &str) -> CoreScenarioBuilder; + /// Like `with_metrics_query_url_str`, but returns an error instead of + /// panicking. + fn try_with_metrics_query_url_str( + self, + url: &str, + ) -> Result, BuilderInputError>; + /// Configure the OTLP HTTP metrics ingest endpoint to which nodes should /// export metrics (must be a full URL, including any required path). fn with_metrics_otlp_ingest_url( @@ -119,12 +138,25 @@ pub trait ObservabilityBuilderExt: Sized { url: &str, ) -> CoreScenarioBuilder; + /// Like `with_metrics_otlp_ingest_url_str`, but returns an error instead of + /// panicking. + fn try_with_metrics_otlp_ingest_url_str( + self, + url: &str, + ) -> Result, BuilderInputError>; + /// Optional Grafana base URL for printing/logging (human access). fn with_grafana_url(self, url: reqwest::Url) -> CoreScenarioBuilder; /// Convenience wrapper that parses a URL string (panics if invalid). fn with_grafana_url_str(self, url: &str) -> CoreScenarioBuilder; + /// Like `with_grafana_url_str`, but returns an error instead of panicking. + fn try_with_grafana_url_str( + self, + url: &str, + ) -> Result, BuilderInputError>; + #[deprecated(note = "use with_metrics_query_url")] fn with_external_prometheus( self, @@ -175,6 +207,18 @@ impl ObservabilityBuilderExt for CoreScenarioBuilder<()> { self.with_metrics_query_url(parsed) } + fn try_with_metrics_query_url_str( + self, + url: &str, + ) -> Result, BuilderInputError> { + let parsed = reqwest::Url::parse(url).map_err(|err| BuilderInputError::InvalidUrl { + field: "metrics_query_url", + value: url.to_string(), + message: err.to_string(), + })?; + Ok(self.with_metrics_query_url(parsed)) + } + fn with_metrics_otlp_ingest_url( self, url: reqwest::Url, @@ -194,6 +238,18 @@ impl ObservabilityBuilderExt for CoreScenarioBuilder<()> { self.with_metrics_otlp_ingest_url(parsed) } + fn try_with_metrics_otlp_ingest_url_str( + self, + url: &str, + ) -> Result, BuilderInputError> { + let parsed = reqwest::Url::parse(url).map_err(|err| BuilderInputError::InvalidUrl { + field: "metrics_otlp_ingest_url", + value: url.to_string(), + message: err.to_string(), + })?; + Ok(self.with_metrics_otlp_ingest_url(parsed)) + } + fn with_grafana_url(self, url: reqwest::Url) -> CoreScenarioBuilder { self.with_capabilities(ObservabilityCapability { metrics_query_url: None, @@ -206,6 +262,18 @@ impl ObservabilityBuilderExt for CoreScenarioBuilder<()> { let parsed = reqwest::Url::parse(url).expect("grafana url must be valid"); self.with_grafana_url(parsed) } + + fn try_with_grafana_url_str( + self, + url: &str, + ) -> Result, BuilderInputError> { + let parsed = reqwest::Url::parse(url).map_err(|err| BuilderInputError::InvalidUrl { + field: "grafana_url", + value: url.to_string(), + message: err.to_string(), + })?; + Ok(self.with_grafana_url(parsed)) + } } impl ObservabilityBuilderExt for CoreScenarioBuilder { @@ -222,6 +290,18 @@ impl ObservabilityBuilderExt for CoreScenarioBuilder { self.with_metrics_query_url(parsed) } + fn try_with_metrics_query_url_str( + self, + url: &str, + ) -> Result, BuilderInputError> { + let parsed = reqwest::Url::parse(url).map_err(|err| BuilderInputError::InvalidUrl { + field: "metrics_query_url", + value: url.to_string(), + message: err.to_string(), + })?; + Ok(self.with_metrics_query_url(parsed)) + } + fn with_metrics_otlp_ingest_url( mut self, url: reqwest::Url, @@ -238,6 +318,18 @@ impl ObservabilityBuilderExt for CoreScenarioBuilder { self.with_metrics_otlp_ingest_url(parsed) } + fn try_with_metrics_otlp_ingest_url_str( + self, + url: &str, + ) -> Result, BuilderInputError> { + let parsed = reqwest::Url::parse(url).map_err(|err| BuilderInputError::InvalidUrl { + field: "metrics_otlp_ingest_url", + value: url.to_string(), + message: err.to_string(), + })?; + Ok(self.with_metrics_otlp_ingest_url(parsed)) + } + fn with_grafana_url( mut self, url: reqwest::Url, @@ -250,6 +342,18 @@ impl ObservabilityBuilderExt for CoreScenarioBuilder { let parsed = reqwest::Url::parse(url).expect("grafana url must be valid"); self.with_grafana_url(parsed) } + + fn try_with_grafana_url_str( + self, + url: &str, + ) -> Result, BuilderInputError> { + let parsed = reqwest::Url::parse(url).map_err(|err| BuilderInputError::InvalidUrl { + field: "grafana_url", + value: url.to_string(), + message: err.to_string(), + })?; + Ok(self.with_grafana_url(parsed)) + } } /// Builder for transaction workloads. @@ -279,6 +383,16 @@ impl TransactionFlowBuilder { self } + /// Like `rate`, but returns an error instead of panicking. + pub fn try_rate(self, rate: u64) -> Result { + let Some(rate) = NonZeroU64::new(rate) else { + return Err(BuilderInputError::ZeroValue { + field: "transaction_rate", + }); + }; + Ok(self.rate_per_block(rate)) + } + #[must_use] /// Set transaction submission rate per block. pub const fn rate_per_block(mut self, rate: NonZeroU64) -> Self { @@ -296,6 +410,17 @@ impl TransactionFlowBuilder { self } + /// Like `users`, but returns an error instead of panicking. + pub fn try_users(mut self, users: usize) -> Result { + let Some(value) = NonZeroUsize::new(users) else { + return Err(BuilderInputError::ZeroValue { + field: "transaction_users", + }); + }; + self.users = Some(value); + Ok(self) + } + #[must_use] /// Attach the transaction workload to the scenario. pub fn apply(mut self) -> CoreScenarioBuilder { @@ -347,6 +472,16 @@ impl DataAvailabilityFlowBuilder { self } + /// Like `channel_rate`, but returns an error instead of panicking. + pub fn try_channel_rate(self, rate: u64) -> Result { + let Some(rate) = NonZeroU64::new(rate) else { + return Err(BuilderInputError::ZeroValue { + field: "da_channel_rate", + }); + }; + Ok(self.channel_rate_per_block(rate)) + } + #[must_use] /// Set the number of DA channels to run. pub const fn channel_rate_per_block(mut self, rate: NonZeroU64) -> Self { @@ -361,6 +496,16 @@ impl DataAvailabilityFlowBuilder { self } + /// Like `blob_rate`, but returns an error instead of panicking. + pub fn try_blob_rate(self, rate: u64) -> Result { + let Some(rate) = NonZeroU64::new(rate) else { + return Err(BuilderInputError::ZeroValue { + field: "da_blob_rate", + }); + }; + Ok(self.blob_rate_per_block(rate)) + } + #[must_use] /// Set blob publish rate per block. pub const fn blob_rate_per_block(mut self, rate: NonZeroU64) -> Self {