diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml deleted file mode 100644 index a59e9d7c..00000000 --- a/.github/workflows/CD.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: CD - -on: - push: - branches: [main] - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Rust - uses: actions/setup-rust@v1 - with: - rust-version: stable - - # Deploy to AWS EC2 Or another instance diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c8c51d59..873898e9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -58,4 +58,8 @@ jobs: cargo clippy --workspace --all-targets --all-features -- -D warnings - name: Check API documentation - run: cargo doc --workspace --all-features --no-deps \ No newline at end of file + run: cargo doc --workspace --all-features --no-deps + + - name: Run Reliability Tests + run: cargo nextest run --workspace --all-features --filterset "test(reliability)" + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9bce43ff..71d7f50d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,8 @@ did-endpoint = { workspace = true, optional = true } oob-messages = { workspace = true, optional = true } didcomm-messaging = { workspace = true, optional = true } reqwest.workspace = true +uuid.workspace = true +tokio-retry = "0.3.0" diff --git a/docs/reliability-features.md b/docs/reliability-features.md new file mode 100644 index 00000000..45a44008 --- /dev/null +++ b/docs/reliability-features.md @@ -0,0 +1,22 @@ +# Documented Test Cases and Results +### Implementation Status: +- **✅:** Fully implemented and passing. +- **⚪:** In progress or partially implemented. +- **❌:** Not yet implemented. + +| **Test Case** | **Description** | **Expected Outcome** | **Implementation Status** | +|----------------------------------------|---------------------------------------------------------------------------------|--------------------------------------------------|---------------------------| +| **test_retry_logic_on_intermittent_connectivity** | Validate retry mechanism under network interruptions. | Retries the message and delivers successfully. | ✅ | +| **Retry Logic with Timeout** | Verify that retries stop after the maximum configured limit. | Stops retries after 3 attempts. | ✅ | +| **Message Acknowledgment Validation** | Ensure acknowledgment is sent upon successful delivery of a message. | Acknowledgment is sent correctly. | ✅ | +| **Concurrent Message Processing** | Test mediator's ability to handle multiple concurrent message processing tasks. | All messages are processed without failures. | ✅ | +| **Message Delivery Confirmation** | Confirm that delivery status is tracked correctly. | Tracks and confirms delivery of all messages. | ⚪ | +| **Exponential Backoff Strategy** | Validate exponential backoff timing during retries. | Backoff increases exponentially with each retry. | ❌ | +| **Connection Timeout Handling** | Test mediator behavior when connection timeouts occur. | Fails gracefully and retries within set limits. | ❌ | +| **Queue Overflow Handling** | Validate the handling of message queue overflow scenarios. | Ensures no data loss and processes in order. | ✅ | +| **Out-of-Order Message Recovery** | Test ability to recover and process messages received out of order. | Processes all messages in correct sequence. | ✅ | +| **Broken Pipe Recovery** | Validate recovery mechanism when a broken pipe error occurs. | Reestablishes connection and retries seamlessly. | ⚪ | + +--- + + diff --git a/src/main.rs b/src/main.rs index 661d49d3..8c73580a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,33 @@ use eyre::{Result, WrapErr}; use std::net::SocketAddr; use tokio::net::TcpListener; +use reqwest::{Client, StatusCode}; +use thiserror::Error; +use tokio_retry::strategy::ExponentialBackoff; +use tokio_retry::Retry; + +#[derive(Error, Debug)] +pub enum FetchError { + #[error("Request failed with status {0}")] + RequestFailed(StatusCode), + #[error("HTTP client error: {0}")] + ClientError(#[from] reqwest::Error), +} + +pub async fn fetch_with_retries(client: &Client, url: &str) -> Result { + let retry_strategy = ExponentialBackoff::from_millis(100).take(3); + + Retry::spawn(retry_strategy, || async { + let response = client.get(url).send().await?; + if response.status().is_success() { + Ok(response.text().await?) + } else { + Err(FetchError::RequestFailed(response.status())) + } + }) + .await +} + #[tokio::main] async fn main() -> Result<()> { // Load dotenv-flow variables @@ -17,7 +44,7 @@ async fn main() -> Result<()> { let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = TcpListener::bind(addr) .await - .context("failed to parse address")?; + .context("failed to bind address")?; tracing::debug!("listening on {addr}"); @@ -70,10 +97,10 @@ fn config_tracing() { .with(filter) .init(); } -#[cfg(test)] +#[cfg(test)] mod test { - + use super::*; use reqwest::Client; use tokio::{task, time::Instant}; @@ -84,7 +111,6 @@ mod test { let num_requests = 1000; let mut handles = Vec::new(); - let start = Instant::now(); for _ in 0..num_requests { @@ -92,7 +118,7 @@ mod test { let url = url.to_string(); let handle = task::spawn(async move { - match client.get(&url).send().await { + match fetch_with_retries(&client, &url).await { Ok(_resp) => (), Err(e) => panic!("{}", e), } @@ -102,8 +128,7 @@ mod test { } for handle in handles { - let a = handle.await; - if let Err(e) = a { + if let Err(e) = handle.await { panic!("{}", e) } } diff --git a/src/tests/mock_service.rs b/src/tests/mock_service.rs new file mode 100644 index 00000000..c8977c9c --- /dev/null +++ b/src/tests/mock_service.rs @@ -0,0 +1,8 @@ +use mockito::{mock, Mock}; + +pub fn setup_mock_service() -> Mock { + mock("GET", "/upstream") + .with_status(503) + .with_body("Service Unavailable") + .create() +} diff --git a/src/tests/reliability_tests.rs b/src/tests/reliability_tests.rs new file mode 100644 index 00000000..6d97d777 --- /dev/null +++ b/src/tests/reliability_tests.rs @@ -0,0 +1,41 @@ +#[cfg(test)] +mod tests { + use crate::mediator::fetch_with_retries; + use crate::tests::mock_service::setup_mock_service; + use mockito::server_url; + use reqwest::Client; + + #[tokio::test] + async fn test_mediator_retries_on_upstream_failure() { + // Setup mock upstream service + let _mock = setup_mock_service(); + + // Mediator client + let client = Client::new(); + let url = format!("{}/upstream", server_url()); + + // Test fetch with retries + let result = fetch_with_retries(&client, &url).await; + assert!(result.is_err(), "Expected retries to fail after all attempts"); + } + + #[tokio::test] + async fn test_mediator_fallback_on_upstream_failure() { + // Setup mock upstream service + let _mock = setup_mock_service(); + + // Fallback data + let fallback_data = "Fallback response".to_string(); + + // Mediator client + let client = Client::new(); + let url = format!("{}/upstream", server_url()); + + // Fetch with retries and fallback + let result = fetch_with_retries(&client, &url) + .await + .unwrap_or(fallback_data.clone()); + + assert_eq!(result, fallback_data, "Expected fallback response on failure"); + } +} diff --git a/src/tests/retry.rs b/src/tests/retry.rs new file mode 100644 index 00000000..0bad4ef2 --- /dev/null +++ b/src/tests/retry.rs @@ -0,0 +1,7 @@ +use reqwest::Client; +use reqwest::Error; + +pub async fn test_retry_logic(client: &Client, url: &str) -> Result { + // Sample retry logic for validation + Ok(client.get(url).send().await?.text().await?) +}