Skip to content

Commit 49580fc

Browse files
authored
feat: introduce the recoverable crate (#18)
Add a new `recoverable` crate that provides standardized types for classifying error conditions as recoverable or non-recoverable, enabling consistent retry behavior across different error types and resilience middleware. Core features: - `RecoveryInfo` type for classifying errors with recovery metadata - `Recoverable` trait for types that can determine their recoverability - `RecoveryKind` enum distinguishing between retry, outage, never, and unknown - Support for explicit retry delays via `delay()` method - Service outage detection with optional recovery hints
1 parent e3289c3 commit 49580fc

File tree

13 files changed

+892
-2
lines changed

13 files changed

+892
-2
lines changed

.spelling

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
294
1+
297
22
0.X.Y
33
100k
44
10k
@@ -22,6 +22,7 @@ Async
2222
auditable
2323
backend
2424
backends
25+
backoff
2526
backtrace
2627
backtraces
2728
Backtraces
@@ -149,6 +150,7 @@ metadata
149150
Metas
150151
Microservices
151152
microsoft.com
153+
middleware
152154
mimalloc
153155
Mimalloc
154156
miri
@@ -197,6 +199,8 @@ Proc
197199
profiler
198200
PullRequest
199201
RDME
202+
recoverability
203+
recoverable
200204
Redis
201205
reentrancy
202206
relocations
@@ -292,4 +296,4 @@ workflows
292296
workspace
293297
w.r.t.
294298
Xamarin
295-
xxH3
299+
xxH3

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Please see each crate's change log below:
1212
- [`fundle_macros_impl`](./crates/fundle_macros_impl/CHANGELOG.md)
1313
- [`ohno`](./crates/ohno/CHANGELOG.md)
1414
- [`ohno_macros`](./crates/ohno_macros/CHANGELOG.md)
15+
- [`recoverable`](./crates/recoverable/CHANGELOG.md)
1516
- [`thread_aware`](./crates/thread_aware/CHANGELOG.md)
1617
- [`thread_aware_macros`](./crates/thread_aware_macros/CHANGELOG.md)
1718
- [`thread_aware_macros_impl`](./crates/thread_aware_macros_impl/CHANGELOG.md)

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fundle_macros = { path = "crates/fundle_macros", default-features = false, versi
3434
fundle_macros_impl = { path = "crates/fundle_macros_impl", default-features = false, version = "0.3.0" }
3535
ohno = { path = "crates/ohno", default-features = false, version = "0.2.0" }
3636
ohno_macros = { path = "crates/ohno_macros", default-features = false, version = "0.2.0" }
37+
recoverable = { path = "crates/recoverable", default-features = false, version = "0.1.0" }
3738
testing_aids = { path = "crates/testing_aids", default-features = false }
3839
thread_aware = { path = "crates/thread_aware", default-features = false, version = "0.6.0" }
3940
thread_aware_macros = { path = "crates/thread_aware_macros", default-features = false, version = "0.6.0" }

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ These are the crates built out of this repo:
3434
- [`fundle_macros_impl`](crates/fundle_macros_impl/README.md) - Macros for the `fundle` crate.
3535
- [`ohno`](./crates/ohno/README.md) - High-quality Rust error handling.
3636
- [`ohno_macros`](./crates/ohno_macros/README.md) - Macros for the `ohno` crate.
37+
- [`recoverable`](./crates/recoverable/README.md) - Recovery information and classification for resilience patterns.
3738
- [`thread_aware`](./crates/thread_aware/README.md) - Facilities to support thread-isolated state.
3839
- [`thread_aware_macros`](./crates/thread_aware_macros/README.md) - Macros for the `thread_aware` crate.
3940
- [`thread_aware_macros_impl`](./crates/thread_aware_macros_impl/README.md) - Macros for the `thread_aware` crate.

crates/recoverable/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Changelog
2+
3+
## [0.1.0] - 2025-12-30
4+
5+
- ✨ Features
6+
7+
- introduce the recoverable crate

crates/recoverable/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
[package]
5+
name = "recoverable"
6+
description = "Recovery information and classification for resilience patterns."
7+
version = "0.1.0"
8+
readme = "README.md"
9+
keywords = ["oxidizer", "resilience", "metadata", "classification", "oxidizer"]
10+
categories = ["data-structures"]
11+
12+
edition.workspace = true
13+
rust-version.workspace = true
14+
authors.workspace = true
15+
license.workspace = true
16+
homepage.workspace = true
17+
repository.workspace = true
18+
19+
[package.metadata.docs.rs]
20+
all-features = true
21+
22+
[dependencies]
23+
24+
[dev-dependencies]
25+
ohno.workspace = true
26+
static_assertions.workspace = true
27+
28+
[lints]
29+
workspace = true

crates/recoverable/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<div align="center">
2+
<img src="./logo.png" alt="Recoverable Logo" width="96">
3+
4+
# Recoverable
5+
6+
[![crate.io](https://img.shields.io/crates/v/recoverable.svg)](https://crates.io/crates/recoverable)
7+
[![docs.rs](https://docs.rs/recoverable/badge.svg)](https://docs.rs/recoverable)
8+
[![MSRV](https://img.shields.io/crates/msrv/recoverable)](https://crates.io/crates/recoverable)
9+
[![CI](https://github.com/microsoft/oxidizer/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/microsoft/oxidizer/actions/workflows/main.yml)
10+
[![Coverage](https://codecov.io/gh/microsoft/oxidizer/graph/badge.svg?token=FCUG0EL5TI)](https://codecov.io/gh/microsoft/oxidizer)
11+
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
12+
<a href="../.."><img src="../../logo.svg" alt="This crate was developed as part of the Oxidizer project" width="20"></a>
13+
14+
</div>
15+
16+
Recovery information and classification for resilience patterns.
17+
18+
## Why
19+
20+
This crate provides types for classifying conditions based on their **recoverability state**,
21+
enabling consistent recovery behavior across different error types and resilience middleware.
22+
23+
## Recovery Information
24+
25+
The recovery information describes whether recovering from an operation might help, not whether
26+
the operation succeeded or failed. Both successful operations and permanent failures
27+
should use [`RecoveryInfo::never`][__link0] since recovery is not necessary or desirable.
28+
29+
## Core Types
30+
31+
* [`RecoveryInfo`][__link1]: Classifies conditions as recoverable (transient) or non-recoverable (permanent/successful).
32+
* [`Recovery`][__link2]: A trait for types that can determine their recoverability.
33+
* [`RecoveryKind`][__link3]: An enum representing the kind of recovery that can be attempted.
34+
35+
## Examples
36+
37+
### Recovery Error
38+
39+
```rust
40+
use recoverable::{Recovery, RecoveryInfo, RecoveryKind};
41+
42+
#[derive(Debug)]
43+
enum DatabaseError {
44+
ConnectionTimeout,
45+
InvalidCredentials,
46+
TableNotFound,
47+
}
48+
49+
impl Recovery for DatabaseError {
50+
fn recovery(&self) -> RecoveryInfo {
51+
match self {
52+
// Transient failure - might succeed if retried
53+
DatabaseError::ConnectionTimeout => RecoveryInfo::retry(),
54+
// Permanent failures - retrying won't help
55+
DatabaseError::InvalidCredentials => RecoveryInfo::never(),
56+
DatabaseError::TableNotFound => RecoveryInfo::never(),
57+
}
58+
}
59+
}
60+
61+
let error = DatabaseError::ConnectionTimeout;
62+
assert_eq!(error.recovery().kind(), RecoveryKind::Retry);
63+
64+
// For successful operations, also use never() since retry is unnecessary
65+
let success_result: Result<(), DatabaseError> = Ok(());
66+
// If we had a wrapper type for success, it would also return RecoveryInfo::never()
67+
```
68+
69+
### Retry Delay
70+
71+
You can specify when to retry an operation using the `delay` method:
72+
73+
```rust
74+
use std::time::Duration;
75+
use recoverable::{RecoveryInfo, RecoveryKind};
76+
77+
// Retry with a 30-second delay (e.g., from a Retry-After header)
78+
let recovery = RecoveryInfo::retry().delay(Duration::from_secs(30));
79+
assert_eq!(recovery.kind(), RecoveryKind::Retry);
80+
assert_eq!(recovery.get_delay(), Some(Duration::from_secs(30)));
81+
82+
// Immediate retry
83+
let immediate = RecoveryInfo::retry().delay(Duration::ZERO);
84+
assert_eq!(immediate.get_delay(), Some(Duration::ZERO));
85+
```
86+
87+
88+
<hr/>
89+
<sub>
90+
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/oxidizer/tree/main/crates/recoverable">source code</a>.
91+
</sub>
92+
93+
[__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG4cFLMVQymhvG3_1rzbl1X55G-vZhEWC9_13GwjdQK0PrVchYWSBgmtyZWNvdmVyYWJsZWUwLjEuMA
94+
[__link0]: https://docs.rs/recoverable/0.1.0/recoverable/?search=RecoveryInfo::never
95+
[__link1]: https://docs.rs/recoverable/0.1.0/recoverable/struct.RecoveryInfo.html
96+
[__link2]: https://docs.rs/recoverable/0.1.0/recoverable/trait.Recovery.html
97+
[__link3]: https://docs.rs/recoverable/0.1.0/recoverable/enum.RecoveryKind.html
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Example demonstrating how to use the `Recovery` trait with error types.
5+
//!
6+
//! This example shows how to implement the `Recovery` trait for custom error types
7+
//! and use `RecoveryInfo` to classify errors as transient or permanent.
8+
9+
use std::error::Error;
10+
use std::fmt::Display;
11+
use std::time::Duration;
12+
13+
use recoverable::{Recovery, RecoveryInfo, RecoveryKind};
14+
15+
fn main() {
16+
handle_network_error(&NetworkError::DnsResolutionFailed);
17+
handle_network_error(&NetworkError::InvalidUrl);
18+
handle_network_error(&NetworkError::ServiceUnavailable { retry_after: None });
19+
}
20+
21+
/// A network error type demonstrating different recovery scenarios.
22+
#[derive(Debug)]
23+
enum NetworkError {
24+
/// DNS resolution failed - might be transient
25+
DnsResolutionFailed,
26+
/// Invalid URL format - permanent error
27+
InvalidUrl,
28+
/// Service is unavailable, for example circuit breaker is open
29+
ServiceUnavailable { retry_after: Option<Duration> },
30+
}
31+
32+
impl Recovery for NetworkError {
33+
fn recovery(&self) -> RecoveryInfo {
34+
match self {
35+
Self::DnsResolutionFailed => RecoveryInfo::retry(),
36+
Self::InvalidUrl => RecoveryInfo::never(),
37+
Self::ServiceUnavailable { retry_after: Some(after) } => RecoveryInfo::unavailable().delay(*after),
38+
Self::ServiceUnavailable { retry_after: None } => RecoveryInfo::unavailable(),
39+
}
40+
}
41+
}
42+
43+
impl Display for NetworkError {
44+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45+
match self {
46+
Self::DnsResolutionFailed => write!(f, "DNS resolution failed"),
47+
Self::InvalidUrl => write!(f, "invalid URL format"),
48+
Self::ServiceUnavailable { retry_after } => {
49+
if let Some(after) = retry_after {
50+
write!(f, "service unavailable, retry after {after:?}")
51+
} else {
52+
write!(f, "service unavailable")
53+
}
54+
}
55+
}
56+
}
57+
}
58+
59+
impl Error for NetworkError {}
60+
61+
/// Demonstrates handling network errors.
62+
fn handle_network_error(error: &NetworkError) {
63+
let recovery = error.recovery();
64+
65+
println!("\nError: {error}");
66+
println!("Recovery strategy: {recovery}");
67+
68+
match recovery.kind() {
69+
RecoveryKind::Retry => println!("→ transient network issue, retry recommended"),
70+
RecoveryKind::Unavailable => println!("→ service appears to be down"),
71+
RecoveryKind::Never => println!("→ configuration or code change needed"),
72+
RecoveryKind::Unknown => println!("→ unknown recovery status"),
73+
_ => println!("→ unhandled recovery kind"),
74+
}
75+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Example demonstrating how to use implement `Recovery` trait
5+
//! for errors implemented using the `ohno` crate.
6+
7+
use recoverable::{Recovery, RecoveryInfo, RecoveryKind};
8+
9+
fn main() {
10+
handle_network_error(&NetworkError::dns_resolution_failed());
11+
handle_network_error(&NetworkError::invalid_url());
12+
handle_network_error(&NetworkError::service_unavailable());
13+
}
14+
15+
/// A transparent network error type demonstrating different recovery scenarios.
16+
#[ohno::error]
17+
struct NetworkError {
18+
recovery_info: RecoveryInfo,
19+
}
20+
21+
impl NetworkError {
22+
fn dns_resolution_failed() -> Self {
23+
Self::caused_by(RecoveryInfo::retry(), "DNS resolution failed")
24+
}
25+
26+
fn invalid_url() -> Self {
27+
Self::caused_by(RecoveryInfo::never(), "invalid URL format")
28+
}
29+
30+
fn service_unavailable() -> Self {
31+
Self::caused_by(RecoveryInfo::unavailable(), "service unavailable")
32+
}
33+
}
34+
35+
impl Recovery for NetworkError {
36+
fn recovery(&self) -> RecoveryInfo {
37+
self.recovery_info.clone()
38+
}
39+
}
40+
41+
/// Demonstrates handling network errors.
42+
fn handle_network_error(error: &NetworkError) {
43+
let recovery = error.recovery();
44+
45+
println!("\nError: {error}");
46+
println!("Recovery strategy: {recovery}");
47+
48+
match recovery.kind() {
49+
RecoveryKind::Retry => println!("→ transient network issue, retry recommended"),
50+
RecoveryKind::Unavailable => println!("→ service appears to be down"),
51+
RecoveryKind::Never => println!("→ configuration or code change needed"),
52+
RecoveryKind::Unknown => println!("→ unknown recovery status"),
53+
_ => println!("→ unhandled recovery kind"),
54+
}
55+
}

0 commit comments

Comments
 (0)