Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Please see each crate's change log below:

- [`data_privacy`](./crates/data_privacy/CHANGELOG.md)
- [`data_privacy_macros`](./crates/data_privacy_macros/CHANGELOG.md)
- [`recoverable`](./crates/recoverable/CHANGELOG.md)
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 13 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

[workspace]
resolver = "3"
members = [
"crates/*",
]
members = ["crates/*"]

[workspace.package]
edition = "2024"
Expand All @@ -20,20 +18,22 @@ repository = "https://github.com/microsoft/oxidizer"

# local dependencies
data_privacy = { path = "crates/data_privacy", default-features = false, version = "0.4.0" }
data_privacy_macros = { path = "crates/data_privacy_macros", default-features = false, version = "0.3.0" }
data_privacy_macros = { path = "crates/data_privacy_macros", default-features = false, version = "0.3.0" }
recoverable = { path = "crates/recoverable", default-features = false, version = "0.1.0" }

# external dependencies
insta = { version = "1.42.0", default-features = false }
mutants = { version = "0.0.3", default-features = false }
once_cell = { version = "1.21.3", default-features = false }
prettyplease = { version = "0.2.29", default-features = false }
proc-macro2 = { version = "1.0.101", default-features = true }
proc-macro-crate = { version = "3.3.0", default-features = false }
quote = { version = "1.0.38", default-features = false }
insta = { version = "1.42.0", default-features = false }
mutants = { version = "0.0.3", default-features = false }
once_cell = { version = "1.21.3", default-features = false }
prettyplease = { version = "0.2.29", default-features = false }
proc-macro2 = { version = "1.0.101", default-features = true }
proc-macro-crate = { version = "3.3.0", default-features = false }
quote = { version = "1.0.38", default-features = false }
serde = { version = "1.0.217", default-features = false }
serde_json = { version = "1.0.135", default-features = false }
serde_json = { version = "1.0.135", default-features = false }
static_assertions = { version = "1.1.0", default-features = false }
syn = { version = "2.0.106", default-features = false }
xxhash-rust = { version = "0.8.15", default-features = false }
xxhash-rust = { version = "0.8.15", default-features = false }

[workspace.lints.rust]
ambiguous_negative_literals = "warn"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ These are the crates built out of this repo:

- [`data_privacy`](./crates/data_privacy/README.md) - Mechanisms to classify, manipulate, and redact sensitive data.
- [`data_privacy_macros`](./crates/data_privacy_macros/README.md) - Macros to generate data taxonomies.
- [`recoverable`](./crates/recoverable/README.md) - Recovery metadata and classification for resilience patterns.

## Repo Guidelines

Expand Down
1 change: 1 addition & 0 deletions crates/recoverable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
28 changes: 28 additions & 0 deletions crates/recoverable/Cargo.toml
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"
75 changes: 75 additions & 0 deletions crates/recoverable/README.md
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

[![crate.io](https://img.shields.io/crates/v/recoverable.svg)](https://crates.io/crates/recoverable)
[![docs.rs](https://docs.rs/recoverable/badge.svg)](https://docs.rs/recoverable)
[![CI](https://github.com/microsoft/oxidizer/workflows/main/badge.svg)](https://github.com/microsoft/oxidizer/actions)
[![Coverage](https://codecov.io/gh/microsoft/oxidizer/graph/badge.svg?token=FCUG0EL5TI)](https://codecov.io/gh/microsoft/oxidizer)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](../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>
75 changes: 75 additions & 0 deletions crates/recoverable/examples/recoverable_error.rs
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be Recovery.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geeknoid Would you agree with the rename?

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 Recoverable more appropriate than Recovery.
(capability vs action)

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"),
}
}
3 changes: 3 additions & 0 deletions crates/recoverable/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading