forked from scylladb/scylladb
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathresult_try.hh
622 lines (542 loc) · 24.1 KB
/
result_try.hh
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
/*
* Copyright 2022-present ScyllaDB
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#pragma once
#include <concepts>
#include <type_traits>
#include "utils/result.hh"
namespace utils {
namespace internal {
struct noop_converter {
template<typename T>
using wrapped_type = T;
template<typename T>
static
wrapped_type<T>
wrap(T&& t) {
return std::move(t);
}
template<typename F, typename... Args>
static
wrapped_type<std::invoke_result_t<F, Args...>>
invoke(const F& f, Args&&... args) {
return std::invoke(f, std::forward<Args>(args)...);
}
};
struct futurizing_converter {
template<typename T>
using wrapped_type = seastar::futurize_t<T>;
template<typename T>
static
wrapped_type<T>
wrap(T&& t) {
if constexpr (seastar::is_future<T>::value) {
return t;
} else {
return seastar::make_ready_future<T>(std::forward<T>(t));
}
}
template<typename F, typename... Args>
static
wrapped_type<std::invoke_result_t<F, Args...>>
invoke(const F& f, Args&&... args) {
return seastar::futurize_invoke(f, std::forward<Args>(args)...);
}
};
template<typename From, typename Converter, typename To>
concept ConvertsWithTo = std::convertible_to<typename Converter::template wrapped_type<From>, typename Converter::template wrapped_type<To>>;
// We require forall<ExceptionContainerResult R> ExceptionHandle<H, R>.
// However, C++ does not support quantification like that in the constraints.
// Here, we use the dummy_result to SCYLLA_ASSERT that the handles defined below
// at least work with dummy_result.
template<typename T = void>
using dummy_result = result_with_exception<T, std::exception>;
// TODO: Should we make a virtual base class for this?
// This would allow using non-generic lambdas in result_catch and result_catch_dots.
template<typename Handle>
concept ExceptionHandle =
ExceptionContainerResult<dummy_result<>> &&
requires (Handle handle, seastar::promise<dummy_result<int>>& p) {
{ handle.into_result() } -> std::same_as<dummy_result<>>;
{ handle.into_future() } -> std::same_as<seastar::future<dummy_result<>>>;
{ handle.forward_to_promise(p) } -> std::same_as<void>;
};
template<ExceptionContainerResult R>
struct failed_result_handle {
private:
R&& _failed_result;
public:
failed_result_handle(R&& r) : _failed_result(std::move(r)) {}
// Returns the failed result. Can be used only once as this moves out
// the failed result. This should be called once during the handler's
// lifetime, preferably as `return handler.into_result()`.
R into_result() {
return std::move(_failed_result);
}
seastar::future<R> into_future() {
return seastar::make_ready_future<R>(std::move(_failed_result));
}
template<ExceptionContainerResult S>
requires std::same_as<typename R::error_type, typename S::error_type>
void forward_to_promise(seastar::promise<S>& p) {
p.set_value(std::move(_failed_result).as_failure());
}
const typename R::error_type& as_inner() const & {
return _failed_result.assume_error();
}
typename R::error_type&& as_inner() && {
return std::move(_failed_result).assume_error();
}
typename R::error_type clone_inner() {
return _failed_result.assume_error().clone();
}
};
static_assert(ExceptionHandle<failed_result_handle<dummy_result<>>>);
template<ExceptionContainerResult R>
struct exception_ptr_handle {
private:
std::exception_ptr _eptr;
public:
exception_ptr_handle(std::exception_ptr&& eptr) : _eptr(std::move(eptr)) {}
// Throws the exception.
[[noreturn]] R into_result() {
std::rethrow_exception(std::move(_eptr));
}
seastar::future<R> into_future() {
return seastar::make_exception_future<R>(std::move(_eptr));
}
template<ExceptionContainerResult S>
requires std::same_as<typename R::error_type, typename S::error_type>
void forward_to_promise(seastar::promise<S>& p) {
p.set_exception(std::move(_eptr));
}
const std::exception_ptr& as_inner() const & {
return _eptr;
}
std::exception_ptr&& as_inner() && {
return std::move(_eptr);
}
std::exception_ptr clone_inner() {
return _eptr;
}
};
// Check that it is a valid handle with some arbitrary ExceptionContainerResult
static_assert(ExceptionHandle<exception_ptr_handle<dummy_result<>>>);
template<typename Ex, typename Cb>
struct result_catcher {
private:
Cb _cb;
public:
using exception_type = Ex;
using callback_type = Cb;
public:
result_catcher(Cb&& cb) : _cb(std::move(cb)) {}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
typename Converter::template wrapped_type<R>
handle_exception_from_result(const auto& ex, R& original_result, Continuation&& cont) {
if constexpr (std::is_base_of_v<Ex, std::remove_cvref_t<decltype(ex)>>) {
if constexpr (std::is_invocable_v<Cb, const Ex&, failed_result_handle<R>>) {
// Invoke with exception reference and handle
return Converter::template invoke<Cb, const Ex&, failed_result_handle<R>>(_cb, ex, failed_result_handle<R>(std::move(original_result)));
} else {
// Simplified interface - invoke with reference to exception only
static_assert(std::is_invocable_v<Cb, const Ex&>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<Cb, const Ex&>(_cb, ex);
}
} else {
// Let another handler try it
return std::invoke(std::forward<Continuation>(cont));
}
}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
auto wrap_in_catch(Continuation&& cont) {
return [this, cont = std::forward<Continuation>(cont)] (bool& already_caught) mutable -> typename Converter::template wrapped_type<R> {
try {
return std::invoke(std::forward<Continuation>(cont), already_caught);
} catch (const Ex& ex) {
if (already_caught) {
throw;
}
already_caught = true;
if constexpr (std::is_invocable_v<Cb, const Ex&, exception_ptr_handle<R>>) {
// Invoke with exception reference and handle
return Converter::template invoke<const Cb, const Ex&, exception_ptr_handle<R>>(_cb, ex, exception_ptr_handle<R>(std::current_exception()));
} else {
// Simplified interface - invoke with reference to exception only
static_assert(std::is_invocable_v<Cb, const Ex&>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<const Cb, const Ex&>(_cb, ex);
}
}
};
}
};
template<typename T>
struct is_result_catcher : std::false_type {};
template<typename Ex, typename Cb>
struct is_result_catcher<result_catcher<Ex, Cb>> : std::true_type {};
template<typename Handler, typename Converter, typename Result>
concept ResultCatcherCallback =
requires (typename Handler::callback_type callback, typename Handler::exception_type& ex,
failed_result_handle<Result>&& frh, exception_ptr_handle<Result>&& eph) {
{ callback(ex, frh) } -> ConvertsWithTo<Converter, Result>;
{ callback(ex, eph) } -> ConvertsWithTo<Converter, Result>;
} ||
requires (typename Handler::callback_type callback, typename Handler::exception_type& ex) {
{ callback(ex) } -> ConvertsWithTo<Converter, Result>;
};
template<typename Handler, typename Converter, typename Result>
concept ResultCatcher =
is_result_catcher<Handler>::value &&
ResultCatcherCallback<Handler, Converter, Result>;
template<typename Cb>
struct result_catcher_dots {
private:
Cb _cb;
public:
using callback_type = Cb;
public:
result_catcher_dots(Cb&& cb) : _cb(std::move(cb)) {}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
typename Converter::template wrapped_type<R>
handle_exception_from_result(const auto& ex, R& original_result, Continuation&& cont) {
if constexpr (std::is_invocable_v<Cb, failed_result_handle<R>>) {
// Invoke with handle
return Converter::template invoke<Cb, failed_result_handle<R>>(_cb, failed_result_handle<R>(std::move(original_result)));
} else {
// Simplified interface - invoke without arguments
static_assert(std::is_invocable_v<Cb>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<Cb>(_cb);
}
// Don't propagate to the next handler. The catch (...) is supposed
// to match on all errors.
}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
auto wrap_in_catch(Continuation&& cont) {
return [this, cont = std::forward<Continuation>(cont)] (bool& already_caught) mutable -> typename Converter::template wrapped_type<R> {
try {
return std::invoke(std::forward<Continuation>(cont), already_caught);
} catch (...) {
if (already_caught) {
throw;
}
already_caught = true;
if constexpr (std::is_invocable_v<Cb, exception_ptr_handle<R>>) {
// Invoke with handle
return Converter::template invoke<Cb, exception_ptr_handle<R>>(_cb, exception_ptr_handle<R>(std::current_exception()));
} else {
// Simplified interface - invoke without arguments
static_assert(std::is_invocable_v<Cb>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<Cb>(_cb);
}
}
};
}
};
template<typename T>
struct is_result_catcher_dots : std::false_type {};
template<typename Cb>
struct is_result_catcher_dots<result_catcher_dots<Cb>> : std::true_type {};
template<typename Handler, typename Converter, typename Result>
concept DotsResultCatcherCallback =
requires (typename Handler::callback_type callback,
failed_result_handle<Result>&& frh, exception_ptr_handle<Result>&& eph) {
{ callback(frh) } -> ConvertsWithTo<Converter, Result>;
{ callback(eph) } -> ConvertsWithTo<Converter, Result>;
} ||
requires (typename Handler::callback_type callback) {
{ callback() } -> ConvertsWithTo<Converter, Result>;
};
template<typename Handler, typename Converter, typename Result>
concept DotsResultCatcher =
is_result_catcher_dots<Handler>::value &&
DotsResultCatcherCallback<Handler, Converter, Result>;
// Constructs an `invoke_in_try_catch` function which allows to call a callback
// and handle C++ exceptions using a set of handlers given during construction.
//
// The `Converter` is used to appropriately invoke the exception handlers.
template<ExceptionContainerResult R, typename Converter, typename... CatchHandlers>
struct try_catch_chain_impl {};
template<ExceptionContainerResult R, typename Converter, typename FirstCatchHandler, typename... CatchHandlers>
struct try_catch_chain_impl<R, Converter, FirstCatchHandler, CatchHandlers...> {
template<typename Continuation>
static
typename Converter::template wrapped_type<R>
invoke_in_try_catch(Continuation&& cont, FirstCatchHandler& first_handler, CatchHandlers&... catch_handlers) {
return try_catch_chain_impl<R, Converter, CatchHandlers...>::template invoke_in_try_catch<>(
first_handler.template wrap_in_catch<R, Converter, Continuation>(std::forward<Continuation>(cont)),
catch_handlers...);
}
};
template<ExceptionContainerResult R, typename Converter>
struct try_catch_chain_impl<R, Converter> {
template<typename Continuation>
static
typename Converter::template wrapped_type<R>
invoke_in_try_catch(Continuation&& cont) {
// Not using an invoker as we always want it to throw
bool already_caught = false;
return std::invoke(std::forward<Continuation>(cont), already_caught);
}
};
// Given a set of handlers, constructs a visitor which can be used to inspect
// a failed result.
//
// The `Converter` is used to appropriately invoke the exception handlers
// and convert the result to the return type if no handlers match on it.
template<ExceptionContainerResult R, typename Converter, typename... ResultHandlers>
struct combined_handler_impl {};
template<ExceptionContainerResult R, typename Converter, typename FirstResultHandler, typename... ResultHandlers>
struct combined_handler_impl<R, Converter, FirstResultHandler, ResultHandlers...>
: protected combined_handler_impl<R, Converter, ResultHandlers...> {
private:
using base = combined_handler_impl<R, Converter, ResultHandlers...>;
FirstResultHandler& _first_handler;
public:
combined_handler_impl(FirstResultHandler& first_handler, ResultHandlers&... result_handlers)
: base(result_handlers...)
, _first_handler(first_handler)
{ }
typename Converter::template wrapped_type<R>
handle(const auto& ex, R& original_result) {
return _first_handler.template handle_exception_from_result<R, Converter>(
ex, original_result,
[this, &ex, &original_result] () mutable {
return base::handle(ex, original_result);
});
}
auto with_original_result(R& res) {
return [this, &res] (const auto& ex) {
return this->handle(ex, res);
};
}
};
template<ExceptionContainerResult R, typename Converter>
struct combined_handler_impl<R, Converter> {
public:
typename Converter::template wrapped_type<R>
handle(const auto& ex, R& original_result) {
// No more handlers to try out, just return
return Converter::template wrap<R>(std::move(original_result));
}
auto with_original_result(R& res) {
return [this, &res] (const auto& ex) {
return handle(ex, res);
};
}
};
template<typename Result, typename... Handlers>
[[gnu::always_inline]]
inline
seastar::future<Result>
result_handle_available_future(seastar::future<Result>&& f, Handlers&... handlers) {
using combined_handler_type = internal::combined_handler_impl<Result, internal::futurizing_converter, Handlers...>;
using try_catch_chain_type = internal::try_catch_chain_impl<Result, internal::futurizing_converter, Handlers...>;
if (!f.failed()) {
Result&& res = f.get();
if (res) {
return seastar::make_ready_future<Result>(std::move(res));
}
// Handle the exception in result
auto combined_handler = combined_handler_type(handlers...);
return res.assume_error().accept(combined_handler.with_original_result(res));
} else {
// The future has an exception
// We need to create a try..catch chain from the handlers
// and rethrow the exception inside it
//
// futurize_invoke will protect us in case the exception is not handled
// by any of the provided handlers.
return seastar::futurize_invoke([&] {
return try_catch_chain_type::invoke_in_try_catch(
[&f] (bool&) mutable -> seastar::future<Result> { std::rethrow_exception(std::move(f).get_exception()); },
handlers...);
});
}
}
}
template<typename Handler, typename Converter, typename Result>
concept ResultCatcherMaybeDots =
internal::ResultCatcher<Handler, Converter, Result> ||
internal::DotsResultCatcher<Handler, Converter, Result>;
template<typename Fun>
concept ResultTryBody = requires (Fun fun) {
{ fun() } -> ExceptionContainerResult<>;
} && std::is_nothrow_move_constructible_v<std::invoke_result_t<Fun>>;
template<typename Fun>
concept ResultFuturizeTryBody = requires (Fun&& fun) {
{ seastar::futurize_invoke(std::forward<Fun>(fun)) } -> ExceptionContainerResultFuture<>;
} && std::is_nothrow_move_constructible_v<typename seastar::futurize_t<std::invoke_result_t<Fun>>::value_type>;
/// \brief Allows to handle C++ exceptions and failed results in a unified way.
///
/// When you modify a code path to return a result<> and you encounter
/// a try..catch block, you can use it to migrate the block so that it handles
/// both C++ exceptions and exceptions stored in the result<>.
///
/// \section Example
///
/// Let's say that you have the following try..catch chain:
///
/// try {
/// return a_function_that_may_throw();
/// } catch (const my_exception& ex) {
/// return 123;
/// } catch (...) {
/// throw;
/// }
///
/// You can add support for results in the following way:
///
/// return utils::result_try([&] {
/// return a_function_that_may_throw_or_return_a_failed_result();
/// }, utils::result_catch<my_exception>([&] (const Ex&) -> result<int> {
/// return 123;
/// }), utils::result_catch_dots([&] (auto&& handle) -> result<int> {
/// return handle.into_result();
/// });
///
/// Each `result_catch` handler declares the exception type it handles
/// and accepts a const reference to it as the first argument.
/// The `result_catch_dots` handler is equivalent to `(...)` so it does not
/// declare exception type or accept a reference to it.
///
/// In addition, each `result_catch` and `result_catch_dots` can optionally
/// accept a reference to an exception handle. The exception handle can be used
/// to return/rethrow the exception being currently handled. Depending on
/// whether the handler is invoked for a C++ exception or a failed result<>,
/// it will have a different type and its `into_result()` method will either
/// rethrow the exception or return the failed result, respectively.
///
/// \section Limitations
///
/// The main limitation of result_try is that is is not as flexible with control
/// flow as a try..catch block is. If you have some logic after the try..catch
/// block and some code paths in it block return and some don't, you need to
/// mark it somehow in the return value - for example, you can change result<T>
/// to result<std::optional<T>> and continue if it is std::nullopt, otherwise
/// return.
///
/// The return type of the first argument must be nothrow move constructible.
/// Because this function uses many recursive calls to simulate a multi-catch
/// clause try..catch block and NRVO is not in general guaranteed, there is
/// a risk that some moves are not elided and one of them can throw
/// in the middle of the stack. This exception can be thrown after we already
/// exited the try..catch blocks for some handlers, so not all of them will
/// participate in handling this exception. Because of the inability to properly
/// handle exceptions from failed moves, hence the nothrow-move-constructibility
/// requirement.
///
/// This function does not work with futures and will trigger a static_assert
/// if you use it with a future-returning function. See result_futurize_try
/// for a version which additionally works with exceptional futures.
template<ResultTryBody Fun, ResultCatcherMaybeDots<internal::noop_converter, std::invoke_result_t<Fun>>... Handlers>
requires (!seastar::is_future<std::invoke_result_t<Fun>>::value)
inline
std::invoke_result_t<Fun>
result_try(Fun&& fun, Handlers&&... handlers) {
using result_type = std::invoke_result_t<Fun>;
using combined_handler_type = internal::combined_handler_impl<result_type, internal::noop_converter, Handlers...>;
using try_catch_chain_type = internal::try_catch_chain_impl<result_type, internal::noop_converter, Handlers...>;
// Invoke `fun` and catch C++ exceptions if any occur
result_type res = try_catch_chain_type::template invoke_in_try_catch<>([&fun] (bool&) { return fun(); }, handlers...);
if (res) {
return res;
}
// No C++ exceptions but the result is a failure - inspect using a visitor
auto combined_handler = combined_handler_type(handlers...);
return res.assume_error().accept(combined_handler.with_original_result(res));
}
/// \brief A version of `result_try` which works with futures. It handles
/// C++ exceptions, failed results and exceptional futures returned from `fun`.
///
/// Migration from a try..catch block or f.handle_exception(...) is similar
/// as in the case of `result_try`, with a small number of differences
/// described below.
///
/// The `fun` function and all exception handlers are futurize_invoked,
/// therefore all C++ exceptions are converted to exceptional futures.
///
/// In order to perform `throw`/`make_exception_future<>(std::current_exception())`,
/// you should prefer `handle.into_future()` instead of `handle.into_result()`.
/// Unlike the latter, the former avoids rethrowing the exception and just
/// returns an exceptional future from a captured exception pointer.
///
/// Important note about preemption: if the `try` body returns a failure
/// without preemption, then the `catch` handler will be called without
/// preemption, too.
template<ResultFuturizeTryBody Fun,
ResultCatcherMaybeDots<internal::futurizing_converter,
typename seastar::futurize_t<std::invoke_result_t<Fun>>::value_type>... Handlers>
seastar::futurize_t<std::invoke_result_t<Fun>>
result_futurize_try(Fun&& fun, Handlers&&... handlers) {
using fun_return_type = std::invoke_result_t<Fun>;
using future_type = seastar::futurize_t<fun_return_type>;
using result_type = typename future_type::value_type;
using try_catch_chain_type = internal::try_catch_chain_impl<result_type, internal::futurizing_converter, Handlers...>;
// Try to invoke the function and handle C++ exceptions if there are any
bool no_exceptions = false;
future_type f = seastar::futurize_invoke([&] () {
return try_catch_chain_type::template invoke_in_try_catch<>([&no_exceptions, &fun] (bool&) {
// If `fun` didn't throw, we can set the `no_exceptions` flag
// so that we know that there was a throw and we shouldn't
// run the exception handling logic again.
fun_return_type ret = fun();
no_exceptions = true;
return seastar::futurize<fun_return_type>::convert(std::move(ret));
}, handlers...);
});
if (!__builtin_expect(no_exceptions, true)) {
// There were C++ exceptions, and we handled them already
return f;
}
// Although then_wrapped should call the lambda without preemption,
// this is not guaranteed in debug mode, and I feel that the compiler
// struggles to generate efficient code for this case in release mode.
// We manually check if the future is available so that we can ensure
// the guarantee stated in the comment.
if (f.available()) {
return internal::result_handle_available_future(std::move(f), handlers...);
}
return f.then_wrapped([...handlers = std::move(handlers)] (future_type&& f) mutable -> future_type {
return internal::result_handle_available_future(std::move(f), handlers...);
});
}
/// \brief Represents a `catch (const Ex& ex) {}` part in a `result_try` chain.
///
/// The callback must be a generic lambda, and be callable with one
/// of the following sets of arguments:
///
/// (const Ex&)
/// (const Ex&, Handle&&)
///
/// where `Handle` is satisfies the ExceptionHandle<R> concept
/// and `R` is the result return type.
///
/// See the description of `result_try` for more info.
template<typename Ex, typename Cb>
auto result_catch(Cb&& cb) {
return internal::result_catcher<Ex, Cb>(std::move(cb));
}
/// \brief Represents a `catch (...) {}` part in a `result_try` chain.
///
/// The callback must be a generic lambda, and be callable with one
/// of the following sets of arguments:
///
/// ()
/// (Handle&&)
///
/// where `Handle` is satisfies the ExceptionHandle<R> concept
/// and `R` is the result return type.
///
/// See the description of `result_try` for more info.
template<typename Cb>
auto result_catch_dots(Cb&& cb) {
return internal::result_catcher_dots<Cb>(std::move(cb));
}
}