this is important for rpc error reporting: if the peer disconnects we want to throw a useful error that can e.g. let pending builds finish, not kill the entire process because kj errors are non-lix exceptions. Change-Id: I41ebe10ad7fd65d886238ce52eeca1fa243b1bb2
180 lines
6.2 KiB
C++
180 lines
6.2 KiB
C++
#pragma once
|
|
///@file
|
|
|
|
#include "lix/libutil/error.hh"
|
|
#include "lix/libutil/result.hh"
|
|
#include "lix/libutil/signals.hh"
|
|
#include <future>
|
|
#include <kj/async-io.h>
|
|
#include <kj/async.h>
|
|
#include <kj/time.h>
|
|
#include <optional>
|
|
#include <source_location>
|
|
#include <type_traits>
|
|
|
|
namespace nix {
|
|
|
|
struct AsyncContext
|
|
{
|
|
static inline thread_local AsyncContext * current = nullptr;
|
|
|
|
kj::AsyncIoProvider & provider;
|
|
kj::LowLevelAsyncIoProvider & lowLevelProvider;
|
|
kj::UnixEventPort & unixEventPort;
|
|
|
|
explicit AsyncContext(kj::AsyncIoContext & aio)
|
|
: provider(*aio.provider)
|
|
, lowLevelProvider(*aio.lowLevelProvider)
|
|
, unixEventPort(aio.unixEventPort)
|
|
{
|
|
assert(current == nullptr);
|
|
current = this;
|
|
}
|
|
|
|
~AsyncContext()
|
|
{
|
|
current = nullptr;
|
|
}
|
|
|
|
KJ_DISALLOW_COPY_AND_MOVE(AsyncContext);
|
|
|
|
/**
|
|
* Wrap a promise in a timeout. `Result<void>` promises are turned into
|
|
* `Result<bool>` promises, where `true` means that the wrapped promise
|
|
* ran to completion and `false` means it timed out. Other promises are
|
|
* wrapped to return `Result<std::optional<T>>` and return `nullopt` if
|
|
* the have time out or wrap their inner type as an optional otherwise.
|
|
*/
|
|
template<typename T>
|
|
auto timeoutAfter(kj::Duration timeout, kj::Promise<Result<T>> && p)
|
|
{
|
|
using RetT = std::conditional_t<std::is_void_v<T>, bool, std::optional<T>>;
|
|
return p
|
|
.then([](Result<T> r) -> Result<RetT> {
|
|
if (r.has_value()) {
|
|
if constexpr (std::is_void_v<T>) {
|
|
return true;
|
|
} else {
|
|
return std::move(r.value());
|
|
}
|
|
} else {
|
|
return r.error();
|
|
}
|
|
})
|
|
.exclusiveJoin(provider.getTimer().afterDelay(timeout).then([]() -> Result<RetT> {
|
|
return RetT{};
|
|
}));
|
|
}
|
|
};
|
|
|
|
struct AsyncIoRoot
|
|
{
|
|
kj::AsyncIoContext kj;
|
|
AsyncContext context;
|
|
|
|
AsyncIoRoot() : kj(kj::setupAsyncIo()), context(kj) {}
|
|
KJ_DISALLOW_COPY_AND_MOVE(AsyncIoRoot);
|
|
|
|
template<typename T>
|
|
auto blockOn(
|
|
kj::Promise<T> && promise, std::source_location call_site = std::source_location::current()
|
|
);
|
|
};
|
|
|
|
inline AsyncContext & AIO()
|
|
{
|
|
assert(AsyncContext::current != nullptr);
|
|
return *AsyncContext::current;
|
|
}
|
|
|
|
namespace detail {
|
|
inline void materializeResult(Result<void> r)
|
|
{
|
|
r.value();
|
|
}
|
|
|
|
template<typename T>
|
|
inline T materializeResult(Result<T> r)
|
|
{
|
|
return std::move(r.value());
|
|
}
|
|
|
|
template<typename T>
|
|
T runAsyncUnwrap(T t)
|
|
{
|
|
return t;
|
|
}
|
|
template<typename T>
|
|
T runAsyncUnwrap(Result<T> t)
|
|
{
|
|
return std::move(t).value();
|
|
}
|
|
}
|
|
}
|
|
|
|
#define LIX_TRY_AWAIT_CONTEXT_MAP(_l_ctx, _l_map, ...) \
|
|
({ \
|
|
auto _lix_awaited = (_l_map) (co_await (__VA_ARGS__)); \
|
|
if (_lix_awaited.has_error()) { \
|
|
try { \
|
|
_lix_awaited.value(); \
|
|
} catch (::nix::BaseException & e) { \
|
|
e.addAsyncTrace(::std::source_location::current(), _l_ctx()); \
|
|
throw; \
|
|
/* NOLINTNEXTLINE(lix-foreign-exceptions) */ \
|
|
} catch (::kj::Exception & e) { \
|
|
::nix::Error fe{e.getDescription().cStr()}; \
|
|
fe.addAsyncTrace(::std::source_location::current(), _l_ctx()); \
|
|
throw fe; \
|
|
} catch (...) { \
|
|
auto fe = ::nix::ForeignException::wrapCurrent(); \
|
|
fe.addAsyncTrace(::std::source_location::current(), _l_ctx()); \
|
|
throw fe; \
|
|
} \
|
|
} \
|
|
::nix::detail::materializeResult(std::move(_lix_awaited)); \
|
|
})
|
|
|
|
#define LIX_TRY_AWAIT_CONTEXT(_l_ctx, ...) \
|
|
LIX_TRY_AWAIT_CONTEXT_MAP(_l_ctx, (std::identity{}), __VA_ARGS__)
|
|
|
|
/**
|
|
* Magic name used by `LIX_TRY_AWAIT` to insert additional context into an
|
|
* async trace frame. This name will be looked up in the local scope every
|
|
* time a try-await expression encounters an exception and then called. As
|
|
* such it can be a function, a member function name, or even a type name.
|
|
*/
|
|
static constexpr std::optional<std::string> lixAsyncTaskContext()
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
// force materialization of the value. result::value() returns only an rvalue reference
|
|
// and is thus unsuitable for use in e.g. range for without materialization. ideally we
|
|
// would wrap the expression in `auto()`, but apple clang fails when given `auto(void)`
|
|
#define LIX_TRY_AWAIT(...) LIX_TRY_AWAIT_CONTEXT(lixAsyncTaskContext, __VA_ARGS__)
|
|
|
|
#if LIX_UR_COMPILER_UWU
|
|
#define TRY_AWAIT LIX_TRY_AWAIT
|
|
#endif
|
|
|
|
template<typename T>
|
|
inline auto nix::AsyncIoRoot::blockOn(kj::Promise<T> && promise, std::source_location call_site)
|
|
try {
|
|
// always check for user interrupts. since this is c++ we must always be prepared for
|
|
// random exceptions out of literally nowhere, which is why RAII is such an important
|
|
// idiom. interruptions are also exceptions, so all exception-safe (and for promises,
|
|
// cancellation-safe) code is automatically interruption-safe. in this code base with
|
|
// its very creative approach to exception usage all promises *must* be cancellation-
|
|
// safe to not wreck system state constantly, so calling checkInterrupt is safe here.
|
|
checkInterrupt();
|
|
return detail::runAsyncUnwrap(promise.wait(kj.waitScope));
|
|
} catch (BaseException & e) {
|
|
e.addAsyncTrace(call_site);
|
|
throw;
|
|
} catch (...) {
|
|
auto fe = ForeignException::wrapCurrent();
|
|
fe.addAsyncTrace(call_site);
|
|
throw fe;
|
|
}
|