1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! Resting place for [RetryResult]
use crate::{
keen_retry_executor::KeenRetryExecutor,
keen_retry_async_executor::KeenRetryAsyncExecutor,
};
use std::future::Future;
/// Wrapper for the return type of fallible & retryable functions -- an extension for `Result<OkPayload, ErrorType>`,
/// but also accepting an `input`.\
/// Considering zero-copy, both `Retry` & `Fatal` variants will contain the original input payload, which is consumed by an `Ok` operation;
/// The `Ok` operation, on the other hand, has the outcome result.
pub enum RetryResult<ReportedInput,
OriginalInput,
Output,
ErrorType> {
Ok {
reported_input: ReportedInput,
output: Output,
},
Retry {
input: OriginalInput,
error: ErrorType,
},
Fatal {
input: OriginalInput,
error: ErrorType,
},
}
impl<ReportedInput,
OriginalInput,
Output,
ErrorType>
RetryResult<ReportedInput,
OriginalInput,
Output,
ErrorType> {
pub fn inspect_ok<IgnoredReturn,
F: FnOnce(&ReportedInput, &Output) -> IgnoredReturn>
(self, f: F) -> Self {
if let Self::Ok { ref reported_input, ref output } = self {
f(reported_input, output);
}
self
}
pub fn inspect_retry<IgnoredReturn,
F: FnOnce(&OriginalInput, &ErrorType) -> IgnoredReturn>
(self, f: F) -> Self {
if let Self::Retry { input: ref payload, ref error } = self {
f(payload, error);
}
self
}
/// Spots on the data of an operation that failed fatably on the first shot (in which case, no retrying will be attempted).
/// - `f(&original_input, &error_type)`
pub fn inspect_fatal<IgnoredReturn,
F: FnOnce(&OriginalInput, &ErrorType) -> IgnoredReturn>
(self, f: F) -> Self {
if let Self::Fatal { ref input, ref error } = self {
f(input, error);
}
self
}
/// Changes the original input data to be re-fed to an operation that failed and will be retried.
/// - `f(original_input) -> new_original_input`.\
/// See [Self::map_ok()] if you'd rather change the "reported input" & output of an operation that succeeded/failed fatably.
///
/// A nice usage would be to upgrade the initial payload to a tuple, keeping track of how much time will be spent retrying:
/// ```nocompile
/// .map_input(|payload| (payload, SystemTime::now()))
pub fn map_input<NewOriginalInput,
F: FnOnce(OriginalInput) -> NewOriginalInput>
(self, f: F) -> RetryResult<ReportedInput, NewOriginalInput, Output, ErrorType> {
match self {
RetryResult::Ok { reported_input, output } => RetryResult::Ok { reported_input, output },
RetryResult::Retry { input, error } => RetryResult::Retry { input: f(input), error },
RetryResult::Fatal { input, error } => RetryResult::Fatal { input: f(input), error },
}
}
/// Changes the input & output data associated with an operation that was successful at the first shot.
/// - `f(reported_input, output) -> (new_reported_input, new_output)`.\
/// See [Self::map_input()] if you'd rather change original input instead
pub fn map_ok<NewReportedInput,
NewOutput,
F: FnOnce(ReportedInput, Output) -> (NewReportedInput, NewOutput)>
(self, f: F) -> RetryResult<NewReportedInput, OriginalInput, NewOutput, ErrorType> {
match self {
RetryResult::Retry { input, error } => RetryResult::Retry { input, error },
RetryResult::Fatal { input, error } => RetryResult::Fatal { input, error },
RetryResult::Ok { reported_input, output } => {
let (reported_input, output) = f(reported_input, output);
RetryResult::Ok { reported_input, output }
},
}
}
/// Changes the (value of the) error that indicates this operation may be retried.\
/// See [Self::map_errors()] if you'd also like to change the type;\
/// See [Self::map_inputs_and_errors()] if you'd like to remap/swap all possible pairs of input/error
pub fn map_retry_error<F: FnOnce(ErrorType) -> ErrorType>
(self, f: F) -> RetryResult<ReportedInput, OriginalInput, Output, ErrorType> {
match self {
RetryResult::Ok { reported_input, output } => RetryResult::Ok { reported_input, output },
RetryResult::Retry { input, error } => RetryResult::Retry { input, error: f(error) },
RetryResult::Fatal { input, error } => RetryResult::Fatal { input, error },
}
}
/// Changes the (value of the) error that indicates this operation may not be retried.\
/// See [Self::map_errors()] if you'd also like to change the type.
pub fn map_fatal_error<F: FnOnce(ErrorType) -> ErrorType>
(self, f: F) -> RetryResult<ReportedInput, OriginalInput, Output, ErrorType> {
match self {
RetryResult::Ok { reported_input, output } => RetryResult::Ok { reported_input, output },
RetryResult::Retry { input, error } => RetryResult::Retry { input, error },
RetryResult::Fatal { input, error } => RetryResult::Fatal { input, error: f(error) },
}
}
/// Allows changing the (input,error) pairs for both error possibilities:
/// - fatal_map_fn(input, fatal_error) -> (new_input, new_fatal_error)
/// - retry_map_fn(input, retry_error) -> (new_input, new_retry_error)
///
/// Covers the case where it is desireable to bail out the retrying process of a consumer operation,
/// moving the consumed input back to the error, so the caller may not lose the payload.
///
/// See also [ResolvedResult::map_inputs_and_errors()] for similar semantics after exhausting retries.
pub fn map_inputs_and_errors<NewOriginalInput,
NewErrorType,
FatalMapFn: FnOnce(OriginalInput, ErrorType) -> (NewOriginalInput, NewErrorType),
RetryMapFn: FnOnce(OriginalInput, ErrorType) -> (NewOriginalInput, NewErrorType)>
(self,
fatal_map_fn: FatalMapFn,
retry_map_fn: RetryMapFn)
-> RetryResult<ReportedInput, NewOriginalInput, Output, NewErrorType> {
match self {
RetryResult::Ok { reported_input, output } => RetryResult::Ok { reported_input, output },
RetryResult::Retry { input, error } => {
let (new_input, new_error) = retry_map_fn(input, error);
RetryResult::Retry { input: new_input, error: new_error }
},
RetryResult::Fatal { input, error } => {
let (new_input, new_error) = fatal_map_fn(input, error);
RetryResult::Fatal { input: new_input, error: new_error }
},
}
}
/// Upgrades this [RetryResult] into a [KeenRetryExecutor], which will, on its turn, be upgraded to [ResolvedResult], containing the final results after executing the retryable operation
pub fn retry_with<RetryFn: FnMut(OriginalInput) -> RetryResult<ReportedInput, OriginalInput, Output, ErrorType>>
(self,
retry_operation: RetryFn)
-> KeenRetryExecutor<ReportedInput, OriginalInput, Output, ErrorType, RetryFn> {
match self {
RetryResult::Ok { reported_input, output } => KeenRetryExecutor::from_ok_result(reported_input, output),
RetryResult::Fatal { input, error } => KeenRetryExecutor::from_err_result(input, error),
RetryResult::Retry { input, error } => KeenRetryExecutor::new(input, retry_operation, error),
}
}
/// Upgrades this [RetryResult] into a [KeenRetryAsyncExecutor], which will, on its turn, be upgraded to [ResolvedResult], containing the final results after executing the retryable operation
pub fn retry_with_async<AsyncRetryFn: FnMut(OriginalInput) -> OutputFuture,
OutputFuture: Future<Output=RetryResult<ReportedInput, OriginalInput, Output, ErrorType>>>
(self,
retry_operation: AsyncRetryFn)
-> KeenRetryAsyncExecutor<ReportedInput, OriginalInput, Output, ErrorType, AsyncRetryFn, OutputFuture> {
match self {
RetryResult::Ok { reported_input, output } => KeenRetryAsyncExecutor::from_ok_result(reported_input, output),
RetryResult::Fatal { input, error } => KeenRetryAsyncExecutor::from_err_result(input, error),
RetryResult::Retry { input, error } => KeenRetryAsyncExecutor::new(input, retry_operation, error),
}
}
pub fn is_ok(&self) -> bool {
matches!(self, RetryResult::Ok {..})
}
pub fn is_fatal(&self) -> bool {
matches!(self, RetryResult::Fatal {..})
}
pub fn is_retry(&self) -> bool {
matches!(self, RetryResult::Retry {..})
}
pub fn expect_ok(&self, panic_msg: &str) {
if !self.is_ok() {
panic!("{panic_msg}")
}
}
pub fn expect_fatal(&self, panic_msg: &str) {
if !self.is_fatal() {
panic!("{panic_msg}")
}
}
pub fn expect_retry(&self, panic_msg: &str) {
if !self.is_retry() {
panic!("{panic_msg}")
}
}
/// Syntatic sugar for [Result<Output, ErrorType>::from()].\
/// See also [Self::into()]
pub fn into_result(self) -> Result::<Output, ErrorType> {
Result::<Output, ErrorType>::from(self)
}
}
impl<ReportedInput,
OriginalInput,
Output,
ErrorType>
From<RetryResult<ReportedInput,
OriginalInput,
Output,
ErrorType>> for
Result<Output, ErrorType> {
/// Opts out of any retrying attempts and converts the "first shot" of a retryable operation into a `Result<>`.\
/// To opt-in the retrying process, see [RetryResult::retry_with()] or [RetryResult::retry_with_async()]
fn from(retry_result: RetryResult<ReportedInput, OriginalInput, Output, ErrorType>) -> Self {
match retry_result {
RetryResult::Ok { reported_input: _, output } => Ok(output),
RetryResult::Fatal { input: _, error } => Err(error),
RetryResult::Retry { input: _, error } => Err(error),
}
}
}