Delays and Timeouts

Corosio schedules delays and deadlines through two free functions, delay() and timeout(). Neither one names an I/O object: there is no timer to construct, store as a member, move around, or close. Each call returns an awaitable that arms whatever clock resource it needs for the duration of a single co_await, then releases it. A loop that waits repeatedly, like a heartbeat or a retry backoff, just calls delay() again on the next iteration instead of resetting a long-lived object’s expiry.

Code snippets assume:

#include <boost/corosio/delay.hpp>
#include <boost/corosio/timeout.hpp>
#include <boost/capy/cond.hpp>

namespace corosio = boost::corosio;
namespace capy = boost::capy;
using namespace std::chrono_literals;

Delaying for a Duration

delay(duration) suspends the calling coroutine until the given duration elapses:

auto [ec] = co_await corosio::delay(500ms);
if (!ec)
    std::cout << "500ms elapsed\n";

A zero or negative duration completes synchronously without suspending.

Delaying Until a Time Point

delay(time_point) suspends until an absolute steady_clock time is reached. This is the right tool for periodic work, since it avoids the drift that accumulates when each iteration re-measures a relative duration from "now":

auto next = std::chrono::steady_clock::now();
for (int i = 0; i < 10; ++i)
{
    next += 100ms;
    auto [ec] = co_await corosio::delay(next);
    if (ec)
        break;
    std::cout << "Tick " << i << "\n";
}

A time point already in the past also completes synchronously.

Cancellation

delay() honors the stop token of its co_await environment. If the token is already stopped when the coroutine suspends, or becomes stopped while it is waiting, the wait ends early and resumes with capy::cond::canceled:

auto [ec] = co_await corosio::delay(10s);
if (ec == capy::cond::canceled)
    std::cout << "Delay was cancelled\n";

There is no cancel() to call directly; cancellation flows entirely through the coroutine’s stop token, the same mechanism every other Corosio operation uses.

Racing an Operation Against a Deadline

timeout(op, duration) starts an awaitable and a deadline together. Whichever finishes first decides the outcome:

auto [ec, n] = co_await corosio::timeout(
    sock.read_some(buffer), 200ms);

if (ec == capy::cond::timeout)
    std::cout << "No data within 200ms\n";
else if (!ec)
    std::cout << "Read " << n << " bytes\n";

If the inner operation wins, its io_result is returned unchanged, error or success, payload and all. If the deadline wins, the inner operation is cancelled and timeout() produces its own result: ec is capy::cond::timeout and any payload (such as a byte count) is default-initialized, not whatever the cancelled operation happened to leave behind.

timeout() also accepts an absolute deadline:

auto deadline = std::chrono::steady_clock::now() + 5s;
auto [ec] = co_await corosio::timeout(sock.connect(ep), deadline);

Distinguishing Timeout from Cancellation

A timeout() call sits inside whatever stop token its own coroutine is awaited under. If that parent token is stopped, the race ends the same way an unguarded operation would: the inner awaitable is cancelled and timeout() reports capy::cond::canceled, not capy::cond::timeout. The two conditions are never ambiguous:

Result Meaning

capy::cond::timeout

The deadline elapsed before the operation completed.

capy::cond::canceled

The coroutine’s own stop token fired, independent of the deadline.

auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s);
if (ec == capy::cond::timeout)
    std::cout << "Connect attempt timed out\n";
else if (ec == capy::cond::canceled)
    std::cout << "Connect attempt cancelled by caller\n";

Requires an io_context

Both delay() and timeout() need a clock service to arm, which they obtain from the awaiting coroutine’s executor. That executor must belong to an io_context; awaiting either one from any other kind of execution context terminates the program, since silently skipping the requested delay would be worse than a hard failure.

Composing with Other Operations

Because timeout() takes any awaitable that yields an io_result, it composes with anything Corosio returns, not just socket reads. A connect-with-retry loop pairs timeout() for the per-attempt deadline with delay() for the pause between attempts:

capy::task<>
connect_with_deadline(
    corosio::tcp_socket& sock,
    corosio::endpoint ep,
    int max_attempts)
{
    for (int attempt = 0; attempt < max_attempts; ++attempt)
    {
        auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s);
        if (!ec)
            co_return;

        if (ec == capy::cond::canceled)
            co_return;

        sock.close();
        co_await corosio::delay(500ms);
    }
}

Next Steps