#![allow(non_snake_case)]
use anyhow::anyhow;
use clap::{
ArgGroup,
ValueEnum,
};
use fuel_core::service::config::{
RedisLeaderLockConfig,
Trigger as PoATrigger,
};
use humantime::Duration;
#[derive(Debug, Clone, clap::Args)]
pub struct PoATriggerArgs {
#[clap(flatten)]
instant: Instant,
#[clap(flatten)]
interval: Interval,
#[clap(flatten)]
open: Open,
#[clap(flatten)]
leader_lock: LeaderLock,
}
impl PoATriggerArgs {
pub fn leader_lock(&self) -> anyhow::Result<Option<RedisLeaderLockConfig>> {
let LeaderLock {
enabled,
redis_urls,
lease_key,
lease_ttl,
node_timeout,
retry_delay,
max_retry_delay_offset,
max_attempts,
stream_max_len,
quorum_disruption_budget,
} = self.leader_lock.clone();
if enabled {
Ok(Some(RedisLeaderLockConfig {
redis_urls: redis_urls
.ok_or(anyhow!("`redis_urls` is required when `enabled` is true"))?,
lease_key: lease_key
.ok_or(anyhow!("`lease_key` is required when `enabled` is true"))?,
lease_ttl: lease_ttl
.ok_or(anyhow!("`lease_ttl` is required when `enabled` is true"))?
.into(),
node_timeout: node_timeout.into(),
retry_delay: retry_delay.into(),
max_retry_delay_offset: max_retry_delay_offset.into(),
max_attempts,
stream_max_len,
quorum_disruption_budget,
}))
} else {
Ok(None)
}
}
}
impl From<PoATriggerArgs> for PoATrigger {
fn from(value: PoATriggerArgs) -> Self {
match value {
PoATriggerArgs {
open: Open { period: Some(p) },
..
} => PoATrigger::Open { period: p.into() },
PoATriggerArgs {
interval:
Interval {
block_time: Some(p),
},
..
} => PoATrigger::Interval {
block_time: p.into(),
},
PoATriggerArgs { instant, .. } if instant.instant == Boolean::True => {
PoATrigger::Instant
}
_ => PoATrigger::Never,
}
}
}
#[derive(Debug, Clone, clap::Args)]
#[clap(
group = ArgGroup::new("instant-mode").args(&["instant"]).conflicts_with_all(&["interval-mode", "open-mode"]),
)]
struct Instant {
#[arg(long = "poa-instant", default_value = "true", value_parser, env)]
instant: Boolean,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Boolean {
True,
False,
}
#[derive(Debug, Clone, clap::Args)]
#[clap(
group = ArgGroup::new("interval-mode").args(&["block_time"]).conflicts_with_all(&["instant-mode", "open-mode"]),
)]
struct Interval {
#[clap(long = "poa-interval-period", env)]
pub block_time: Option<Duration>,
}
#[derive(Debug, Clone, clap::Args)]
#[clap(
group = ArgGroup::new("open-mode").args(&["period"]).conflicts_with_all(&["instant-mode", "interval-mode"]),
)]
struct Open {
#[clap(long = "poa-open-period", env)]
pub period: Option<Duration>,
}
#[derive(Debug, Clone, clap::Args)]
struct LeaderLock {
#[clap(
long = "poa-leader-lock",
env,
default_value_t = false,
requires_all = ["redis_urls", "lease_key", "lease_ttl"]
)]
enabled: bool,
#[clap(long = "poa-leader-lock-redis-url", env, num_args = 1..)]
redis_urls: Option<Vec<String>>,
#[clap(long = "poa-leader-lock-key", env)]
lease_key: Option<String>,
#[clap(long = "poa-leader-lock-ttl", env)]
lease_ttl: Option<Duration>,
#[clap(long = "poa-leader-lock-node-timeout", env, default_value = "100ms")]
node_timeout: Duration,
#[clap(long = "poa-leader-lock-retry-delay", env, default_value = "200ms")]
retry_delay: Duration,
#[clap(
long = "poa-leader-lock-max-retry-delay-offset",
env,
default_value = "100ms"
)]
max_retry_delay_offset: Duration,
#[clap(long = "poa-leader-lock-max-attempts", env, default_value_t = 3)]
max_attempts: u32,
#[clap(long = "poa-leader-lock-stream-max-len", env, default_value_t = 1000)]
stream_max_len: u32,
#[clap(
long = "poa-leader-lock-quorum-disruption-budget",
env,
default_value_t = 0
)]
quorum_disruption_budget: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use fuel_core::service::config::Trigger;
use std::time::Duration as StdDuration;
use test_case::test_case;
#[derive(Debug, Clone, Parser)]
pub struct Command {
#[clap(flatten)]
trigger: PoATriggerArgs,
}
#[test_case(&[] => Ok(Trigger::Instant); "defaults to instant trigger")]
#[test_case(&["", "--poa-instant=false"] => Ok(Trigger::Never); "never trigger if instant is explicitly disabled")]
#[test_case(&["", "--poa-interval-period=1s"] => Ok(Trigger::Interval { block_time: StdDuration::from_secs(1)}); "uses interval mode if set")]
#[test_case(&["", "--poa-open-period=1s"] => Ok(Trigger::Open { period: StdDuration::from_secs(1)}); "uses open mode if set")]
#[test_case(&["", "--poa-instant=true", "--poa-interval-period=1s"] => Err(()); "can't set interval and instant at the same time")]
#[test_case(&["", "--poa-open-period=1s", "--poa-interval-period=1s"] => Err(()); "can't set open and interval at the same time")]
fn parse(args: &[&str]) -> Result<Trigger, ()> {
Command::try_parse_from(args)
.map_err(|_| ())
.map(|c| c.trigger.into())
}
#[test]
fn leader_lock__defaults_to_none() {
let command = Command::try_parse_from([""]).unwrap();
assert!(command.trigger.clone().leader_lock().unwrap().is_none());
}
#[test]
fn leader_lock__when_all_args_set_then_some() {
let command = Command::try_parse_from([
"",
"--poa-leader-lock",
"--poa-leader-lock-redis-url",
"redis://127.0.0.1:6379/",
"redis://127.0.0.1:6380/",
"--poa-leader-lock-key",
"poa:leader:lock",
"--poa-leader-lock-ttl",
"2s",
])
.unwrap();
let leader_lock = command.trigger.clone().leader_lock().unwrap().unwrap();
assert_eq!(
leader_lock.redis_urls,
vec![
"redis://127.0.0.1:6379/".to_string(),
"redis://127.0.0.1:6380/".to_string()
]
);
assert_eq!(leader_lock.lease_key, "poa:leader:lock");
assert_eq!(leader_lock.lease_ttl, StdDuration::from_secs(2));
assert_eq!(leader_lock.node_timeout, StdDuration::from_millis(100));
assert_eq!(leader_lock.retry_delay, StdDuration::from_millis(200));
assert_eq!(
leader_lock.max_retry_delay_offset,
StdDuration::from_millis(100)
);
assert_eq!(leader_lock.max_attempts, 3);
assert_eq!(leader_lock.stream_max_len, 1000);
assert_eq!(leader_lock.quorum_disruption_budget, 0);
}
#[test]
fn leader_lock__when_enabled_without_required_fields_then_parse_error() {
let result = Command::try_parse_from(["", "--poa-leader-lock"]);
assert!(result.is_err());
}
#[test]
fn leader_lock__when_disruption_budget_is_set_then_config_uses_value() {
let command = Command::try_parse_from([
"",
"--poa-leader-lock",
"--poa-leader-lock-redis-url",
"redis://127.0.0.1:6379/",
"--poa-leader-lock-key",
"poa:leader:lock",
"--poa-leader-lock-ttl",
"2s",
"--poa-leader-lock-quorum-disruption-budget",
"2",
])
.unwrap();
let leader_lock = command.trigger.clone().leader_lock().unwrap().unwrap();
assert_eq!(leader_lock.quorum_disruption_budget, 2);
}
}