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. There is no long-lived object whose expiry must be reset.

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.

Delaying on a Different Clock

delay(time_point) also accepts a time point on clocks other than steady_clock. One example is std::chrono::system_clock, for a deadline naturally expressed as wall-clock time rather than a monotonic duration:

auto deadline = std::chrono::system_clock::now() + std::chrono::minutes(5);
auto [ec]     = co_await corosio::delay(deadline);

Internally this performs one or more bounded steady_clock waits, re-reading the clock between them. An adjustment to the clock is therefore observed at the next re-check rather than only at the original deadline. Pass a custom traits type as delay<Traits>(time_point) to bound how quickly such an adjustment is observed:

// Re-read the wall clock at least once per second, so a step of
// the clock is observed within that bound
struct capped_traits
{
    static std::chrono::system_clock::duration
    to_wait_duration(std::chrono::system_clock::duration d)
    {
        auto cap = std::chrono::system_clock::duration(std::chrono::seconds(1));
        return d < cap ? d : cap;
    }
};

auto deadline = std::chrono::system_clock::now() + std::chrono::minutes(5);
auto [ec]     = co_await corosio::delay<capped_traits>(deadline);

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. It is not whatever the cancelled operation happened to leave behind. Because the deadline-win path default-initializes that payload, the inner operation’s io_result payload type must be default constructible.

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: 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();
        auto [dec] = co_await corosio::delay(500ms);
        if (dec == capy::cond::canceled)
            co_return;
    }
}

Next Steps