Skip to content

Commit 111690d

Browse files
committed
Adding README for retry! macro
1 parent e755a03 commit 111690d

File tree

3 files changed

+255
-42
lines changed

3 files changed

+255
-42
lines changed

retryable/Cargo.lock

+25-23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

retryable/README.md

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Retryable Macros
2+
3+
This demo will cover two different implementations of similar logic, retrying fallible functions.
4+
5+
- `retry!`
6+
- Wraps a given function with retry logc (*optional number of retries can be given*)
7+
- A progression of the previous `timeit!` macro, with added logic defined within the `macro_rules!`
8+
- `retryable!`
9+
- We'll build a `Retryable` type with flexible `RetryStrategy` options (retries, delay, etc.)
10+
- `Retryable` can be used without a macro, but requires verbose setup
11+
- `retryable!` macro will warp the setup logic, offering rules for passing strategy options
12+
13+
## Use Cases
14+
Functions can fail. Some failures are persistent, like trying to open an invalid file path or parsing numeric values out of a string that doesn't contain numbers. Other failures are intermittent, like attempting to read data from a remote server. In the intermittent case it can be useful to have some logic to retry the attempted call in hopes for a successful result. This is exactly what our `retry!` and `retryable!` macros will do!
15+
16+
Here's a function that fails with a given failure rate that we'll use to illustrate the retry functionality:
17+
18+
```rust
19+
use rand::Rng;
20+
21+
/// Given a failure rate percentage (0..=100),
22+
/// fail with that probability
23+
fn sometimes_fail(failure_rate: u8) -> Result<(), ()> {
24+
assert!(failure_rate <= 100, "Failure rate is a % (0..=100)");
25+
let mut rng = rand::thread_rng();
26+
let val = rng.gen_range(0u8, 100u8);
27+
if val > failure_rate {
28+
Ok(())
29+
} else {
30+
Err(())
31+
}
32+
}
33+
```
34+
35+
With a given failure rate of 50%, we could hope that retrying the function call would pass given 3 or more retries:
36+
```rust
37+
#[test]
38+
fn test_retry() {
39+
// Closure invocation
40+
let fallible = || {
41+
sometimes_fail(10)
42+
};
43+
let res = retry!(fallible; retries = 3);
44+
assert!(res.is_ok());
45+
46+
// Alternate func + args invocation
47+
let res = retry!(sometimes_fail, 10; retries = 3);
48+
assert!(res.is_ok());
49+
}
50+
```
51+
52+
# A First Attempt
53+
Before we dive into writing our `retry!` macro, let's look at what retrying a fallible function looks like in Rust. A helpful way to approach writing macros is to:
54+
55+
- Write the code in non-macro form
56+
- Look at what parts should/could be parameterized
57+
- Build a macro for a specific use case
58+
- Expand to include additional use cases when it makes sense
59+
60+
## Retry Logic
61+
A pre-req for our retryable logic is that the function or closure the code is retrying should return `Result`. This allows us to check the `Result` variant (`Ok`/`Err`) and retry accordingly. A [example of this is](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=1d7273b8ce7ac487ca5e1e23127c4c42):
62+
63+
```rust
64+
let mut retries = 3; // How many times to retry on error
65+
66+
let func = || { sometimes_fail(10) };
67+
// Loop until we hit # of retries or success
68+
let res = loop {
69+
// Run our function, capturing the Result in `res`
70+
let res = func();
71+
// Upon success, break out the loop and return the `Result::Ok`
72+
if res.is_ok() {
73+
break res;
74+
}
75+
// Otherwise, decrement retries and loop around again
76+
if retries > 0 {
77+
retries -= 1;
78+
continue;
79+
}
80+
// When retries have been exhausted, finally return the `Result::Err`
81+
break res;
82+
};
83+
84+
assert!res.is_ok());
85+
```
86+
87+
One very clear parameter for this macro is the function that's being called (in this case, `sometimes_fail`). Turning this into macro form would look something like:
88+
89+
```rust
90+
macro_rules! retry {
91+
($f:expr) => {{
92+
let mut retries = 3;
93+
loop {
94+
let res = $f();
95+
if res.is_ok() {
96+
break res;
97+
}
98+
if retries > 0 {
99+
retries -= 1;
100+
continue;
101+
}
102+
break res;
103+
}
104+
}};
105+
}
106+
```
107+
108+
This mostly looks similar to our non-macro code above, but I'll explain the match rule `($f:expr)` a bit. This rule will only allow a single expression to be passed into the macro. Additionally, since we're eventually calling the `expr` like `$f()`, the expression must be something that results in a function. So a closure seems like a perfect fit and this macro can be used like ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4c42053cf00e6affb9e26ab5acb163d9)):
109+
110+
```rust
111+
let res = retry!(|| sometimes_fail(10));
112+
assert!res.is_ok());
113+
```
114+
115+
Currently, we can't pass a function directly (E.g. `retry!(sometimes_fail(10)`) as the macro expansion would end up like:
116+
117+
```rust
118+
// ...
119+
let res = sometimes_fail(10)();
120+
// ...
121+
```
122+
123+
And we can't call the `Result` that `sometimes_fail(10)` returns. To make this work we should look at using yet another macro to coerce closures & functions into a common form for the `retry!` macro.
124+
125+
## Nesting Macros
126+
To keep the `retry!` macro_rules implementation clean, we'll create another macro (`_wrapper!`) to faciliate the passing of closures **or** functions with arguments which will add this additional use case to our examples above:
127+
128+
```rust
129+
// Alternate func + args invocation
130+
let res = retry!(sometimes_fail(10));
131+
assert!(res.is_ok());
132+
```
133+
134+
Let's implement the case covered in `retry!` already:
135+
136+
```rust
137+
macro_rules! _wrapper {
138+
// Single expression (like a function name or closure)
139+
($f:expr) => {{
140+
$f()
141+
}};
142+
}
143+
```
144+
145+
I'm using `_wrapper` as the name here to signal that it's intended to be use internally by the `retry!` macro and won't be exported by this library (perhaps a bad habit coming from Python). We can now use this example to get the same functionality as the prior `retry!` macro example:
146+
147+
```rust
148+
macro_rules! retry {
149+
($f:expr) => {{
150+
let mut retries = 3;
151+
loop {
152+
let res = _wrapper!($f);
153+
if res.is_ok() {
154+
break res;
155+
}
156+
if retries > 0 {
157+
retries -= 1;
158+
continue;
159+
}
160+
break res;
161+
}
162+
}};
163+
}
164+
```
165+
166+
As far as functionality goes, nothing has been added here, but this will enable us to build matching logic for our multiple use-cases within `_wrapper!` instead of duplicating code in `retry!` for different match rules.
167+
168+
### Repeating matches
169+
Something we learned with the `timeit!` macro was that we can match on repeating items, and then add code-expansion for each item. We'll use that same trick here to match on multiple arguments for the case of a function & args being passed into `retry!`:
170+
171+
```rust
172+
macro_rules! _wrapper {
173+
($f:expr) => {{ /* code from previous section */ }};
174+
// Variadic number of args (Allowing trailing comma)
175+
($f:expr, $( $args:expr $(,)? )* ) => {{
176+
$f( $($args,)* )
177+
}};
178+
}
179+
```
180+
181+
There's a lot going on in this single line so let's break it down:
182+
- `$f:expr`: The function passed in for retrying
183+
- `,`: Comma separator before the function arguments
184+
- `$( .. )*`: Anything in these parentheses can repeat (zero or more times, like `*` in regex)
185+
- `$args:expr`: Capture each repeating expr into `$args`
186+
- `$(,)?`: Allow optional commas (? == 0 or 1 times, like `regex` )
187+
188+
This match rule will capture something like `_wrapper!(my_func, 10, 20)` into something that resembles:
189+
- `$f` == `my_func`
190+
- `$args` == `[10, 20]`
191+
192+
And let's break down the expansion: `$f( $( $args, )* )`:
193+
194+
- `$f( ... )`: Function name, with literal parenthesis wrapping whatever is inside
195+
- `$( ... )*`: Repeat what's inside per expr in `$args`
196+
- `$args,`: Write out an expr, followed by a literal comma
197+
198+
Which expands to:
199+
```rust
200+
my_func(10, 20,)
201+
```
202+
203+
With these two match rules inside `_wrapper!`, we can now successfully use `retry!` with all of the use cases! Check out the [final implementation](https://github.com/thepacketgeek/rust-macros-demo/blob/master/retryable/src/lib.rs#L39) and [accompanying tests here](https://github.com/thepacketgeek/rust-macros-demo/blob/master/retryable/src/lib.rs#L326).

0 commit comments

Comments
 (0)