-
Notifications
You must be signed in to change notification settings - Fork 7
feat: introduce the recoverable crate #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3d1a6b3
5def248
8057c25
4eaa666
ae0bf19
766ca83
93f27c7
d78f9b1
3f57aa1
d32c546
965356f
673b41e
c46f556
0b776e8
56d10c3
fc6e8c8
088ba4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Changelog |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| [package] | ||
| name = "recoverable" | ||
| description = "Recovery information and classification for resilience patterns" | ||
| version = "0.1.0" | ||
| readme = "README.md" | ||
| keywords = ["oxidizer", "resilience", "metadata", "classification", "error"] | ||
| categories = ["data-structures"] | ||
|
|
||
| edition.workspace = true | ||
| rust-version.workspace = true | ||
| authors.workspace = true | ||
| license-file.workspace = true | ||
| homepage.workspace = true | ||
| repository.workspace = true | ||
|
|
||
| [dependencies] | ||
|
|
||
| [dev-dependencies] | ||
| static_assertions.workspace = true | ||
|
|
||
| [lints] | ||
| workspace = true | ||
|
|
||
| [[example]] | ||
| name = "recoverable_error" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| <div align="center"> | ||
| <img src="./logo.png" alt="Recoverable Logo" width="128"> | ||
|
|
||
| # Recoverable | ||
|
|
||
| [](https://crates.io/crates/recoverable) | ||
| [](https://docs.rs/recoverable) | ||
| [](https://github.com/microsoft/oxidizer/actions) | ||
| [](https://codecov.io/gh/microsoft/oxidizer) | ||
| [](../LICENSE) | ||
|
|
||
| </div> | ||
|
|
||
| - [Summary](#summary) | ||
| - [Core Types](#core-types) | ||
| - [Examples](#examples) | ||
|
|
||
| ## Summary | ||
|
|
||
| <!-- cargo-rdme start --> | ||
|
|
||
| Recovery information and classification for resilience patterns. | ||
|
|
||
| This crate provides types for classifying conditions based on their **recoverability state**, | ||
| enabling consistent recovery behavior across different error types and resilience middleware. | ||
|
|
||
| The recovery information describes whether recovering from an operation might help, not whether | ||
| the operation succeeded or failed. Both successful operations and permanent failures | ||
| should use [`RecoveryInfo::never`](https://docs.rs/recoverable/latest/recoverable/struct.RecoveryInfo.html#method.never) since recovery won't change the outcome. | ||
|
|
||
| ## Core Types | ||
|
|
||
| - [`RecoveryInfo`](https://docs.rs/recoverable/latest/recoverable/struct.RecoveryInfo.html): Classifies conditions as recoverable (transient) or non-recoverable (permanent/successful). | ||
| - [`Recoverable`](https://docs.rs/recoverable/latest/recoverable/trait.Recoverable.html): A trait for types that can determine their recoverability. | ||
| - [`RecoveryKind`](https://docs.rs/recoverable/latest/recoverable/enum.RecoveryKind.html): An enum representing the kind of recovery that can be attempted. | ||
|
|
||
| ## Examples | ||
|
|
||
| ```rust | ||
| use recoverable::{Recoverable, RecoveryInfo, RecoveryKind}; | ||
|
|
||
| #[derive(Debug)] | ||
| enum DatabaseError { | ||
| ConnectionTimeout, | ||
| InvalidCredentials, | ||
| TableNotFound, | ||
| } | ||
|
|
||
| impl Recoverable for DatabaseError { | ||
| fn recovery(&self) -> RecoveryInfo { | ||
| match self { | ||
| // Transient failure - might succeed if retried | ||
| DatabaseError::ConnectionTimeout => RecoveryInfo::retry(), | ||
| // Permanent failures - retrying won't help | ||
| DatabaseError::InvalidCredentials => RecoveryInfo::never(), | ||
| DatabaseError::TableNotFound => RecoveryInfo::never(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let error = DatabaseError::ConnectionTimeout; | ||
| assert_eq!(error.recovery().kind(), RecoveryKind::Retry); | ||
|
|
||
| // For successful operations, also use never() since retry is unnecessary | ||
| let success_result: Result<(), DatabaseError> = Ok(()); | ||
| // If we had a wrapper type for success, it would also return RecoveryInfo::never() | ||
| ``` | ||
|
|
||
| <!-- cargo-rdme end --> | ||
|
|
||
| <div style="font-size: 75%" ><hr/> | ||
|
|
||
| This crate was developed as part of [The Oxidizer Project](https://github.com/microsoft/oxidizer). | ||
|
|
||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! Example demonstrating how to use the Recoverable trait with error types. | ||
| //! | ||
| //! This example shows how to implement the Recoverable trait for custom error types | ||
| //! and use `RecoveryInfo` to classify errors as transient or permanent. | ||
|
|
||
| use std::error::Error; | ||
| use std::fmt::Display; | ||
| use std::time::Duration; | ||
|
|
||
| use recoverable::{Recoverable, RecoveryInfo, RecoveryKind}; | ||
|
|
||
| fn main() { | ||
| handle_network_error(&NetworkError::DnsResolutionFailed); | ||
| handle_network_error(&NetworkError::InvalidUrl); | ||
| handle_network_error(&NetworkError::ServiceUnavailable { retry_after: None }); | ||
| } | ||
|
|
||
| /// A network error type demonstrating different recovery scenarios. | ||
| #[derive(Debug)] | ||
| enum NetworkError { | ||
| /// DNS resolution failed - might be transient | ||
| DnsResolutionFailed, | ||
| /// Invalid URL format - permanent error | ||
| InvalidUrl, | ||
| /// Service is unavailable, for example circuit breaker is open | ||
| ServiceUnavailable { retry_after: Option<Duration> }, | ||
| } | ||
|
|
||
| impl Recoverable for NetworkError { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @geeknoid Would you agree with the rename?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I don't think it's right. This doesn't do anything about recovery, it merely says that an error can be recovered from. When I see "recovery", I think "action", this IS the recovery to the problem.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This trait only tells that particular error or result is recoverable but does not nor it is able to do the recovery action. It's up to the caller to do the action itself based on the conditions. For example, caller might decide ahead of time that particular action can be retried and he needs to put aside all information/parameters required to do that recovery action. This trait only gives the information that such recovery is possible. Our names should reflect that. With this context in mind, I find the |
||
| fn recovery(&self) -> RecoveryInfo { | ||
| match self { | ||
| Self::DnsResolutionFailed => RecoveryInfo::retry(), | ||
| Self::InvalidUrl => RecoveryInfo::never(), | ||
| Self::ServiceUnavailable { retry_after: Some(after) } => RecoveryInfo::unavailable().delay(*after), | ||
| Self::ServiceUnavailable { retry_after: None } => RecoveryInfo::unavailable(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Display for NetworkError { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| match self { | ||
| Self::DnsResolutionFailed => write!(f, "DNS resolution failed"), | ||
| Self::InvalidUrl => write!(f, "invalid URL format"), | ||
| Self::ServiceUnavailable { retry_after } => { | ||
| if let Some(after) = retry_after { | ||
| write!(f, "service unavailable, retry after {after:?}") | ||
| } else { | ||
| write!(f, "service unavailable") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Error for NetworkError {} | ||
|
|
||
| /// Demonstrates handling network errors. | ||
| fn handle_network_error(error: &NetworkError) { | ||
| let recovery = error.recovery(); | ||
|
|
||
| println!("\nError: {error}"); | ||
| println!("Recovery strategy: {recovery}"); | ||
|
|
||
| match recovery.kind() { | ||
| RecoveryKind::Retry => println!("→ transient network issue, retry recommended"), | ||
| RecoveryKind::Unavailable => println!("→ service appears to be down"), | ||
| RecoveryKind::Never => println!("→ configuration or code change needed"), | ||
| RecoveryKind::Unknown => println!("→ unknown recovery status"), | ||
| _ => println!("→ unhandled recovery kind"), | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.