workflows: add try_* builder APIs for URLs and rates

This commit is contained in:
andrussal 2025-12-18 14:53:08 +01:00
parent f0e9d2807b
commit 8a6d7236ef

View File

@ -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<ObservabilityCapability>;
/// Like `with_metrics_query_url_str`, but returns an error instead of
/// panicking.
fn try_with_metrics_query_url_str(
self,
url: &str,
) -> Result<CoreScenarioBuilder<ObservabilityCapability>, 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<ObservabilityCapability>;
/// 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<CoreScenarioBuilder<ObservabilityCapability>, BuilderInputError>;
/// Optional Grafana base URL for printing/logging (human access).
fn with_grafana_url(self, url: reqwest::Url) -> CoreScenarioBuilder<ObservabilityCapability>;
/// Convenience wrapper that parses a URL string (panics if invalid).
fn with_grafana_url_str(self, url: &str) -> CoreScenarioBuilder<ObservabilityCapability>;
/// Like `with_grafana_url_str`, but returns an error instead of panicking.
fn try_with_grafana_url_str(
self,
url: &str,
) -> Result<CoreScenarioBuilder<ObservabilityCapability>, 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<CoreScenarioBuilder<ObservabilityCapability>, 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<CoreScenarioBuilder<ObservabilityCapability>, 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<ObservabilityCapability> {
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<CoreScenarioBuilder<ObservabilityCapability>, 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<ObservabilityCapability> {
@ -222,6 +290,18 @@ impl ObservabilityBuilderExt for CoreScenarioBuilder<ObservabilityCapability> {
self.with_metrics_query_url(parsed)
}
fn try_with_metrics_query_url_str(
self,
url: &str,
) -> Result<CoreScenarioBuilder<ObservabilityCapability>, 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<ObservabilityCapability> {
self.with_metrics_otlp_ingest_url(parsed)
}
fn try_with_metrics_otlp_ingest_url_str(
self,
url: &str,
) -> Result<CoreScenarioBuilder<ObservabilityCapability>, 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<ObservabilityCapability> {
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<CoreScenarioBuilder<ObservabilityCapability>, 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<Caps> TransactionFlowBuilder<Caps> {
self
}
/// Like `rate`, but returns an error instead of panicking.
pub fn try_rate(self, rate: u64) -> Result<Self, BuilderInputError> {
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<Caps> TransactionFlowBuilder<Caps> {
self
}
/// Like `users`, but returns an error instead of panicking.
pub fn try_users(mut self, users: usize) -> Result<Self, BuilderInputError> {
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<Caps> {
@ -347,6 +472,16 @@ impl<Caps> DataAvailabilityFlowBuilder<Caps> {
self
}
/// Like `channel_rate`, but returns an error instead of panicking.
pub fn try_channel_rate(self, rate: u64) -> Result<Self, BuilderInputError> {
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<Caps> DataAvailabilityFlowBuilder<Caps> {
self
}
/// Like `blob_rate`, but returns an error instead of panicking.
pub fn try_blob_rate(self, rate: u64) -> Result<Self, BuilderInputError> {
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 {