Naios / continuable

C++14 asynchronous allocation aware futures (supporting then, exception handling, coroutines and connections)
https://naios.github.io/continuable/
MIT License
823 stars 44 forks source link

Missing failure chaining support #9

Closed HatefulMoron closed 5 years ago

HatefulMoron commented 5 years ago

@Naios


To better support my asynchronous operations, I would like to create a wrapper on my asynchronous chain to release a mutex regardless of the outcome of the chain, for example;

do_stuff().then(/* ... */).next(invariant_unlock<int>{M_mut});

To accomplish this, I wrote a supporting type invariant_unlock which looks similar to:

template <typename... Ret_Ts> struct invariant_unlock
{
    explicit invariant_unlock(std::mutex& mutex) : M_mutex(mutex)
    {
    }
    template <typename... Ts> auto operator()(Ts&&... ts)
    {
        M_mutex.unlock();
        return std::make_tuple<Ts...>(std::forward<Ts>(ts)...);
    }
    auto operator()(cti::dispatch_error_tag, std::exception_ptr e_ptr)
    {
        M_mutex.unlock();
        return cti::make_exceptional_continuable<Ret_Ts...>(std::move(e_ptr));
    }
  private:
    std::mutex& M_mutex;
};

Unfortunately, using the class doesn't work, in fact I can't figure out how to chain together failure clauses at all:

auto test() {
    return cti::make_continuable<int>([](auto&& promise) { promise.set_value(5); });
}
int main() {
    test()
        .then([]() { throw std::runtime_error("a"); })
        .fail([](std::exception_ptr) { throw std::runtime_error("b"); })
        .fail([](std::exception_ptr) { std::cout<<"Ex2\n"; });
}

I never reach the second failure block, in fact the program is aborted.

I found a relative snippet from the documentation which states: Multiple handlers are allowed to be registered, however the asynchronous call hierarchy is aborted after the first called fail handler and only the closest handler below is called. Which (to me) states that the behaviour is completely expected and defined.


If it's not possible to pass a failure down the chain, I believe it would be useful as it would allow me to wrap my chain with new functionality without consuming any errors.

Thanks

Naios commented 5 years ago

Thank you for your your request.

As described by the documentation you are right, that the asynchronous control flow ends after the first error handler was called and this would need to be changed at the libraries side. So the behaviour you are trying to accomplish is currently not possible: Instead you could also store some kind of a RAII wrapper into the chain which will release the mutex on destruction.

Currently I'm thinking about rewriting that part since I also want to support recovering from errors but this will take some time. I will definitly take your request into account then.

If you are interested I've just held a talk about the library which could have some useful information for you slides.

HatefulMoron commented 5 years ago

Hey,

I figured out that RAII method you alluded to in the meantime and it seems to work perfectly for my particular case.

Thanks for your work, I'll read through the slides you linked.

Rogiel commented 5 years ago

I would like to second this. Chaining failures is a feature that I would very much appreciate too. In the current design, there is no way to represent the following pattern (in synchronous code):

void func() {
    try {
        something();
    } catch(...) {
        failures++;
        throw; // < we can't rethrow in continuables
    }
}

If we catch the exception (through the fail continuation) we can no longer propagate the error to caller, which makes it much harder to properly handle errors and maintain internal object states like the failure counter example above.

Naios commented 5 years ago

This requested feature is now fully implemented in the devel development branch, and will make it into the next major or minor update. See https://github.com/Naios/continuable/commit/4d58e3bded32585c12be7f3a59ad8599e929f3df.

It is possible to change the continuation path through recover(...), rethrow(...) or cancel() when returning a cti::result<...> from a continuation or failure handler.

auto test() {
    return cti::make_continuable<int>([](auto&& promise) { promise.set_value(5); });
}
int main() {
    test()
        .then([]() { throw std::runtime_error("a"); })
        .fail([](std::exception_ptr) { throw std::runtime_error("b"); })
        .fail([](std::exception_ptr e) { return cti::rethrow(e); })
        .fail([](std::exception_ptr e) -> cti::result<> { return cti::rethrow(e); })
        .fail([](std::exception_ptr) { std::cout<<"Ex2\n"; });
}

See the corresponding unit tests for examples: https://github.com/Naios/continuable/blob/devel/test/unit-test/multi/test-continuable-base-multipath.cpp

Rogiel commented 5 years ago

Thank you @Naios. The implementation is really good and I have already integrated it! No more variant-like wrapping and unwrapping!