Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ set(TARGET_LIBRARY beman_${TARGET_NAME})
set(TARGET_ALIAS beman::${TARGET_NAME})
set(TARGETS_EXPORT_NAME ${CMAKE_PROJECT_NAME})

option(BEMAN_NET_WITH_URING "Enable liburing io context" OFF)

include(FetchContent)
FetchContent_Declare(
execution
Expand Down
6 changes: 6 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ set(xEXAMPLES taps)
foreach(EXAMPLE ${EXAMPLES})
set(EXAMPLE_TARGET ${TARGET_PREFIX}.examples.${EXAMPLE})
add_executable(${EXAMPLE_TARGET})
if(BEMAN_NET_WITH_URING)
target_compile_definitions(
${EXAMPLE_TARGET}
PRIVATE BEMAN_NET_USE_URING
)
endif()
target_sources(${EXAMPLE_TARGET} PRIVATE ${EXAMPLE}.cpp)
target_link_libraries(${EXAMPLE_TARGET} PRIVATE ${TARGET_LIBRARY})
target_link_libraries(${EXAMPLE_TARGET} PRIVATE beman::task)
Expand Down
6 changes: 5 additions & 1 deletion examples/demo_task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,12 @@ struct task {
state->callback.reset();
state->handle->stop_state = task::stop_state::stopping;
state->handle->stop_source.request_stop();
if (state->handle->stop_state == task::stop_state::stopped)
if (state->handle->stop_state == task::stop_state::stopped) {
this->object->handle->state->complete_stopped();
} else {
// transition back to running so sender_awaiter::stop() can safely complete later
state->handle->stop_state = task::stop_state::running;
}
}
};
using stop_token = decltype(ex::get_stop_token(ex::get_env(::std::declval<Receiver>())));
Expand Down
11 changes: 9 additions & 2 deletions include/beman/net/detail/io_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
#include <beman/net/detail/container.hpp>
#include <beman/net/detail/context_base.hpp>
#include <beman/net/detail/io_context_scheduler.hpp>
#ifdef BEMAN_NET_USE_URING
#include <beman/net/detail/uring_context.hpp>
#else
#include <beman/net/detail/poll_context.hpp>
#endif
#include <beman/net/detail/repeat_effect_until.hpp>
#include <beman/execution/execution.hpp>

#include <cstdint>
#include <sys/socket.h>
#include <unistd.h>
#include <poll.h>
#include <cerrno>
#include <csignal>
#include <limits>
Expand All @@ -33,8 +36,12 @@ class io_context;

class beman::net::io_context {
private:
#ifdef BEMAN_NET_USE_URING
::std::unique_ptr<::beman::net::detail::context_base> d_owned{new ::beman::net::detail::uring_context()};
#else
::std::unique_ptr<::beman::net::detail::context_base> d_owned{new ::beman::net::detail::poll_context()};
::beman::net::detail::context_base& d_context{*this->d_owned};
#endif
::beman::net::detail::context_base& d_context{*this->d_owned};

public:
using scheduler_type = ::beman::net::detail::io_context_scheduler;
Expand Down
321 changes: 321 additions & 0 deletions include/beman/net/detail/uring_context.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
// include/beman/net/detail/uring_context.hpp -*-C++-*-
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#ifndef INCLUDED_BEMAN_NET_DETAIL_URING_CONTEXT
#define INCLUDED_BEMAN_NET_DETAIL_URING_CONTEXT

#include <beman/net/detail/container.hpp>
#include <beman/net/detail/context_base.hpp>

#include <cassert>
#include <cstdint>
#include <system_error>
#include <tuple>
#include <liburing.h>

namespace beman::net::detail {

// io_context implementation based on liburing
struct uring_context final : context_base {
static constexpr unsigned QUEUE_DEPTH = 128;
::io_uring ring;
container<native_handle_type> sockets;
task* tasks = nullptr;
::std::size_t submitting = 0; // sqes not yet submitted
::std::size_t outstanding = 0; // cqes expected

uring_context() {
int flags = 0;
int r = ::io_uring_queue_init(QUEUE_DEPTH, &ring, flags);
if (r < 0) {
throw ::std::system_error(-r, ::std::system_category(), "io_uring_queue_init failed");
}
}
~uring_context() override { ::io_uring_queue_exit(&ring); }

auto make_socket(int fd) -> socket_id override { return sockets.insert(fd); }

auto make_socket(int d, int t, int p, ::std::error_code& error) -> socket_id override {
int fd(::socket(d, t, p));
if (fd < 0) {
error = ::std::error_code(errno, ::std::system_category());
return socket_id::invalid;
}
return make_socket(fd);
}

auto release(socket_id id, ::std::error_code& error) -> void override {
const native_handle_type handle = sockets[id];
sockets.erase(id);
if (::close(handle) < 0) {
error = ::std::error_code(errno, ::std::system_category());
}
}

auto native_handle(socket_id id) -> native_handle_type override { return sockets[id]; }

auto set_option(socket_id id, int level, int name, const void* data, ::socklen_t size, ::std::error_code& error)
-> void override {
if (::setsockopt(native_handle(id), level, name, data, size) < 0) {
error = ::std::error_code(errno, ::std::system_category());
}
}

auto bind(socket_id id, const endpoint& ep, ::std::error_code& error) -> void override {
if (::bind(native_handle(id), ep.data(), ep.size()) < 0) {
error = ::std::error_code(errno, ::std::system_category());
}
}

auto listen(socket_id id, int no, ::std::error_code& error) -> void override {
if (::listen(native_handle(id), no) < 0) {
error = ::std::error_code(errno, ::std::system_category());
}
}

auto submit() -> void {
int r = ::io_uring_submit(&ring);
if (r < 0) {
throw ::std::system_error(-r, ::std::system_category(), "io_uring_submit failed");
}
assert(submitting >= r);
submitting -= r;
outstanding += r;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this logic is guaranteed to be accurate! I had the impression that items can be picked up by the kernel without them actually getting submitted: once the queue is updated (i.e., the SQE pointer(s) is(are) set and the count is increased) work can be picked up. The call to ::io_uring_submit is used to make sure things get picked up. On the other hand, the man page explicitly mentions that SQPOLL affects whether the value is correct. It also implies that the primary use of tracking submissions it to determine whether the data pointed to by the SQE needs to remain valid.

My inclination would be to update outstanding every time to work gets added to the context and have it be independent of whether the work still needs to be submitted: every work item added needs to be completed, i.e., outstanding would indicate how much work there is still to do. The current value is how much submitted work is still to do which seems to muddle two separate concerns. It is reasonable (possibly necessary) to separately track submitting to determine whether any work still needs to be submitted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this logic is guaranteed to be accurate!

i am expecting this return value to be accurate because our io_uring_queue_init() call is not passing the IORING_SETUP_SQPOLL flag documented in io_uring_setup()

My inclination would be to update outstanding every time to work gets added to the context and have it be independent of whether the work still needs to be submitted.

thanks, this would make it easier to experiment with SQPOLL in the future - done

}

auto get_sqe(io_base* completion) -> ::io_uring_sqe* {
auto sqe = ::io_uring_get_sqe(&ring);
while (sqe == nullptr) {
// if the submission queue is full, flush and try again
submit();
sqe = ::io_uring_get_sqe(&ring);
}
Comment on lines +87 to +91
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit I tripped over the last time I tried to review the code thoroughly: this is, effectively, a busy wait for an indefinite amount of time. The assumption would be that it actually isn't hit (especially if the interface eventually gets enhanced to cope with sequences of results with just one submission) so it shouldn't be a problem (and I'm won't push back on this one).

I was thinking about alternative approaches. One idea is to keep track of unsubmitted work. The io_base object could be put into a [singly-linked] intrusive list of unsubmitted work which gets submitted on next "opportunity". Of course, this opportunity may not come unless there is something making sure it comes. A potential for that could be to submit an IORING_OP_NOP operation (io_uring_prep_nop) using the last empty slot in the submission queue to trigger processing of the unsubmitted work queue (if any).

Also, as the submission is communication between the program and the kernel, this loop shouldn't block indefinitely in any case. I may be entirely considering the wrong problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in a single-threaded application, i am assuming that io_uring_submit() will reliably drain the submission queue and guarantee that the next io_uring_get_sqe() has something to return. if my assumption is wrong, we might avoid an infinite loop here by throwing after a single submit/retry fails

if many threads are submitting work, then it's possible for enough io_uring_get_sqe() calls (QUEUE_DEPTH=128) to occur between this thread's calls to io_uring_submit() and io_uring_get_sqe(). however, i've made no considerations for thread-safety in this design. can you please clarify the requirements here?

  1. can io_context::run() be called multiple times to establish a pool of worker threads? if so, we probably want a separate ring for each but this gets complicated
  2. can accept()/connect()/etc be called to submit work from multiple threads? if so, we would need to add their io_bases to a thread-safe queue so that run_one() alone is calling into liburing

::io_uring_sqe_set_data(sqe, completion);
++submitting;
return sqe;
}

auto wait() -> ::std::tuple<int, io_base*> {
::io_uring_cqe* cqe = nullptr;
int r = ::io_uring_wait_cqe(&ring, &cqe);
if (r < 0) {
throw ::std::system_error(-r, ::std::system_category(), "io_uring_wait_cqe failed");
}

assert(outstanding > 0);
--outstanding;

const int res = cqe->res;
const auto completion = ::io_uring_cqe_get_data(cqe);
::io_uring_cqe_seen(&ring, cqe);

return {res, static_cast<io_base*>(completion)};
}

auto run_one() -> ::std::size_t override {
if (auto count = process_task(); count) {
return count;
}
Comment on lines +116 to +118
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This special treatment of tasks gives the tasks priority over outstanding I/O. I would consider scheduling the tasks as a IORING_OP_NOP (::io_uring_prep_nop) which should achieve two things:

  1. There is no special case in run_one needed to process tasks.
  2. Even if there are always tasks ready to be scheduled, completions of I/O operations get a chance to run.

If using IORING_OP_NOP isn't a good idea (I'm not experienced with io_uring to tell) my inclination would be to still give some priority to I/O operations over scheduled tasks: I could imagine a strategy which makes sure that there is always something [hopefully short] to run instead of waiting on I/O to come in. However, if there is always a task scheduled, I think I/O never gets a chance. So, the alternative approach would be to:

  1. ::io_uring_submit work if any is unsubmitted.
  2. Use ::io_uring_peek_cqe to see if I/O is ready and, if so, process that.
  3. Otherwise, see if there is a task outstanding which can be scheduled.
  4. Otherwise (when there is no work which can be progressed) wait().

If that is believed to give too much priority to I/O operations a variation alternating between preferring I/O or scheduled tasks could be used. Submitting IORING_OP_NOP items would kind of do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if these "tasks" are related to scheduling, is it safe to assume that process_task() is likely just calling some chained sender that will immediately start more i/o? if so, processing these tasks before io_uring_submit() has the advantage that we can batch those new sqes with any that were already pending to minimize the number of uring system calls

for example, consider 3 concurrent calls to net::resume_after(scheduler, duration). ideally, this would make 3 calls to io_uring_prep_timeout() then one io_uring_submit() for all 3 - whereas your alternative would issue a separate io_uring_submit() after each io_uring_prep_timeout()

it's probably worth prioritizing completions over io_uring_submit() too, as they may also prepare chained work that can be batched. there's also the possibility that the kernel has overflowed the completion queue, which would apparently cause io_uring_submit() to fail with EBUSY. using io_uring_peek_cqe() to drain completions first should help to avoid this

what do you think about the following order?

  1. if io_uring_peek_cqe() finds a ready completion, process it and return
  2. if there are pending tasks, process one and return
  3. io_uring_submit() if sqes are pending
  4. io_uring_wait_cqe() to wait for a completion to process


if (submitting) {
// if we have anything to submit, batch the submit and wait in a
// single system call. this allows io_uring_wait_cqe() below to be
// served directly from memory
unsigned wait_nr = 1;
int r = ::io_uring_submit_and_wait(&ring, wait_nr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this ::io_uring_submit_and_wait instead of just ::io_uring_submit? The wait() function will ::io_uring_wait_cqe, i.e., waiting will occur if necessary anyway (if no results are ready).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/axboe/liburing/wiki/io_uring-and-networking-in-2023#batching recommends the use of io_uring_submit_and_wait() to minimize the number of io_uring_enter() system calls in "IO event loops" like ours

my understanding is that:

  • io_uring_submit() calls io_uring_enter() to submit sqes to the kernel, and
  • io_uring_wait_cqe() calls io_uring_enter() to wait only if there are no ready cqes in userspace memory

io_uring_submit_and_wait() does both in a single system call, guaranteeing that our later call to io_uring_wait_cqe() can be served from memory as if by io_uring_peek_cqe()

if (r < 0) {
throw ::std::system_error(-r, ::std::system_category(), "io_uring_submit_and_wait failed");
}
assert(submitting >= r);
submitting -= r;
outstanding += r;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned previously, I think I wouldn't tie submitting and outstanding: I don't see a reason why outstanding can't just track how much outstanding work there is, independent of whether it was submitted.

}

if (!outstanding) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think testing outstanding alone is correct: I don't see why ::io_uring_submit_and_wait needs to return a non-zero result when it called, i.e., submitting may be non-zero even though outstanding is zero. I entirely agree that is very unlikely that ::io_uring_submit_and_wait (or io_uring_submit) would return zero, though.

Of course, if outstanding just tracks the total outstanding work, this concern is addressed, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, if outstanding just tracks the total outstanding work, this concern is addressed, too.

i did apply this change to outstanding, but i don't think it fully addresses this concern with a 0-return from io_uring_submit_and_wait(). if we prepared some sqes but none were successfully submitted to the kernel, our call to io_uring_wait_cqe() could deadlock waiting for a completion that isn't ever coming

before the change to outstanding, a failure to submit any sqes could instead cause io_context::run() to return early

since this breaks our implementation either way, maybe it's best to assert or throw on an unexpected 0-return? if we do end up finding cases where this happens, the reproducer may give us more information to guide the design

// nothing to submit and nothing to wait on, we're done
return 0;
}

// read the next completion, waiting if necessary
auto [res, completion] = wait();

if (completion) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe all operations are submitted with a completion set (in get_sqe; peeking ahead I see that cancel [currently] uses a nullptr but I don't think that is right), i.e., this check shouldn't be needed. If this check is needed, there may have been nothing completed but the function still returns 1u.

// work() functions depend on res, so pass it in via 'extra'
completion->extra.reset(&res);
completion->work(*this, completion);
}

return 1;
}

auto cancel(io_base* cancel_op, io_base* op) -> void override {
auto sqe = get_sqe(nullptr);
int flags = 0;
::io_uring_prep_cancel(sqe, op, flags);

// use io_uring_prep_cancel() for asynchronous cancellation of op.
// cancel_op, aka sender_state::cancel_callback, lives inside of op's
// operation state. op's completion may race with this cancellation,
// causing that sender_state and its cancel_callback to be destroyed.
// so we can't pass cancel_op to io_uring_sqe_set_data() and attach a
// cancel_op->work() function to handle its completion in run_one().
// instead, we just complete it here without waiting for the result
cancel_op->complete();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct! Yes, cancellation and completion are racy! However, the senders should be (need to be) implemented such that the number of outstanding completions is accurately tracked! That is, if an operation gets cancelled, its count of outstanding completions needs to be first incremented and then any actual cancellation gets submitted. Only once the count is incremented the cancellation state is created. I believe this is the relevant code: if outstanding was incremented beyond 1u, the operation wasn't completed, yet, and with outstanding being 2u a non-cancellation completion won't complete the operation.

If the logic I tried to outline above doesn't work as described, it is a bug which needs fixing (this wouldn't be part of this pull request: it would be a separate issue).

Looking at the current implementation I'm not entirely happy with the behaviour: if cancellation was triggered but there was a successful completion before the cancellation completes (which is a potential in particular with io_uring but can't happen with poll, epoll, or kqueue, I think), the ultimate result is currently set_stopped(...) although it could be set_value(...).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only once the count is incremented the cancellation state is created. I believe this is the relevant code: if outstanding was incremented beyond 1u, the operation wasn't completed, yet, and with outstanding being 2u a non-cancellation completion won't complete the operation.

thanks for the context! i added a squash commit that restores the asynchronous completion of uring_context::cancel(), and was able to reproduce the original crash

i believe the problem is that sender_state::complete() and error() both destroy this cancel_callback even if a cancellation is pending:

auto complete() -> void override final {
d_callback.reset();
if (0 == --this->d_outstanding) {
this->d_data.set_value(*this, ::std::move(this->d_receiver));

the crash no longer reproduces if i move the d_callback.reset() line inside that if-block. does that look right?

If the logic I tried to outline above doesn't work as described, it is a bug which needs fixing (this wouldn't be part of this pull request: it would be a separate issue).

i added this change as an additional commit to this pr, but i'm happy to raise a separate pr too if you like. similar to the demo::task fix, it only applies to async cancellation which is specific to uring_context

}

auto schedule(task* t) -> void override {
t->next = tasks;
tasks = t;
}

auto process_task() -> ::std::size_t {
if (tasks) {
auto* t = tasks;
tasks = t->next;
t->complete();
return 1u;
}
return 0u;
}

auto accept(accept_operation* op) -> submit_result override {
op->work = [](context_base& ctx, io_base* io) {
auto res = *static_cast<int*>(io->extra.get());
if (res == -ECANCELED) {
io->cancel();
return submit_result::ready;
} else if (res < 0) {
io->error(::std::error_code(-res, ::std::system_category()));
return submit_result::error;
}
auto op = static_cast<accept_operation*>(io);
// set socket
::std::get<2>(*op) = ctx.make_socket(res);
io->complete();
return submit_result::ready;
};

auto sqe = get_sqe(op);
auto fd = native_handle(op->id);
auto addr = ::std::get<0>(*op).data();
auto addrlen = &::std::get<1>(*op);
int flags = 0;
::io_uring_prep_accept(sqe, fd, addr, addrlen, flags);
return submit_result::submit;
Comment on lines +183 to +205
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks as if this code is stamped out a few times below (I haven't verified if the blocks are actually mostly identical). I think I would factor most of the logic into a function which may need to take two [lambda] functions (one wrapping the "prep" function and one producing the result) as arguments. Also, I probably wouldn't create variables out of the various op accesses but just pass them to the "prep" function. I do admit that it helps with readability spelling out what the various members are, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I probably wouldn't create variables out of the various op accesses but just pass them to the "prep" function. I do admit that it helps with readability spelling out what the various members are, though.

i do like to give names to function arguments. while it uses some extra vertical space, each of these functions still fits nicely on a single page in the editor 🤷

It looks as if this code is stamped out a few times below (I haven't verified if the blocks are actually mostly identical).

each of the completion lambdas differ slightly in how they map error codes and store return values, if any. each of the prepare blocks differ significantly in the io_uring_prep_* function and arguments

I think I would factor most of the logic into a function which may need to take two [lambda] functions (one wrapping the "prep" function and one producing the result) as arguments.

i don't really see the benefit of a helper function that combines the two, assuming it just looks something like this:

auto prepare_sqe(io_base* op, auto prepare, auto complete) -> submit_result {
	op->work = std::move(complete);
	return prepare(op);
}

}

auto connect(connect_operation* op) -> submit_result override {
op->work = [](context_base&, io_base* io) {
auto res = *static_cast<int*>(io->extra.get());
if (res == -ECANCELED) {
io->cancel();
return submit_result::ready;
} else if (res < 0) {
io->error(::std::error_code(-res, ::std::system_category()));
return submit_result::error;
}
io->complete();
return submit_result::ready;
};

auto sqe = get_sqe(op);
auto fd = native_handle(op->id);
auto& addr = ::std::get<0>(*op);
::io_uring_prep_connect(sqe, fd, addr.data(), addr.size());
return submit_result::submit;
}

auto receive(receive_operation* op) -> submit_result override {
op->work = [](context_base&, io_base* io) {
auto res = *static_cast<int*>(io->extra.get());
if (res == -ECANCELED) {
io->cancel();
return submit_result::ready;
} else if (res < 0) {
io->error(::std::error_code(-res, ::std::system_category()));
return submit_result::error;
}
auto op = static_cast<receive_operation*>(io);
// set bytes received
::std::get<2>(*op) = res;
io->complete();
return submit_result::ready;
};

auto sqe = get_sqe(op);
auto fd = native_handle(op->id);
auto msg = &::std::get<0>(*op);
auto flags = ::std::get<1>(*op);
::io_uring_prep_recvmsg(sqe, fd, msg, flags);
return submit_result::submit;
}

auto send(send_operation* op) -> submit_result override {
op->work = [](context_base&, io_base* io) {
auto res = *static_cast<int*>(io->extra.get());
if (res == -ECANCELED) {
io->cancel();
return submit_result::ready;
} else if (res < 0) {
io->error(::std::error_code(-res, ::std::system_category()));
return submit_result::error;
}
auto op = static_cast<send_operation*>(io);
// set bytes sent
::std::get<2>(*op) = res;
io->complete();
return submit_result::ready;
};

auto sqe = get_sqe(op);
auto fd = native_handle(op->id);
auto msg = &::std::get<0>(*op);
auto flags = ::std::get<1>(*op);
::io_uring_prep_sendmsg(sqe, fd, msg, flags);
return submit_result::submit;
}

static auto make_timespec(auto dur) -> __kernel_timespec {
auto sec = ::std::chrono::duration_cast<::std::chrono::seconds>(dur);
dur -= sec;
auto nsec = ::std::chrono::duration_cast<::std::chrono::nanoseconds>(dur);
__kernel_timespec ts;
ts.tv_sec = sec.count();
ts.tv_nsec = nsec.count();
return ts;
}

auto resume_at(resume_at_operation* op) -> submit_result override {
auto at = ::std::get<0>(*op);
op->work = [](context_base&, io_base* io) {
auto res = *static_cast<int*>(io->extra.get());
auto op = static_cast<resume_at_operation*>(io);
if (res == -ECANCELED) {
io->cancel();
return submit_result::ready;
} else if (res == -ETIME) {
io->complete();
return submit_result::ready;
}
io->error(::std::error_code(-res, ::std::system_category()));
return submit_result::error;
};

auto sqe = get_sqe(op);
auto ts = make_timespec(at.time_since_epoch());
unsigned count = 0;
unsigned flags = IORING_TIMEOUT_ABS | IORING_TIMEOUT_REALTIME;
::io_uring_prep_timeout(sqe, &ts, count, flags);

// unlike other operations whose submissions can be batched in run_one(),
// the timeout argument to io_uring_prep_timeout() is a pointer to memory
// on the stack. this memory must remain valid until submit, so we either
// have to call submit here or allocate heap memory to store it
submit();
return submit_result::submit;
}
};

} // namespace beman::net::detail

#endif
Loading