Skip to content

Commit 521f9b9

Browse files
committed
feat(cli): add tower-resilience integration framework
Implements Phase 1 of issue #446 - Tower resilience patterns infrastructure ## Changes ### Core Infrastructure - Add tower-resilience dependency with circuitbreaker, retry, and ratelimiter features - Create resilience.rs module with framework for wrapping API clients - Add ResilienceConfig to profile configuration structure - Add CLI flags for controlling resilience patterns ### CLI Flags - --no-resilience: Disable all resilience patterns - --no-circuit-breaker: Disable circuit breaker only - --no-retry: Disable retry only - --retry-attempts N: Override retry attempts - --rate-limit N: Set rate limit (requests per minute) ### Configuration - Profiles can now include resilience configuration - Support for circuit breaker, retry, and rate limiting settings - Configurable failure thresholds, timeouts, and backoff strategies ## Implementation Status - Infrastructure and configuration: ✅ Complete - Tower middleware integration: 🚧 TODO (awaiting stable tower-resilience API) - The wrap_cloud_client() and wrap_enterprise_client() functions currently pass through the service unchanged until tower-resilience API stabilizes ## Testing - Added unit tests for configuration defaults and CLI overrides - All existing tests pass - Clippy and fmt checks pass Next steps will implement the actual middleware once tower-resilience API is stable.
1 parent ce1c8a5 commit 521f9b9

File tree

11 files changed

+613
-6
lines changed

11 files changed

+613
-6
lines changed

Cargo.lock

Lines changed: 69 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/redisctl-config/src/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ pub struct Profile {
4444
/// Supports keyring: prefix for secure storage.
4545
#[serde(default, skip_serializing_if = "Option::is_none")]
4646
pub files_api_key: Option<String>,
47+
/// Resilience configuration for this profile
48+
#[serde(default, skip_serializing_if = "Option::is_none")]
49+
pub resilience: Option<crate::ResilienceConfig>,
4750
}
4851

4952
/// Supported deployment types
@@ -460,6 +463,7 @@ mod tests {
460463
api_url: "https://api.redislabs.com/v1".to_string(),
461464
},
462465
files_api_key: None,
466+
resilience: None,
463467
};
464468

465469
config.set_profile("test".to_string(), cloud_profile);
@@ -482,6 +486,7 @@ mod tests {
482486
api_url: "url".to_string(),
483487
},
484488
files_api_key: None,
489+
resilience: None,
485490
};
486491

487492
let (key, secret, url) = cloud_profile.cloud_credentials().unwrap();
@@ -616,6 +621,7 @@ api_url = "${REDIS_TEST_URL:-https://api.redislabs.com/v1}"
616621
insecure: false,
617622
},
618623
files_api_key: None,
624+
resilience: None,
619625
};
620626
config.set_profile("ent1".to_string(), enterprise_profile);
621627

@@ -646,6 +652,7 @@ api_url = "${REDIS_TEST_URL:-https://api.redislabs.com/v1}"
646652
api_url: "https://api.redislabs.com/v1".to_string(),
647653
},
648654
files_api_key: None,
655+
resilience: None,
649656
};
650657
config.set_profile("cloud1".to_string(), cloud_profile);
651658

@@ -676,6 +683,7 @@ api_url = "${REDIS_TEST_URL:-https://api.redislabs.com/v1}"
676683
api_url: "https://api.redislabs.com/v1".to_string(),
677684
},
678685
files_api_key: None,
686+
resilience: None,
679687
};
680688
config.set_profile("cloud1".to_string(), cloud_profile.clone());
681689
config.set_profile("cloud2".to_string(), cloud_profile);
@@ -690,6 +698,7 @@ api_url = "${REDIS_TEST_URL:-https://api.redislabs.com/v1}"
690698
insecure: false,
691699
},
692700
files_api_key: None,
701+
resilience: None,
693702
};
694703
config.set_profile("ent1".to_string(), enterprise_profile.clone());
695704
config.set_profile("ent2".to_string(), enterprise_profile);
@@ -729,6 +738,7 @@ api_url = "${REDIS_TEST_URL:-https://api.redislabs.com/v1}"
729738
api_url: "https://api.redislabs.com/v1".to_string(),
730739
},
731740
files_api_key: None,
741+
resilience: None,
732742
};
733743
config.set_profile("cloud1".to_string(), cloud_profile);
734744

crates/redisctl-config/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
//! api_url: "https://api.redislabs.com/v1".to_string(),
3636
//! },
3737
//! files_api_key: None,
38+
//! resilience: None,
3839
//! };
3940
//!
4041
//! let mut config = Config::default();
@@ -44,8 +45,10 @@
4445
pub mod config;
4546
pub mod credential;
4647
pub mod error;
48+
pub mod resilience;
4749

4850
// Re-export main types for convenience
4951
pub use config::{Config, DeploymentType, Profile, ProfileCredentials};
5052
pub use credential::{CredentialStorage, CredentialStore};
5153
pub use error::{ConfigError, Result};
54+
pub use resilience::ResilienceConfig;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! Resilience configuration for API clients
2+
//!
3+
//! This module defines configuration structures for resilience patterns
4+
//! (circuit breaker, retry, rate limiting) that can be stored in profiles.
5+
6+
use serde::{Deserialize, Serialize};
7+
8+
/// Configuration for resilience patterns
9+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10+
pub struct ResilienceConfig {
11+
/// Circuit breaker configuration
12+
#[serde(default)]
13+
pub circuit_breaker: CircuitBreakerConfig,
14+
15+
/// Retry configuration
16+
#[serde(default)]
17+
pub retry: RetryConfig,
18+
19+
/// Rate limiting configuration
20+
#[serde(default)]
21+
pub rate_limit: RateLimitConfig,
22+
}
23+
24+
/// Circuit breaker configuration
25+
#[derive(Debug, Clone, Serialize, Deserialize)]
26+
pub struct CircuitBreakerConfig {
27+
/// Whether circuit breaker is enabled
28+
#[serde(default = "default_true")]
29+
pub enabled: bool,
30+
31+
/// Failure rate threshold (0.0 to 1.0) to open the circuit
32+
#[serde(default = "default_failure_threshold")]
33+
pub failure_threshold: f32,
34+
35+
/// Number of calls to track in the sliding window
36+
#[serde(default = "default_window_size")]
37+
pub window_size: u32,
38+
39+
/// Duration in seconds to wait before attempting to close the circuit
40+
#[serde(default = "default_reset_timeout")]
41+
pub reset_timeout_secs: u64,
42+
}
43+
44+
impl Default for CircuitBreakerConfig {
45+
fn default() -> Self {
46+
Self {
47+
enabled: true,
48+
failure_threshold: 0.5,
49+
window_size: 20,
50+
reset_timeout_secs: 60,
51+
}
52+
}
53+
}
54+
55+
/// Retry configuration
56+
#[derive(Debug, Clone, Serialize, Deserialize)]
57+
pub struct RetryConfig {
58+
/// Whether retry is enabled
59+
#[serde(default = "default_true")]
60+
pub enabled: bool,
61+
62+
/// Maximum number of retry attempts
63+
#[serde(default = "default_max_attempts")]
64+
pub max_attempts: u32,
65+
66+
/// Initial backoff in milliseconds
67+
#[serde(default = "default_backoff_ms")]
68+
pub backoff_ms: u64,
69+
70+
/// Maximum backoff in milliseconds
71+
#[serde(default = "default_max_backoff_ms")]
72+
pub max_backoff_ms: u64,
73+
}
74+
75+
impl Default for RetryConfig {
76+
fn default() -> Self {
77+
Self {
78+
enabled: true,
79+
max_attempts: 3,
80+
backoff_ms: 100,
81+
max_backoff_ms: 5000,
82+
}
83+
}
84+
}
85+
86+
/// Rate limiting configuration
87+
#[derive(Debug, Clone, Serialize, Deserialize)]
88+
pub struct RateLimitConfig {
89+
/// Whether rate limiting is enabled
90+
#[serde(default)]
91+
pub enabled: bool,
92+
93+
/// Maximum requests per minute
94+
#[serde(default = "default_requests_per_minute")]
95+
pub requests_per_minute: u32,
96+
}
97+
98+
impl Default for RateLimitConfig {
99+
fn default() -> Self {
100+
Self {
101+
enabled: false,
102+
requests_per_minute: 100,
103+
}
104+
}
105+
}
106+
107+
// Default value functions for serde
108+
fn default_true() -> bool {
109+
true
110+
}
111+
112+
fn default_failure_threshold() -> f32 {
113+
0.5
114+
}
115+
116+
fn default_window_size() -> u32 {
117+
20
118+
}
119+
120+
fn default_reset_timeout() -> u64 {
121+
60
122+
}
123+
124+
fn default_max_attempts() -> u32 {
125+
3
126+
}
127+
128+
fn default_backoff_ms() -> u64 {
129+
100
130+
}
131+
132+
fn default_max_backoff_ms() -> u64 {
133+
5000
134+
}
135+
136+
fn default_requests_per_minute() -> u32 {
137+
100
138+
}

crates/redisctl/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ path = "src/main.rs"
1919

2020
[dependencies]
2121
redisctl-config = { version = "0.1.1", path = "../redisctl-config" }
22-
redis-cloud = { version = "0.7.1", path = "../redis-cloud" }
23-
redis-enterprise = { version = "0.6.4", path = "../redis-enterprise" }
22+
redis-cloud = { version = "0.7.1", path = "../redis-cloud", features = ["tower-integration"] }
23+
redis-enterprise = { version = "0.6.4", path = "../redis-enterprise", features = ["tower-integration"] }
2424
files-sdk = { workspace = true, optional = true }
2525

2626
# CLI dependencies
@@ -56,6 +56,10 @@ config = { workspace = true }
5656
# Keyring for Files.com API key storage (separate from profile credentials)
5757
keyring = { version = "3.6", optional = true, features = ["apple-native", "windows-native", "linux-native"] }
5858

59+
# Tower and resilience patterns
60+
tower = { version = "0.5", features = ["util", "timeout", "buffer", "ready-cache"] }
61+
tower-resilience = { version = "0.1", features = ["circuitbreaker", "retry", "ratelimiter"] }
62+
5963
[target.'cfg(unix)'.dependencies]
6064
pager = "0.16"
6165

crates/redisctl/src/cli/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ pub struct Cli {
6969
#[arg(long, short, global = true, action = clap::ArgAction::Count)]
7070
pub verbose: u8,
7171

72+
/// Disable all resilience patterns (circuit breaker, retry, rate limiting)
73+
#[arg(long, global = true)]
74+
pub no_resilience: bool,
75+
76+
/// Disable circuit breaker only
77+
#[arg(long, global = true)]
78+
pub no_circuit_breaker: bool,
79+
80+
/// Disable retry only
81+
#[arg(long, global = true)]
82+
pub no_retry: bool,
83+
84+
/// Override retry attempts (implies --retry-enabled if set)
85+
#[arg(long, global = true)]
86+
pub retry_attempts: Option<u32>,
87+
88+
/// Set rate limit (requests per minute, implies --rate-limit-enabled if set)
89+
#[arg(long, global = true)]
90+
pub rate_limit: Option<u32>,
91+
7292
#[command(subcommand)]
7393
pub command: Commands,
7494
}

0 commit comments

Comments
 (0)