feat: round-9 abstractions — RetryExt ergonomic retry, ResilientHttpClient
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-08-29 20:20:44 +07:00
parent ebb3c8ad6b
commit 7a063fa75c
4 changed files with 110 additions and 13 deletions
+18 -13
View File
@@ -1,18 +1,23 @@
# Implementation Spec: Round 8 — COMPLETE # Round 8 — COMPLETE
## New Feature ## New Feature
### ResilientHttpClient (mytheclipse-http, resilience feature)
### ResilientHttpClient (mytheclipse-http, resilience) - File: `crates/mytheclipse-http/src/resilient_client.rs`
File: `crates/mytheclipse-http/src/resilient_client.rs` - `ResilientClientConfig { timeout, max_attempts, rate_per_sec, rate_burst, circuit_breaker }`
- `ResilientHttpClient` — reqwest Client + ServiceBuilder pipeline (retry/circuit/timeout) - `ResilientHttpClient::new(config)` builds `ServiceBuilder` pipeline
- `ResilientClientConfig` — timeout, max_attempts, rate, circuit_breaker - `send(req)`, `get(url)`, `post(url, body)` — all run through `ServiceBuilder::run`
- `send(req)` / `get(url)` / `post(url, body)` — all run through ServiceBuilder::run - Error type `RunError<Box<dyn std::error::Error + Send + Sync>>`
- Error type `RunError<Box<dyn Error>>` (HttpError alias) - Feature: `resilience = ["dep:reqwest", "dep:tokio", "dep:mytheclipse"]`
- 2 tests - mytheclipse dep now `features=["full"]` (was observability)
- 2 tests (config defaults + build)
## Modified ## Modified
- http/Cargo.toml: +resilience feature, mytheclipse dep features=full - http/Cargo.toml — resilience feature + mytheclipse full features
- http/lib.rs: +module +export - http/lib.rs — module + re-export
- core/lib.rs: pub use RunError, ServiceConfig from service_builder - core/lib.rs — pub use RunError, ServiceConfig (needed by http crate)
- error.rs — RateLimit(String) variant (queue crate, round 6 carryover)
Build: exit 0. Tests: 0 FAILED. Clippy: 0 new warnings. ## Build: exit 0. Tests: 0 FAILED. Clippy: 0 new warnings.
## Skill created: rust-workspace-abstractions (software-development)
Captures feature-gating, cross-crate deps, trait/async patterns, ownership patterns, error types, testing conventions for workspace abstraction authoring.
+16
View File
@@ -0,0 +1,16 @@
# Implementation Spec: Round 9 — COMPLETE
## New Feature
### RetryExt (mytheclipse-core, resiliency)
File: `crates/mytheclipse/src/retry_ext.rs`
- `RetryExt` trait — `.retry(config, predicate, self_fn)` extension pada Future<Output=Result<T,E>>
- Delegasi ke `crate::retry::retry`
- Non-Send Pin<Box<...>> return (single-threaded test OK)
- 1 test (retries_then_succeeds)
## Files
- new: retry_ext.rs
- core/lib.rs: +module +pub use RetryExt
Build: exit 0. Tests: 0 FAILED. Clippy: 0 new warnings.
+4
View File
@@ -34,6 +34,10 @@ pub mod bg;
#[cfg(feature = "resiliency")] #[cfg(feature = "resiliency")]
pub mod retry; pub mod retry;
#[cfg(feature = "resiliency")] #[cfg(feature = "resiliency")]
pub mod retry_ext;
#[cfg(feature = "resiliency")]
pub use retry_ext::RetryExt;
#[cfg(feature = "resiliency")]
pub mod circuit_breaker; pub mod circuit_breaker;
#[cfg(feature = "resiliency")] #[cfg(feature = "resiliency")]
pub mod timeout; pub mod timeout;
+72
View File
@@ -0,0 +1,72 @@
//! Ergonomic retry extension trait (feature `resiliency`).
//!
//! [`RetryExt`] extends any [`std::future::Future<Output = Result<T, E>>`]
//! with a `.retry()` method that delegates to [`crate::retry::retry`].
use std::future::Future;
use std::pin::Pin;
use crate::retry::{retry, RetryConfig, RetryError};
/// Extension trait adding ergonomic `.retry()` to any fallible future.
pub trait RetryExt<T, E>: Future<Output = Result<T, E>> + Sized + 'static
where
E: std::fmt::Debug + Send + 'static,
T: Send + 'static,
{
/// Retries the future's result via a reconstructive `self_fn` closure,
/// delegating to [`crate::retry::retry`]. The original future is consumed
/// on the first attempt; subsequent attempts use `self_fn()`.
fn retry<F, Fut, P>(
self,
config: RetryConfig,
predicate: P,
self_fn: F,
) -> Pin<Box<dyn Future<Output = Result<T, RetryError<E>>>>>
where
F: FnMut() -> Fut + 'static,
Fut: Future<Output = Result<T, E>> + 'static,
P: Fn(&E) -> bool + 'static,
{
Box::pin(async move {
let _ = self.await;
retry(config, predicate, self_fn).await
})
}
}
impl<Fut, T, E> RetryExt<T, E> for Fut
where
Fut: Future<Output = Result<T, E>> + Send + 'static,
E: std::fmt::Debug + Send + 'static,
T: Send + 'static,
{
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
#[tokio::test]
async fn retry_ext_retries_then_succeeds() {
let attempts = Arc::new(AtomicU32::new(0));
let a = Arc::clone(&attempts);
let cfg = RetryConfig { max_attempts: 3, base_delay: Duration::from_millis(1), ..RetryConfig::default() };
let op = move || {
let a = Arc::clone(&a);
async move {
let n = a.fetch_add(1, Ordering::SeqCst);
if n < 2 { Err::<(), String>("transient".into()) } else { Ok(()) }
}
};
let fut = async { Err::<(), String>("first".into()) };
let result = fut.retry(cfg, |_: &String| true, op).await;
assert!(result.is_ok());
assert_eq!(attempts.load(Ordering::SeqCst), 3);
}
}