cppgraphqlgen icon indicating copy to clipboard operation
cppgraphqlgen copied to clipboard

Any real-world examples where the coroutine backing can be applied?

Open AndrewLipscomb opened this issue 2 years ago • 6 comments

I've been having a lot of ... fun implementing boost::asio alongside this lib - mainly due to the fact that the ASIO coroutine co_await handler looks like this

  // Support for co_await keyword.
  template <class U>
  void await_suspend(
      detail::coroutine_handle<detail::awaitable_frame<U, Executor>> h)
  {
    frame_->push_frame(&h.promise());
  }

Obviously, once cppgraphqlgen's resolve function has been entered, all the context needed for filling out the std::coroutine_handle<detail::awaitable_frame<U, Executor>> disappears, and is replaced with graphql::service::AwaitableObject<#value>::promise_type

My not-really-ideal workaround has been

  • Enter the GraphQL system from a boost::asio::awaitable context
  • Drop into the cppgraphqlgen resolve context, lose my immediate boost::asio context and have all functions return std::future<T> to fullfill the graphql::service::AwaitableObject
  • Use boost::asio:co_spawn to restart the boost::asio::awaitable contexts and return futures via boost::asio::use_future

I'm curious that due to the fact that boost::asio::awaitable's context is basically the internal guts for its actual coroutine-execution-operation-element-bits, are there any other good examples for async-execution frameworks/libraries that can be used with this library and don't require modifying that underlying service::Awaitable type to be aware of how the async library works?

I suppose for comparison, I'm thinking of C libraries which do a sync version of their calls, then specialise for libuv, libevent etc - I am really hoping that C++ coroutines give us some means of abstracting the libuv-equivalent away, but given my experience so far, I'm guessing maybe it can't.

AndrewLipscomb avatar Sep 06 '22 22:09 AndrewLipscomb

I'm not sure if I understand the question, it's been a while since I played with Boost.asio, and I wasn't using C++ coroutines yet at the time. Can you point me to a minimal sample of your workaround?

The idea I used for integrating with other coroutines was to make the type-erased service::await_async type. The unit tests in test/CoroutineTests.cpp demonstrate passing different default implementations wrapped in service::await_async, including the std::suspend_never type for synchronous/blocking execution. If you can wrap an awaitable from Boost.asio in service::await_async, you can get it to return control to that executor at suspend points in each resolver.

If you're implementing a coroutine with a Boost.asio awaitable return type, you should still be able to co_await on the top-level resolve call. If the custom awaitable never suspends inside of the resolver, I think it'll just run synchronously and block the current thread.

So, I think the best way to integrate with Boost.asio is probably to figure out how to wrap its awaitable type in graphql::service::await_async as a type-erased awaitable. Then it should call back into Boost.asio's await_suspend and await_resume methods/functions. Inside of your Boost.asio coroutine, use co_await to get the result of the top-level resolver, and C++20 should wire up the suspend points in a way that's compatible in the other direction with Boost.asio.

wravery avatar Sep 07 '22 00:09 wravery

Can you point me to a minimal sample of your workaround?

I'll try to write something up this arvo or tomorrow

I think the best way to integrate with Boost.asio is probably to figure out how to wrap its awaitable type in graphql::service::await_async as a type-erased awaitable

Yeah that is what I'm trying to chase now, hitting the issue of how do I "bridge" the Boost::Asio coro-promise type with the graphqlgen::promise-type - which I think is primarily due to how Boost Asio has implemented their coro-promise type and me just not understanding the coro-promise idea really well. But thats for me to nut out.

I suppose my question is more general - its more around the idea that given your library makes no assumption on executor/event-loop mechanics behind the scenes, can an off-the-shelf implementation of a general-purpose-user-level async library be integrated without needing to have all its specifics be known in advance. I was unsure if you'd written this in mind with plugging it into another async framework that might illustrate how that would work.

This is in comparison to how the same async-awareness concepts get implemented in C, which from what I've seen is along the lines of "reimplement helpers for the 3x big event loop libraries (libuv, libevent, libev) specifically" - nothing "general" about it. But the one nice thing about that style of implementation is that if you know your networking lib uses libuv and your DB driver uses libuv and this lib uses libuv - then it all should go together with minimal glue.

Boost::Asio is a good example here given its conceptual relevance to the Networking TS - so I'll try and write up the minimal example of what I've gotten here (which is basically a HTTP server, GraphQL behind it, and a proxied HTTP client call behind a resolver)

AndrewLipscomb avatar Sep 07 '22 01:09 AndrewLipscomb

Made https://github.com/ALTinners/cppgraphql-asio-demo-problem as an example of something reasonably indicative

So thats a hacked up copy of the Boost Beast HTTP code from https://www.boost.org/doc/libs/1_80_0/libs/beast/doc/html/beast/examples.html - with C++20 coroutine implementations instead of callbacks for most async capable operations

It basically gets a HTTP request, proxies a request off to http://google.com:80 for its status code (301) and returns it

Of interest mainly are the two calls into and out-of GQL https://github.com/ALTinners/cppgraphql-asio-demo-problem/blob/739890e77a20be1a125c1b5ce02dab5f6065e038/src/main.cpp#L241 goes into GQL - but co_await won't work due to the change of coro-promise-type https://github.com/ALTinners/cppgraphql-asio-demo-problem/blob/739890e77a20be1a125c1b5ce02dab5f6065e038/src/main.cpp#L88 is the proxied call - again co_await won't work due to the promise type change

I've got to get on with some other stuff this arvo before I throw some more time at trying to get a await_async that might have some capability of handling this - but AFAIK I can't ever do a simple co_await for the Boost async routines on the resolver side of the Operations object, because of the change of coro-promise type.

AndrewLipscomb avatar Sep 07 '22 03:09 AndrewLipscomb

I took a look at your sample, and I think this may require a change in cppgraphqlgen, specifically for the awaitable types. The await_suspend call only takes a coroutine_handle<void>, whereas Boost.asio uses a template method to specialize coroutine_handle<U> for other promise types, including its own. IOW, I think I understand your question now. 😆

The type-erased awaitable may be flexible enough already, I also haven't fully investigated that.

wravery avatar Sep 07 '22 14:09 wravery

I tried some tweaks to AwaitableScalar<>::await_suspend, but I wasn't able to get it to work with the boost::asio::awaitable<> promise_type (or vice versa). I think the lack of executors in the standard may just mean that there's not a good way to do that yet in C++20.

I was able to use the type-erased graphql::service::await_async as a graphql::service::RequestResolveParams::launch option to get it to at least push execution to the boost::asio executor:

struct await_executor : coro::suspend_always
{
    using asio_executor = boost::asio::any_io_executor;

    await_executor(asio_executor &&executor)
        : m_executor{std::move(executor)}
    {
    }

    void await_suspend(coro::coroutine_handle<> h)
    {
        m_future = boost::asio::co_spawn(
            m_executor,
            [h]() -> boost::asio::awaitable<void>
            {
                h.resume();
                co_return;
            },
            boost::asio::use_future);
    }

    void await_resume()
    {
        if (m_future)
        {
            m_future->get();
            m_future.reset();
        }
    }

private:
    asio_executor m_executor;
    std::optional<std::future<void>> m_future;
};

Since AwaitableScalar<>::await_suspend will spin up another thread for a future, I also had it block the thread and return the value from the std::future<> returned by boost::asio::co_spawn with use_future. That way the only extra threads are the ones that co_spawn uses on its executor to start the async task and wrap it in a future:

    std::optional<int> getStatusCode(std::string addressArg)
    {
        return getAsyncCode(addressArg).get();
    }

wravery avatar Sep 08 '22 17:09 wravery

I think maybe I could implement something like this on AwaitableScalar<T>::promise_type as:

template <typename U>
AwaitableScalar<T> await_transform(U awaitable)
{
    // ...
}

That should implicitly convert co_await for boost::asio::awaitable<T> to an AwaitableScalar<T>. It would probably still need some sort of adapter type though, like await_executor because the logic for how you get from one type of awaitable to the other needs to be provided by the integrator.

It wouldn't work the other way either, where a boost::asio::awaitable<T> coroutine could co_await on an AwaitableScalar<T>, unless one of boost::asio::awaitable<T>'s await_transform template methods support plugging in another type/helper. If not, maybe there's a way to declare an overloaded operator co_await which can map from AwaitableScalar<T> back to boost::asio::awaitable<T>.

wravery avatar Sep 08 '22 22:09 wravery