From cbff88bf35b61dada1680103d5440fe1ba774d50 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Wed, 8 Jul 2026 18:51:42 +0200 Subject: [PATCH 1/2] refactor!: replace timer, cancel_at, cancel_after with stateless delay and timeout Add delay(duration) and delay(time_point), lean awaitables over the existing reactor timer heap, and timeout(op, duration) and timeout(op, time_point), an awaitable adapter that races an inner operation against a deadline. A deadline win maps the result to error::timeout with a default payload; parent-stop cancellation surfaces unchanged as error::canceled; inner exceptions always propagate. Winner detection reads the interposed stop source and the parent token before this level requests stop, which also keeps nested timeouts correct without extra state. Remove the public timer class, cancel_at, cancel_after, native_timer, and the public io_timer wait surface. The timer class survives under detail as the engine behind delay and timeout; detail::timer_service and the scheduler hookups are unchanged. Tests, benchmarks, examples, and docs migrate to the stateless API; delay and timeout suites cover all backends including concurrency_hint == 1. --- doc/design/physical-structure.md | 4 +- doc/modules/ROOT/nav.adoc | 2 +- .../ROOT/pages/3.tutorials/3f.reconnect.adoc | 112 +- doc/modules/ROOT/pages/4.guide/4.intro.adoc | 4 +- .../4.guide/4b.concurrent-programming.adoc | 16 +- .../ROOT/pages/4.guide/4c.io-context.adoc | 5 +- .../ROOT/pages/4.guide/4c2.configuration.adoc | 5 +- doc/modules/ROOT/pages/4.guide/4h.timers.adoc | 347 ++--- .../ROOT/pages/4.guide/4i.signals.adoc | 2 +- .../ROOT/pages/4.guide/4m.error-handling.adoc | 30 +- doc/modules/ROOT/pages/4.guide/4r.wait.adoc | 17 +- .../ROOT/pages/5.testing/5a.mocket.adoc | 4 +- doc/modules/ROOT/pages/benchmark-report.adoc | 8 +- doc/modules/ROOT/pages/glossary.adoc | 17 +- doc/modules/ROOT/pages/index.adoc | 2 +- example/reconnect/reconnect.cpp | 24 +- include/boost/corosio.hpp | 4 +- include/boost/corosio/cancel.hpp | 214 --- include/boost/corosio/delay.hpp | 200 +++ .../corosio/detail/cancel_at_awaitable.hpp | 196 --- .../corosio/detail/timeout_awaitable.hpp | 244 +++ include/boost/corosio/detail/timeout_coro.hpp | 13 +- include/boost/corosio/{ => detail}/timer.hpp | 180 ++- .../boost/corosio/detail/timer_service.hpp | 169 +- include/boost/corosio/io/io_timer.hpp | 204 --- include/boost/corosio/native/native.hpp | 5 +- .../boost/corosio/native/native_cancel.hpp | 237 --- include/boost/corosio/native/native_timer.hpp | 208 --- include/boost/corosio/timeout.hpp | 103 ++ perf/bench/CMakeLists.txt | 3 - perf/bench/asio/callback/benchmarks.hpp | 3 - perf/bench/asio/callback/timer_bench.cpp | 273 ---- perf/bench/asio/coroutine/benchmarks.hpp | 3 - perf/bench/asio/coroutine/fan_out_bench.cpp | 311 ++-- perf/bench/asio/coroutine/timer_bench.cpp | 250 --- perf/bench/corosio/benchmarks.hpp | 7 - perf/bench/corosio/fan_out_bench.cpp | 158 +- perf/bench/corosio/timer_bench.cpp | 257 ---- perf/bench/main.cpp | 3 - perf/common/native_includes.hpp | 1 - src/corosio/src/timer.cpp | 65 +- test/unit/cancel.cpp | 314 ---- test/unit/connect.cpp | 10 +- test/unit/datagram_paths.cpp | 42 +- test/unit/delay.cpp | 415 +++++ test/unit/local_datagram_socket.cpp | 6 +- test/unit/local_stream_socket.cpp | 10 +- test/unit/native/native_cancel.cpp | 109 -- test/unit/native/native_timer.cpp | 130 -- test/unit/native/native_udp_socket.cpp | 22 +- test/unit/reactor_paths.cpp | 76 +- test/unit/resolver.cpp | 2 - test/unit/signal_set.cpp | 96 +- test/unit/socket_stress.cpp | 56 +- test/unit/tcp_acceptor.cpp | 31 +- test/unit/tcp_server.cpp | 65 +- test/unit/tcp_socket.cpp | 36 +- test/unit/test_utils.hpp | 163 +- test/unit/timeout.cpp | 375 +++++ test/unit/timer.cpp | 1368 ----------------- test/unit/tls_stream_stress.cpp | 21 +- test/unit/tls_stream_tests.hpp | 14 +- test/unit/udp_socket.cpp | 37 +- test/unit/wait.cpp | 143 +- 64 files changed, 2437 insertions(+), 5014 deletions(-) delete mode 100644 include/boost/corosio/cancel.hpp create mode 100644 include/boost/corosio/delay.hpp delete mode 100644 include/boost/corosio/detail/cancel_at_awaitable.hpp create mode 100644 include/boost/corosio/detail/timeout_awaitable.hpp rename include/boost/corosio/{ => detail}/timer.hpp (56%) delete mode 100644 include/boost/corosio/io/io_timer.hpp delete mode 100644 include/boost/corosio/native/native_cancel.hpp delete mode 100644 include/boost/corosio/native/native_timer.hpp create mode 100644 include/boost/corosio/timeout.hpp delete mode 100644 perf/bench/asio/callback/timer_bench.cpp delete mode 100644 perf/bench/asio/coroutine/timer_bench.cpp delete mode 100644 perf/bench/corosio/timer_bench.cpp delete mode 100644 test/unit/cancel.cpp create mode 100644 test/unit/delay.cpp delete mode 100644 test/unit/native/native_cancel.cpp delete mode 100644 test/unit/native/native_timer.cpp create mode 100644 test/unit/timeout.cpp delete mode 100644 test/unit/timer.cpp diff --git a/doc/design/physical-structure.md b/doc/design/physical-structure.md index eba103403..167a6f177 100644 --- a/doc/design/physical-structure.md +++ b/doc/design/physical-structure.md @@ -157,7 +157,6 @@ private: | io_read_file | io_object | read_at(offset, buffers) | | io_write_file | io_object | write_at(offset, buffers) | | io_file | io_read_file, io_write_file | read_at, write_at (diamond) | -| io_timer | io_object | wait(duration), cancel() | | io_signal_set | io_object | add(signal), wait(), cancel() | | io_file_watch | io_object | watch(path), wait() yielding change events | @@ -306,7 +305,7 @@ private: | Type | Base(s) | Key Operations | | ----------- | ------------- | ----------------------------------------------------------- | | process | io_object | spawn(), wait_for_exit(); stdin/stdout/stderr are pipe ends | -| timer | io_timer | expires_after(), expires_at(), wait(), cancel() | +| delay/timeout | (free functions) | delay(duration/time_point), timeout(op, duration/time_point) | | signal_set | io_signal_set | add(int), remove(int), wait(), cancel() | | serial_port | io_stream | Baud rate, parity, flow control options | | file_watch | io_file_watch | watch(path), wait() yields change events | @@ -404,7 +403,6 @@ private: | ------------------------------ | ---------------------------- | ---------------------------- | | `native_tcp_socket` | tcp_socket | Awaitables inline impl logic | | `native_tcp_acceptor` | tcp_acceptor | Awaitables inline impl logic | -| `native_timer` | timer | Awaitables inline impl logic | | `native_signal_set` | signal_set | Awaitables inline impl logic | | (aliases) | | | | epoll_tcp_socket | = `native_tcp_socket` | | diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 6edaf54aa..621adcc26 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -28,7 +28,7 @@ ** xref:4.guide/4e.tcp-acceptor.adoc[Acceptors] ** xref:4.guide/4f.endpoints.adoc[Endpoints] ** xref:4.guide/4g.composed-operations.adoc[Composed Operations] -** xref:4.guide/4h.timers.adoc[Timers] +** xref:4.guide/4h.timers.adoc[Delays and Timeouts] ** xref:4.guide/4i.signals.adoc[Signal Handling] ** xref:4.guide/4j.resolver.adoc[Name Resolution] ** xref:4.guide/4k.tcp-server.adoc[TCP Server] diff --git a/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc b/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc index 3dc80914a..790cbdf0c 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc @@ -11,7 +11,7 @@ This tutorial builds a TCP client that connects to a server and automatically reconnects with exponential backoff when the connection fails. You'll learn -how to combine timers with sockets for retry logic and how to use stop tokens +how to combine delays with sockets for retry logic and how to use stop tokens for graceful shutdown. [NOTE] @@ -19,10 +19,10 @@ for graceful shutdown. Code snippets assume: [source,cpp] ---- +#include #include #include #include -#include #include #include #include @@ -36,8 +36,8 @@ namespace capy = boost::capy; == Overview Client applications often need to maintain a persistent connection to a server. -When the server is temporarily unavailable — during a restart, a network blip, -or a deployment — the client should retry rather than give up immediately. +When the server is temporarily unavailable (during a restart, a network blip, +or a deployment), the client should retry rather than give up immediately. Retrying too aggressively wastes resources and can overwhelm a recovering server, so the delay between attempts should grow over time. @@ -47,14 +47,15 @@ brief and backs off gracefully when it isn't. This tutorial demonstrates: -* Separating the backoff _policy_ (pure state) from the _mechanism_ (timer wait) -* Using `timer` for inter-attempt delays +* Separating the backoff _policy_ (pure state) from the _mechanism_ (the + `delay()` call) +* Using `delay()` for inter-attempt delays * Graceful cancellation via stop tokens * Why `io_context::stop()` alone is not sufficient for coroutine shutdown == The Backoff Policy -The delay logic is pure computation — no I/O, no coroutines. A simple value +The delay logic is pure computation: no I/O, no coroutines. A simple value type tracks the current delay, doubles it on each call, and caps it at a configured maximum: @@ -96,8 +97,8 @@ public: With an initial delay of 500ms and a 30s cap, calling `next()` produces: 500, 1000, 2000, 4000, 8000, 16000, 30000, 30000, ... -Keeping the policy separate from the timer means it can be reused in any -context — synchronous retries, tests, or logging — without pulling in +Keeping the policy separate from the `delay()` call means it can be reused +in any context (synchronous retries, tests, or logging) without pulling in async machinery. == Session Coroutine @@ -123,12 +124,12 @@ do_session(corosio::tcp_socket& sock) ---- This is the same read loop you would find in any echo client. The interesting -part is what happens after it returns — the caller reconnects. +part is what happens after it returns: the caller reconnects. == Reconnection Loop The retry loop ties everything together. On each failed connection it asks the -backoff policy for the next delay, waits on a timer, and tries again: +backoff policy for the next delay, calls `delay()`, and tries again: [source,cpp] ---- @@ -140,7 +141,6 @@ connect_with_backoff( int max_attempts) { corosio::tcp_socket sock(ioc); - corosio::timer delay(ioc); int attempt = 0; for (;;) @@ -153,34 +153,44 @@ connect_with_backoff( std::cout << "Connected on attempt " << attempt << std::endl; co_await do_session(sock); - // Peer disconnected — restart the retry sequence + // Peer disconnected, restart the retry sequence sock.close(); backoff.reset(); attempt = 0; + std::cout << "Disconnected, reconnecting..." << std::endl; continue; } sock.close(); + std::cout << "Attempt " << attempt << " failed: " << ec.message() + << std::endl; + if (max_attempts > 0 && attempt >= max_attempts) + { + std::cout << "Giving up after " << attempt << " attempts" + << std::endl; co_return; + } auto wait_for = backoff.next(); - delay.expires_after(wait_for); - auto [timer_ec] = co_await delay.wait(); - if (timer_ec == capy::cond::canceled) - co_return; + std::cout << "Retrying in " << wait_for.count() << "ms" << std::endl; - // delay doubles automatically via backoff.next() + auto [delay_ec] = co_await corosio::delay(wait_for); + if (delay_ec == capy::cond::canceled) + { + std::cout << "Retry cancelled" << std::endl; + co_return; + } } } ---- There are two exit conditions: -1. **Max attempts exhausted** — the coroutine gives up. -2. **Timer cancelled** — someone signaled the stop token, requesting graceful +1. **Max attempts exhausted**: the coroutine gives up. +2. **Delay cancelled**: someone signaled the stop token, requesting graceful shutdown. The coroutine unwinds through normal control flow. After a successful connection and subsequent disconnect, `backoff.reset()` @@ -191,7 +201,7 @@ restarts the delay sequence from the initial value. The key insight of this tutorial: **`io_context::stop()` does not cancel pending operations.** It only stops the event loop. Suspended coroutines are left in place and destroyed during `~io_context` without ever observing an -error. This is by design — `stop()` is a _pause_ that preserves state for +error. This is by design: `stop()` is a _pause_ that preserves state for a potential `restart()`. For graceful shutdown where coroutines unwind through their own control flow, @@ -210,20 +220,20 @@ stop_src.request_stop(); When the stop source is signaled: -1. The loop only inspects the timer's `wait()` for cancellation, so that is - where the stop is observed: `wait()` returns `cond::canceled`. A stop +1. The loop only inspects the `delay()` call for cancellation, so that is + where the stop is observed: `delay()` returns `cond::canceled`. A stop requested mid-`connect()` is not checked there; it simply falls through to - the next timer `wait()`, which then returns `cond::canceled`. Either way + the next `delay()` call, which then returns `cond::canceled`. Either way the coroutine exits cleanly. 2. The coroutine checks the error and executes `co_return`. -3. Local variables (`sock`, `delay`) are destroyed through normal unwinding. +3. Local variables (`sock`) are destroyed through normal unwinding. 4. With no more outstanding work, `run()` returns. -5. `~io_context` finds an empty heap — nothing to clean up. +5. `~io_context` finds an empty heap, nothing to clean up. Contrast with calling `stop()` directly: 1. `run()` exits immediately. -2. The coroutine remains suspended — it never sees an error. +2. The coroutine remains suspended: it never sees an error. 3. `~io_context` calls `h.destroy()` on the coroutine frame, bypassing its error-handling logic. @@ -235,15 +245,15 @@ executes the coroutine's own cleanup code. | Mechanism | Coroutine sees cancellation? | Use case | `stop_token` -| Yes — the awaited operation returns `cond::canceled` (here, the timer `wait()`) +| Yes: the awaited operation returns `cond::canceled` (here, the `delay()` call) | Graceful shutdown | `stop()` + `restart()` -| No — coroutines stay suspended +| No: coroutines stay suspended | Pause and resume the event loop | `~io_context` -| No — frames destroyed via `h.destroy()` +| No: frames destroyed via `h.destroy()` | Final cleanup (after `stop()` or natural exit) |=== @@ -251,7 +261,8 @@ executes the coroutine's own cleanup code. [source,cpp] ---- -int main(int argc, char* argv[]) +int +main(int argc, char* argv[]) { if (argc != 3) { @@ -273,6 +284,16 @@ int main(int argc, char* argv[]) using namespace std::chrono_literals; exponential_backoff backoff(500ms, 30s); + // The stop_source lets us cancel the coroutine gracefully from any + // thread. When signaled, pending delay and connect operations return + // cond::canceled, the coroutine's own loop breaks, and it unwinds + // through normal control flow. + // + // Contrast with io_context::stop(), which yanks the event loop out + // from under suspended coroutines without giving them a chance to + // observe cancellation. stop() is safe (pending operations are + // cleaned up during destruction), but coroutine-internal cleanup + // logic is bypassed. std::stop_source stop_src; capy::run_async(ioc.get_executor(), stop_src.get_token())( @@ -280,18 +301,21 @@ int main(int argc, char* argv[]) // Run the event loop on a background thread so main // can signal cancellation after a timeout. - auto worker = std::jthread([&ioc] { ioc.run(); }); + std::thread worker([&ioc] { ioc.run(); }); std::this_thread::sleep_for(5s); stop_src.request_stop(); + worker.join(); + + return EXIT_SUCCESS; } ---- -The event loop runs on a background thread. After five seconds the main thread -signals cancellation. The coroutine observes `cond::canceled` on its timer -`wait()`, unwinds, the work count reaches zero, and `run()` returns. The -`jthread` destructor joins automatically. The shipped example uses a plain -`std::thread` with an explicit `join()`; the two are equivalent here. +The event loop runs on a background thread. After five seconds the main +thread signals cancellation. The coroutine observes `cond::canceled` on its +next `delay()` call, unwinds, the work count reaches zero, and `run()` +returns. `worker.join()` then waits for the background thread before `main` +returns. == Testing @@ -323,7 +347,7 @@ Attempt 3 failed: Connection refused Retrying in 2000ms ---- -Restart the server — the client reconnects on the next attempt. +Restart the server, and the client reconnects on the next attempt. To test the no-server case, point the client at a port with nothing listening: @@ -342,7 +366,11 @@ After five seconds the stop token fires and the client exits cleanly. == Next Steps -* xref:../4.guide/4h.timers.adoc[Timers Guide] — Timer operations in detail -* xref:../4.guide/4d.sockets.adoc[Sockets Guide] — Socket operations and error handling -* xref:../4.guide/4c.io-context.adoc[I/O Context Guide] — Event loop mechanics, `stop()`, and `restart()` -* xref:../4.guide/4m.error-handling.adoc[Error Handling] — Portable error conditions and `cond` +* xref:../4.guide/4h.timers.adoc[Delays and Timeouts Guide]: delay and timeout + operations in detail +* xref:../4.guide/4d.sockets.adoc[Sockets Guide]: socket operations and error + handling +* xref:../4.guide/4c.io-context.adoc[I/O Context Guide]: event loop mechanics, + `stop()`, and `restart()` +* xref:../4.guide/4m.error-handling.adoc[Error Handling]: portable error + conditions and `cond` diff --git a/doc/modules/ROOT/pages/4.guide/4.intro.adoc b/doc/modules/ROOT/pages/4.guide/4.intro.adoc index 16022191f..96aa33a30 100644 --- a/doc/modules/ROOT/pages/4.guide/4.intro.adoc +++ b/doc/modules/ROOT/pages/4.guide/4.intro.adoc @@ -29,8 +29,8 @@ underlying mechanics that inform correct usage. More advanced topics build on these foundations. You will encounter patterns for building scalable servers, managing connection lifecycles, composing operations for reliable data transfer, and securing connections with encryption. The guide -also covers practical concerns like error handling strategies, timer management, -signal handling, and name resolution. +also covers practical concerns like error handling strategies, delays and +timeouts, signal handling, and name resolution. Each topic builds naturally on previous concepts, but the sections are designed to be read independently. If you already understand a particular area, you can diff --git a/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc index 77a5362ae..cfb213504 100644 --- a/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc +++ b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc @@ -566,12 +566,10 @@ capy::task bad() std::this_thread::sleep_for(1s); // Don't do this! } -// RIGHT: use async timer -capy::task good(corosio::io_context& ioc) +// RIGHT: suspend with an async delay +capy::task good() { - corosio::timer t(ioc); - t.expires_after(1s); - co_await t.wait(); + co_await corosio::delay(1s); } ---- @@ -606,10 +604,10 @@ Don't access an object from a coroutine with different executor affinity: [source,cpp] ---- -// Dangerous: timer created on ctx1, used from ex2 -corosio::timer timer(ctx1); -capy::run_async(ex2)([&timer]() -> capy::task { - co_await timer.wait(); // Wrong executor! +// Dangerous: socket created on ctx1, used from ex2 +corosio::tcp_socket sock(ctx1); +capy::run_async(ex2)([&sock, ep]() -> capy::task { + co_await sock.connect(ep); // Wrong executor! }); ---- diff --git a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc index 3657d09d9..8e9490613 100644 --- a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc @@ -233,10 +233,9 @@ int main() // Create I/O objects corosio::tcp_socket sock(ioc); - corosio::timer timer(ioc); // Launch initial coroutine - capy::run_async(ioc.get_executor())(main_coroutine(sock, timer)); + capy::run_async(ioc.get_executor())(main_coroutine(sock)); // Run until all work completes ioc.run(); @@ -349,4 +348,4 @@ On macOS and FreeBSD, the `io_context` uses kqueue: * xref:4.guide/4d.sockets.adoc[Sockets] — I/O with TCP sockets * xref:4.guide/4e.tcp-acceptor.adoc[Acceptors] — Accept incoming connections -* xref:4.guide/4h.timers.adoc[Timers] — Async delays and timeouts +* xref:4.guide/4h.timers.adoc[Delays and Timeouts] — Async delays and timeouts diff --git a/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc b/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc index d45620a82..0820da43d 100644 --- a/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc @@ -168,5 +168,6 @@ Violating them is undefined behavior. * **POSIX file I/O** (`stream_file`, `random_access_file`) returns `operation_not_supported` on `open()`. * **Signal sets** should not be shared across contexts. -* **Timer cancellation via `stop_token`** from another thread - remains safe (the timer service retains its own mutex). +* **Delay and timeout cancellation via `stop_token`** from another + thread remains safe (the internal timer service retains its own + mutex). diff --git a/doc/modules/ROOT/pages/4.guide/4h.timers.adoc b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc index 049666fd0..36e7a3087 100644 --- a/doc/modules/ROOT/pages/4.guide/4h.timers.adoc +++ b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc @@ -8,317 +8,186 @@ // Official repository: https://github.com/cppalliance/corosio // -= Timers += Delays and Timeouts -The `timer` class provides asynchronous delays and timeouts. It integrates -with the I/O context to schedule operations at specific times or after -durations. +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. [NOTE] ==== Code snippets assume: [source,cpp] ---- -#include +#include +#include +#include + namespace corosio = boost::corosio; +namespace capy = boost::capy; using namespace std::chrono_literals; ---- ==== -== Overview - -Timers let you pause execution for a duration: - -[source,cpp] ----- -corosio::timer t(ioc); -t.expires_after(5s); -co_await t.wait(); // Suspends for 5 seconds ----- - -== Construction - -[source,cpp] ----- -corosio::io_context ioc; -corosio::timer t(ioc); // From execution context ----- - -== Setting Expiry Time +== Delaying for a Duration -=== Relative Time (Duration) +`delay(duration)` suspends the calling coroutine until the given +duration elapses: [source,cpp] ---- -t.expires_after(100ms); // 100 milliseconds from now -t.expires_after(5s); // 5 seconds from now -t.expires_after(2min); // 2 minutes from now ----- - -Any `std::chrono::duration` type works. - -=== Absolute Time (Time Point) - -[source,cpp] ----- -auto deadline = std::chrono::steady_clock::now() + 10s; -t.expires_at(deadline); ----- - -=== Querying Expiry - -[source,cpp] ----- -corosio::timer::time_point when = t.expiry(); ----- - -== Waiting - -The `wait()` operation suspends until the timer expires: - -[source,cpp] ----- -t.expires_after(1s); -auto [ec] = co_await t.wait(); - +auto [ec] = co_await corosio::delay(500ms); if (!ec) - std::cout << "Timer expired normally\n"; ----- - -=== Cancellation - -`cancel()` cancels all pending waits. `cancel_one()` cancels only the -oldest pending wait ( FIFO order ). Both return the number of operations -cancelled: - -[source,cpp] ----- -std::size_t n = t.cancel(); // Cancel all pending waits -std::size_t m = t.cancel_one(); // Cancel oldest pending wait (0 or 1) ----- - -The cancelled wait completes with an error: - -[source,cpp] ----- -auto [ec] = co_await t.wait(); -if (ec == capy::cond::canceled) - std::cout << "Timer was cancelled\n"; ----- - -== Type Aliases - -[source,cpp] ----- -using clock_type = std::chrono::steady_clock; -using time_point = clock_type::time_point; -using duration = clock_type::duration; + std::cout << "500ms elapsed\n"; ---- -The timer uses `steady_clock` for monotonic timing unaffected by system -clock adjustments. +A zero or negative duration completes synchronously without +suspending. -== Resetting Timers +== Delaying Until a Time Point -Setting a new expiry cancels any pending waits and returns the number -cancelled: +`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": [source,cpp] ---- -t.expires_after(10s); -// Later, before 10s elapses: -std::size_t n = t.expires_after(5s); // Resets to 5s, cancels previous waits ----- - -== Multiple Waiters - -Multiple coroutines can wait on the same timer concurrently. When the -timer expires, all waiters complete with success. When cancelled, all -waiters complete with `capy::cond::canceled`: - -[source,cpp] ----- -capy::task waiter(corosio::timer& t, int id) +auto next = std::chrono::steady_clock::now(); +for (int i = 0; i < 10; ++i) { - auto [ec] = co_await t.wait(); - if (!ec) - std::cout << "Waiter " << id << " expired\n"; -} - -capy::task multi_wait(corosio::io_context& ioc) -{ - corosio::timer t(ioc); - t.expires_after(1s); - - // All three coroutines wait on the same timer - co_await capy::when_all( - waiter(t, 1), - waiter(t, 2), - waiter(t, 3)); + next += 100ms; + auto [ec] = co_await corosio::delay(next); + if (ec) + break; + std::cout << "Tick " << i << "\n"; } ---- -Each waiter has independent stop token cancellation. Cancelling one -waiter's stop token does not affect the others. `cancel_one()` cancels -the oldest waiter only. +A time point already in the past also completes synchronously. -== Use Cases +== Cancellation -=== Simple Delay +`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`: [source,cpp] ---- -capy::task delayed_action(corosio::io_context& ioc) -{ - corosio::timer t(ioc); - t.expires_after(2s); - co_await t.wait(); - - std::cout << "2 seconds have passed\n"; -} +auto [ec] = co_await corosio::delay(10s); +if (ec == capy::cond::canceled) + std::cout << "Delay was cancelled\n"; ---- -=== Periodic Timer +There is no `cancel()` to call directly; cancellation flows entirely +through the coroutine's stop token, the same mechanism every other +Corosio operation uses. -[source,cpp] ----- -capy::task periodic_task(corosio::io_context& ioc) -{ - corosio::timer t(ioc); +== Racing an Operation Against a Deadline - for (int i = 0; i < 10; ++i) - { - t.expires_after(1s); - co_await t.wait(); - std::cout << "Tick " << i << "\n"; - } -} ----- - -=== Operation Timeout +`timeout(op, duration)` starts an awaitable and a deadline together. +Whichever finishes first decides the outcome: [source,cpp] ---- -capy::task with_timeout( - corosio::io_context& ioc, - corosio::tcp_socket& sock) -{ - corosio::timer timeout(ioc); - timeout.expires_after(30s); - - // Start both operations - auto read_task = sock.read_some(buffer); - auto timeout_task = timeout.wait(); +auto [ec, n] = co_await corosio::timeout( + sock.read_some(buffer), 200ms); - // In practice, use parallel composition utilities - // This is simplified for illustration -} +if (ec == capy::cond::timeout) + std::cout << "No data within 200ms\n"; +else if (!ec) + std::cout << "Read " << n << " bytes\n"; ---- -NOTE: For proper timeout handling, use Capy's parallel composition utilities -like `when_any` or cancellation tokens. +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. -=== Rate Limiting +`timeout()` also accepts an absolute deadline: [source,cpp] ---- -capy::task rate_limited_work(corosio::io_context& ioc) -{ - corosio::timer t(ioc); - auto next_time = std::chrono::steady_clock::now(); - - for (int i = 0; i < 100; ++i) - { - // Do work - process_item(i); - - // Wait until next interval - next_time += 100ms; - t.expires_at(next_time); - auto [ec] = co_await t.wait(); - if (ec) - break; - } -} +auto deadline = std::chrono::steady_clock::now() + 5s; +auto [ec] = co_await corosio::timeout(sock.connect(ep), deadline); ---- -Using absolute time points prevents drift in periodic operations. +=== Distinguishing Timeout from Cancellation -== Stop Token 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: -Timer waits support stop token cancellation through the affine protocol: - -[source,cpp] ----- -// Inside a cancellable task: -auto [ec] = co_await t.wait(); -// Completes with capy::cond::canceled if stop requested ----- +[cols="1,2"] +|=== +| Result | Meaning -== Move Semantics +| `capy::cond::timeout` +| The deadline elapsed before the operation completed. -Timers are move-only: +| `capy::cond::canceled` +| The coroutine's own stop token fired, independent of the deadline. +|=== [source,cpp] ---- -corosio::timer t1(ioc); -corosio::timer t2 = std::move(t1); // OK - -corosio::timer t3 = t2; // Error: deleted copy constructor +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"; ---- -Move assignment cancels any pending wait on the destination timer. - -NOTE: After a move, the destination uses the source's execution context. +== Requires an io_context -== Thread Safety +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. -[cols="1,2"] -|=== -| Operation | Thread Safety +== Composing with Other Operations -| Distinct timers -| Safe from different threads - -| Same timer -| NOT safe for concurrent operations -|=== - -Don't call `wait()`, `expires_after()`, or `cancel()` concurrently on the -same timer. - -== Example: Heartbeat +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: [source,cpp] ---- -capy::task heartbeat( - corosio::io_context& ioc, +capy::task<> +connect_with_deadline( corosio::tcp_socket& sock, - std::atomic& running) + corosio::endpoint ep, + int max_attempts) { - corosio::timer t(ioc); - - while (running) + for (int attempt = 0; attempt < max_attempts; ++attempt) { - t.expires_after(30s); - auto [ec] = co_await t.wait(); - - if (ec) - break; + auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); + if (!ec) + co_return; - // Send heartbeat - std::string ping = "PING\r\n"; - auto [wec, n] = co_await capy::write( - sock, capy::const_buffer(ping.data(), ping.size())); + if (ec == capy::cond::canceled) + co_return; - if (wec) - break; + sock.close(); + co_await corosio::delay(500ms); } } ---- == Next Steps -* xref:4.guide/4i.signals.adoc[Signal Handling] — Respond to OS signals -* xref:4.guide/4c.io-context.adoc[I/O Context] — The event loop -* xref:4.guide/4m.error-handling.adoc[Error Handling] — Cancellation patterns +* xref:4.guide/4i.signals.adoc[Signal Handling]: respond to OS signals +* xref:4.guide/4c.io-context.adoc[I/O Context]: the event loop +* xref:4.guide/4m.error-handling.adoc[Error Handling]: cancellation patterns diff --git a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc index 20c069ecc..224dc0e59 100644 --- a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc +++ b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc @@ -431,6 +431,6 @@ thread is modifying a `signal_set`. == Next Steps -* xref:4.guide/4h.timers.adoc[Timers] — Timed operations +* xref:4.guide/4h.timers.adoc[Delays and Timeouts] — Timed operations * xref:4.guide/4c.io-context.adoc[I/O Context] — The event loop * xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Server example diff --git a/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc index 49fa63b46..10f27e6b3 100644 --- a/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc +++ b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc @@ -23,6 +23,7 @@ Code snippets assume: namespace corosio = boost::corosio; namespace capy = boost::capy; +using namespace std::chrono_literals; ---- ==== @@ -160,8 +161,8 @@ auto [ec, n] = throw_on_error(co_await capy::read(sock, buffer)); | `broken_pipe` | Write to closed connection -| `timed_out` -| Operation timed out +| `capy::cond::timeout` +| Operation exceeded its deadline | `network_unreachable` | No route to host @@ -188,6 +189,27 @@ if (ec == capy::cond::canceled) std::cout << "Operation was cancelled\n"; ---- +=== Timeout vs. Cancellation + +`corosio::timeout()` races an operation against a deadline and keeps +these two outcomes distinct. A deadline that elapses first produces +`capy::cond::timeout`; a stop token that fires first (the coroutine's +own cancellation, independent of the deadline) produces +`capy::cond::canceled`, exactly as an unguarded operation would: + +[source,cpp] +---- +auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); +if (ec == capy::cond::timeout) + std::cout << "Deadline elapsed before connecting\n"; +else if (ec == capy::cond::canceled) + std::cout << "Cancelled before the deadline\n"; +---- + +Never infer a timeout from `capy::cond::canceled`, and never infer a +cancellation from `capy::cond::timeout`; the two conditions never +overlap for a single result. + == EOF Handling End-of-stream is signaled by the `capy::cond::eof` condition: @@ -303,7 +325,6 @@ capy::task connect_with_retry( int max_retries) { corosio::tcp_socket sock(ioc); - corosio::timer delay(ioc); for (int attempt = 0; attempt < max_retries; ++attempt) { @@ -319,8 +340,7 @@ capy::task connect_with_retry( sock.close(); // Wait before retry (exponential backoff) - delay.expires_after(std::chrono::seconds(1 << attempt)); - co_await delay.wait(); + co_await corosio::delay(std::chrono::seconds(1 << attempt)); } throw std::runtime_error("Failed to connect after retries"); diff --git a/doc/modules/ROOT/pages/4.guide/4r.wait.adoc b/doc/modules/ROOT/pages/4.guide/4r.wait.adoc index edb12839c..aaf21aaef 100644 --- a/doc/modules/ROOT/pages/4.guide/4r.wait.adoc +++ b/doc/modules/ROOT/pages/4.guide/4r.wait.adoc @@ -21,9 +21,13 @@ Code snippets assume: [source,cpp] ---- #include +#include #include +#include namespace corosio = boost::corosio; +namespace capy = boost::capy; +using namespace std::chrono_literals; ---- ==== @@ -109,8 +113,17 @@ auto waiter = [&]() -> capy::task<> { }; ---- -`cancel_after()` composes with `wait()` the same way it does with the -other socket operations. +xref:4.guide/4h.timers.adoc[`timeout()`] composes with `wait()` the +same way it does with the other socket operations, since `wait()` +yields an `io_result` like any other awaitable: + +[source,cpp] +---- +auto [ec] = co_await corosio::timeout( + sock.wait(corosio::wait_type::read), 200ms); +if (ec == capy::cond::timeout) + std::cout << "No readiness within 200ms\n"; +---- == `wait_type::write` Semantics diff --git a/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc index 926f042cc..bc3996e1a 100644 --- a/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc +++ b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc @@ -206,8 +206,8 @@ example. * `provide` / `expect` are byte sequences only; there is no out-of-band signal for "now produce EOF" or "now fail" beyond the matching semantics described above. -* No simulation of network delay; for that, run code under a stop_token - with a timer. +* No simulation of network delay; for that, wrap the operation under + test in `corosio::timeout()`, or insert a `corosio::delay()` call. * Connection-establishment errors are not simulated — the pair always comes back open. diff --git a/doc/modules/ROOT/pages/benchmark-report.adoc b/doc/modules/ROOT/pages/benchmark-report.adoc index 954189597..6c857f745 100644 --- a/doc/modules/ROOT/pages/benchmark-report.adoc +++ b/doc/modules/ROOT/pages/benchmark-report.adoc @@ -671,6 +671,12 @@ Multiple socket pairs operating concurrently (64-byte messages). == Timer Benchmarks +NOTE: The timer category was removed from the benchmark suite. Corosio's +stateless delay() and timeout() acquire their timer per wait while Asio +reuses a caller-owned timer object, so the two sides no longer measure +comparable work. The figures below are a historical snapshot taken while +both libraries exposed an equivalent timer object API. + === Timer Schedule/Cancel Measures the rate of creating and cancelling timers without firing them. @@ -928,7 +934,7 @@ Asio callbacks achieve the highest raw single-threaded dispatch rate, but this a === Key Takeaway -For coroutine-based async programming on Windows (IOCP), *Corosio provides equivalent or better performance* compared to Asio coroutines in every category except bidirectional socket throughput and timer schedule/cancel. Corosio's superior multi-threaded scaling makes it particularly well-suited for applications that distribute work across threads. Bidirectional throughput and timer operations are identified areas for future optimization. +For coroutine-based async programming on Windows (IOCP), *Corosio provides equivalent or better performance* compared to Asio coroutines in every category except bidirectional socket throughput and timer schedule/cancel (a measurement that predates removal of the standalone timer API). Corosio's superior multi-threaded scaling makes it particularly well-suited for applications that distribute work across threads. Bidirectional throughput and timer operations are identified areas for future optimization. == Appendix: Raw Data diff --git a/doc/modules/ROOT/pages/glossary.adoc b/doc/modules/ROOT/pages/glossary.adoc index 7a257eaa1..9fbdcb7ba 100644 --- a/doc/modules/ROOT/pages/glossary.adoc +++ b/doc/modules/ROOT/pages/glossary.adoc @@ -78,6 +78,12 @@ Coroutine Handle:: A low-level handle to a suspended coroutine, represented by `std::coroutine_handle<>`. +== D + +Delay:: +A stateless suspension for a duration or until a time point. See +`corosio::delay()` and xref:4.guide/4h.timers.adoc[Delays and Timeouts]. + == E Endpoint:: @@ -122,7 +128,7 @@ The main event loop in Corosio. Processes asynchronous operations and dispatches completions. See `corosio::io_context`. I/O Object:: -A class representing an I/O resource (socket, timer, etc.). Base class is +A class representing an I/O resource (socket, resolver, etc.). Base class is `corosio::io_object`. I/O Stream:: @@ -248,8 +254,9 @@ Thread Safety:: The ability to use an object safely from multiple threads. Individual I/O objects are generally not thread-safe. -Timer:: -An I/O object for scheduling delays. See `corosio::timer`. +Timeout:: +Racing an awaitable against a deadline. See `corosio::timeout()` and +xref:4.guide/4h.timers.adoc[Delays and Timeouts]. TLS (Transport Layer Security):: Cryptographic protocol for secure communication. See `corosio::wolfssl_stream`. @@ -269,8 +276,8 @@ See xref:4.guide/4p.unix-sockets.adoc[Unix Domain Sockets]. == W Wait:: -An operation that suspends until a condition is met (timer expires, signal -received, etc.). +An operation that suspends until a condition is met (a delay elapses, a +socket becomes ready, a signal is received, etc.). WolfSSL:: A compact TLS library used by Corosio for secure streams. diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 29ce277a3..7d88e27fb 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -28,7 +28,7 @@ executor without manual dispatch. * **local_stream_acceptor** — Unix domain stream listener * **local_datagram_socket** — Unix domain datagram socket for local IPC * **resolver** — Asynchronous DNS resolution -* **timer** — Asynchronous timer for delays and timeouts +* **delay()** / **timeout()**: stateless delays and deadline racing for coroutines * **signal_set** — Asynchronous signal handling * **stream_file** / **random_access_file** — Asynchronous file I/O * **openssl_stream** / **wolfssl_stream** — TLS encryption (OpenSSL or WolfSSL) diff --git a/example/reconnect/reconnect.cpp b/example/reconnect/reconnect.cpp index 6f830c7b5..24bb26f2a 100644 --- a/example/reconnect/reconnect.cpp +++ b/example/reconnect/reconnect.cpp @@ -7,10 +7,10 @@ // Official repository: https://github.com/cppalliance/corosio // +#include #include #include #include -#include #include #include #include @@ -35,11 +35,11 @@ namespace capy = boost::capy; @par Example @code exponential_backoff backoff(500ms, 30s); - timer.expires_after(backoff.next()); // 500ms - timer.expires_after(backoff.next()); // 1000ms - timer.expires_after(backoff.next()); // 2000ms + co_await delay(backoff.next()); // 500ms + co_await delay(backoff.next()); // 1000ms + co_await delay(backoff.next()); // 2000ms backoff.reset(); - timer.expires_after(backoff.next()); // 500ms + co_await delay(backoff.next()); // 500ms @endcode */ struct exponential_backoff @@ -100,11 +100,11 @@ do_session(corosio::tcp_socket& sock) disconnects, then reconnection resumes from the initial delay. - The backoff timer is sensitive to the coroutine's stop + The backoff delay is sensitive to the coroutine's stop token. Cancelling the token (or calling `io_context::stop()`) will end the retry loop cleanly. - @param ioc The I/O context to use for socket and timer + @param ioc The I/O context to use for socket and delay operations. @param ep The endpoint to connect to. @param backoff The backoff policy to use between retries. @@ -119,7 +119,6 @@ connect_with_backoff( int max_attempts) { corosio::tcp_socket sock(ioc); - corosio::timer delay(ioc); int attempt = 0; for (;;) @@ -132,7 +131,7 @@ connect_with_backoff( std::cout << "Connected on attempt " << attempt << std::endl; co_await do_session(sock); - // Peer disconnected — restart the retry sequence + // Peer disconnected, restart the retry sequence sock.close(); backoff.reset(); attempt = 0; @@ -156,9 +155,8 @@ connect_with_backoff( std::cout << "Retrying in " << wait_for.count() << "ms" << std::endl; - delay.expires_after(wait_for); - auto [timer_ec] = co_await delay.wait(); - if (timer_ec == capy::cond::canceled) + auto [delay_ec] = co_await corosio::delay(wait_for); + if (delay_ec == capy::cond::canceled) { std::cout << "Retry cancelled" << std::endl; co_return; @@ -201,7 +199,7 @@ main(int argc, char* argv[]) exponential_backoff backoff(500ms, 30s); // The stop_source lets us cancel the coroutine gracefully - // from any thread. When signaled, pending timer and connect + // from any thread. When signaled, pending delay and connect // operations return cond::canceled, the coroutine's own loop // breaks, and it unwinds through normal control flow. // diff --git a/include/boost/corosio.hpp b/include/boost/corosio.hpp index a61b86fb0..e60c1b970 100644 --- a/include/boost/corosio.hpp +++ b/include/boost/corosio.hpp @@ -11,8 +11,8 @@ #define BOOST_COROSIO_HPP #include -#include #include +#include #include #include #include @@ -28,7 +28,7 @@ #include #include #include -#include +#include #include #include diff --git a/include/boost/corosio/cancel.hpp b/include/boost/corosio/cancel.hpp deleted file mode 100644 index 77a939377..000000000 --- a/include/boost/corosio/cancel.hpp +++ /dev/null @@ -1,214 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_CANCEL_HPP -#define BOOST_COROSIO_CANCEL_HPP - -#include -#include -#include - -#include -#include - -namespace boost::corosio { - -/** Cancel an operation if it does not complete by a deadline. - - Races @p op against the given timer. If the deadline is reached - first, the inner operation is cancelled via its stop token and - completes with an error comparing equal to `capy::cond::canceled`. - If the inner operation completes first, the timer is cancelled. - - Parent cancellation (from the caller's stop token) is forwarded - to both the inner operation and the timeout timer. - - The timer's expiry is overwritten by this call. The timer must - outlive the returned awaitable. Do not issue overlapping waits - on the same timer. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The deadline expires and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @par Example - @code - timer timeout_timer( ioc ); - auto [ec, n] = co_await cancel_at( - sock.read_some( buf ), timeout_timer, - clock::now() + 5s ); - if (ec == capy::cond::canceled) - // timed out or parent cancelled - @endcode - - @param op The inner I/O awaitable to wrap. - @param t The timer to use for the deadline. Must outlive - the returned awaitable. - @param deadline The absolute time point at which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_after -*/ -auto -cancel_at(capy::IoAwaitable auto&& op, timer& t, timer::time_point deadline) -{ - return detail::cancel_at_awaitable, timer>( - std::forward(op), t, deadline); -} - -/** Cancel an operation if it does not complete within a duration. - - Equivalent to `cancel_at( op, t, clock::now() + timeout )`. - - The timer's expiry is overwritten by this call. The timer must - outlive the returned awaitable. Do not issue overlapping waits - on the same timer. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The timeout elapses and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @par Example - @code - timer timeout_timer( ioc ); - auto [ec, n] = co_await cancel_after( - sock.read_some( buf ), timeout_timer, 5s ); - if (ec == capy::cond::canceled) - // timed out - @endcode - - @param op The inner I/O awaitable to wrap. - @param t The timer to use for the timeout. Must outlive - the returned awaitable. - @param timeout The relative duration after which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_at -*/ -auto -cancel_after(capy::IoAwaitable auto&& op, timer& t, timer::duration timeout) -{ - return cancel_at( - std::forward(op), t, timer::clock_type::now() + timeout); -} - -/** Cancel an operation if it does not complete by a deadline. - - Convenience overload that creates a @ref timer internally. - Otherwise identical to the explicit-timer overload. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The deadline expires and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @note Creates a timer per call. Use the explicit-timer overload - to amortize allocation across multiple timeouts. - - @note The awaiting coroutine's executor must be backed by an - io_context (the deadline timer is built from it). Awaiting this - on a non-io_context executor is a precondition violation and - aborts; use the explicit-timer overload to construct the timer - yourself if you need a catchable error. - - @par Example - @code - auto [ec, n] = co_await cancel_at( - sock.read_some( buf ), - clock::now() + 5s ); - if (ec == capy::cond::canceled) - // timed out or parent cancelled - @endcode - - @param op The inner I/O awaitable to wrap. - @param deadline The absolute time point at which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_after -*/ -auto -cancel_at(capy::IoAwaitable auto&& op, timer::time_point deadline) -{ - return detail::cancel_at_awaitable, timer, true>( - std::forward(op), deadline); -} - -/** Cancel an operation if it does not complete within a duration. - - Convenience overload that creates a @ref timer internally. - Equivalent to `cancel_at( op, clock::now() + timeout )`. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The timeout elapses and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @note Creates a timer per call. Use the explicit-timer overload - to amortize allocation across multiple timeouts. - - @note The awaiting coroutine's executor must be backed by an - io_context (the deadline timer is built from it). Awaiting this - on a non-io_context executor is a precondition violation and - aborts; use the explicit-timer overload to construct the timer - yourself if you need a catchable error. - - @par Example - @code - auto [ec, n] = co_await cancel_after( - sock.read_some( buf ), 5s ); - if (ec == capy::cond::canceled) - // timed out - @endcode - - @param op The inner I/O awaitable to wrap. - @param timeout The relative duration after which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_at -*/ -auto -cancel_after(capy::IoAwaitable auto&& op, timer::duration timeout) -{ - return cancel_at( - std::forward(op), timer::clock_type::now() + timeout); -} - -} // namespace boost::corosio - -#endif diff --git a/include/boost/corosio/delay.hpp b/include/boost/corosio/delay.hpp new file mode 100644 index 000000000..a3e6e4fcf --- /dev/null +++ b/include/boost/corosio/delay.hpp @@ -0,0 +1,200 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DELAY_HPP +#define BOOST_COROSIO_DELAY_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace boost::corosio { + +/** IoAwaitable returned by @ref delay. + + Suspends the calling coroutine until the deadline elapses or + the environment's stop token is activated, whichever comes + first. A deadline already elapsed at suspension, or a stop + token already active, resumes the coroutine inline, without + starting a timer (see Cancellation below). Otherwise the + coroutine resumes through the executor once the timer fires + or a mid-wait cancellation arrives. + + Not intended to be named directly; use the @ref delay factory + overloads instead. + + @par Preconditions + The awaiting coroutine's executor must belong to an + `io_context`. Any other execution context terminates with a + diagnostic, because silently running without a timer would + drop the requested delay. + + @par Cancellation + If stop is already requested before suspension, the coroutine + resumes immediately with `error::canceled`. If stop is + requested while suspended, the pending wait is cancelled and + the coroutine resumes with `error::canceled`. Requesting stop + from another thread while the io_context runs in + single_threaded mode (auto-enabled at concurrency_hint == 1) + is not permitted by io_context's threading rules; + cross-thread cancellation requires a multi-threaded-capable + context. + + @see delay +*/ +class delay_awaitable +{ + // wait() names timer's private awaitable type; decltype is + // the only way to store it here. + using wait_type = decltype(std::declval().wait()); + + std::chrono::steady_clock::time_point deadline_{}; + std::chrono::nanoseconds dur_{}; + bool has_deadline_ = false; + bool canceled_ = false; + std::optional timer_; + std::optional wait_; + +public: + /// Construct an awaitable that waits for `dur` nanoseconds. + explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept + : dur_(dur) + { + } + + /// Construct an awaitable that waits until `tp`. + explicit delay_awaitable( + std::chrono::steady_clock::time_point tp) noexcept + : deadline_(tp) + , has_deadline_(true) + { + } + + /// Construct by transferring state from `other`. + // Only moved before await_suspend; wait_ is engaged after. + delay_awaitable(delay_awaitable&&) = default; + + delay_awaitable(delay_awaitable const&) = delete; + delay_awaitable& operator=(delay_awaitable const&) = delete; + delay_awaitable& operator=(delay_awaitable&&) = delete; + + /// Return false unconditionally; see await_suspend. + // The elapsed-deadline fast path must run after the stop-token + // check, and only await_suspend receives the env carrying it. + bool await_ready() const noexcept + { + return false; + } + + /// Resume inline if stopped or elapsed; else wait on a timer. + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + if(env->stop_token.stop_requested()) + { + canceled_ = true; + return h; + } + + // Elapsed deadlines complete synchronously, but only once a + // pending stop request has already been ruled out above. + if(has_deadline_ ? + deadline_ <= std::chrono::steady_clock::now() : + dur_.count() <= 0) + return h; + + // A non-io_context executor cannot supply a timer service, + // and await_suspend is driven through a noexcept wrapper, so + // translate the service-lookup failure into a clear terminate. + try + { + timer_.emplace(env->executor.context()); + } + catch(std::logic_error const&) + { + detail::throw_logic_error( + "delay requires an io_context-backed executor"); + } + + if(has_deadline_) + timer_->expires_at(deadline_); + else + timer_->expires_after(dur_); + + wait_.emplace(timer_->wait()); + return wait_->await_suspend(h, env); + } + + /// Return empty on expiry, `error::canceled` if stop won. + capy::io_result<> await_resume() noexcept + { + if(canceled_) + return {capy::error::canceled}; + if(wait_) + return wait_->await_resume(); + return {}; + } +}; + +/** Suspend the current coroutine for a duration. + + Returns an IoAwaitable that completes at or after the + specified duration, or earlier if the environment's stop + token is activated. Zero or negative durations complete + synchronously. + + @par Example + @code + auto [ec] = co_await delay(std::chrono::milliseconds(100)); + @endcode + + @param dur The duration to wait. + + @return A @ref delay_awaitable yielding `io_result<>`. +*/ +template +[[nodiscard]] delay_awaitable +delay(std::chrono::duration dur) noexcept +{ + using namespace std::chrono; + // Clamp against overflow: a huge source duration would wrap when + // cast to nanoseconds. + auto ns = dur > duration_cast>(nanoseconds::max()) + ? nanoseconds::max() + : duration_cast(dur); + return delay_awaitable(ns); +} + +/** Suspend the current coroutine until a time point. + + Returns an IoAwaitable that completes at or after `tp`, or + earlier if the environment's stop token is activated. Time + points already reached complete synchronously. + + @param tp The steady-clock time point to wait until. + + @return A @ref delay_awaitable yielding `io_result<>`. +*/ +[[nodiscard]] inline delay_awaitable +delay(std::chrono::steady_clock::time_point tp) noexcept +{ + return delay_awaitable(tp); +} + +} // namespace boost::corosio + +#endif diff --git a/include/boost/corosio/detail/cancel_at_awaitable.hpp b/include/boost/corosio/detail/cancel_at_awaitable.hpp deleted file mode 100644 index a757a077a..000000000 --- a/include/boost/corosio/detail/cancel_at_awaitable.hpp +++ /dev/null @@ -1,196 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_DETAIL_CANCEL_AT_AWAITABLE_HPP -#define BOOST_COROSIO_DETAIL_CANCEL_AT_AWAITABLE_HPP - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -/* Races an inner IoAwaitable against a timer via a shared - stop_source. await_suspend arms the timer by launching a - fire-and-forget timeout_coro, then starts the inner op with - an interposed stop_token. Whichever completes first signals - the stop_source, cancelling the other. - - Parent cancellation is forwarded through a stop_callback - stored in a placement-new buffer (stop_callback is not - movable, but the awaitable must be movable for - transform_awaiter). The buffer is inert during moves - (before await_suspend) and constructed in-place once the - awaitable is pinned on the coroutine frame. - - The timeout_coro can outlive this awaitable — it owns its - env and self-destroys via suspend_never. When Owning is - false the caller-supplied timer must outlive both; when - Owning is true the timer lives in std::optional and is - constructed lazily in await_suspend. */ - -namespace boost::corosio::detail { - -/** Awaitable adapter that cancels an inner operation after a deadline. - - Races the inner awaitable against a timer. A shared stop_source - ties them together: whichever completes first cancels the other. - Parent cancellation is forwarded via stop_callback. - - When @p Owning is `false` (default), the caller supplies a timer - reference that must outlive the awaitable. When @p Owning is - `true`, the timer is constructed internally in `await_suspend` - from the execution context in `io_env`. - - @tparam A The inner IoAwaitable type (decayed). - @tparam Timer The timer type (`timer` or `native_timer`). - @tparam Owning When `true`, the awaitable owns its timer. -*/ -template -struct cancel_at_awaitable -{ - struct stop_forwarder - { - std::stop_source* src_; - void operator()() const noexcept - { - src_->request_stop(); - } - }; - - using time_point = std::chrono::steady_clock::time_point; - using stop_cb_type = std::stop_callback; - using timer_storage = - std::conditional_t, Timer*>; - - A inner_; - timer_storage timer_; - time_point deadline_; - std::stop_source stop_src_; - capy::io_env inner_env_; - alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)]; - bool cb_active_ = false; - - /// Construct with a caller-supplied timer reference. - cancel_at_awaitable(A&& inner, Timer& timer, time_point deadline) - requires(!Owning) - : inner_(std::move(inner)) - , timer_(&timer) - , deadline_(deadline) - { - } - - /// Construct without a timer (created in `await_suspend`). - cancel_at_awaitable(A&& inner, time_point deadline) - requires Owning - : inner_(std::move(inner)) - , deadline_(deadline) - { - } - - ~cancel_at_awaitable() - { - destroy_parent_cb(); - } - - // Only moved before await_suspend, when cb_active_ is false - cancel_at_awaitable(cancel_at_awaitable&& o) noexcept( - std::is_nothrow_move_constructible_v) - : inner_(std::move(o.inner_)) - , timer_(std::move(o.timer_)) - , deadline_(o.deadline_) - , stop_src_(std::move(o.stop_src_)) - { - } - - cancel_at_awaitable(cancel_at_awaitable const&) = delete; - cancel_at_awaitable& operator=(cancel_at_awaitable const&) = delete; - cancel_at_awaitable& operator=(cancel_at_awaitable&&) = delete; - - bool await_ready() const noexcept - { - return false; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - { - if constexpr (Owning) - { - // The deadline timer is built here from the awaiting - // coroutine's executor context, the first point at which it - // is known. await_suspend is driven through a noexcept - // wrapper, so a failure cannot be surfaced as a catchable - // exception. An executor whose context is not an io_context - // cannot supply a timer service; silently running the - // operation with no deadline would be a worse failure than - // aborting, so translate the service-lookup error into a - // clear precondition diagnostic. This terminates by design - // (a usage error) rather than dropping the requested timeout. - try - { - timer_.emplace(env->executor.context()); - } - catch (std::logic_error const&) - { - throw_logic_error( - "cancel_after/cancel_at requires an " - "io_context-backed executor"); - } - } - - timer_->expires_at(deadline_); - - // Launch fire-and-forget timeout (starts suspended) - auto timeout = make_timeout(*timer_, stop_src_); - timeout.h_.promise().set_env_owned( - {env->executor, stop_src_.get_token(), env->frame_allocator}); - // Runs synchronously until timer.wait() suspends - timeout.h_.resume(); - // timeout goes out of scope; destructor is a no-op, - // the coroutine self-destroys via suspend_never - - // Forward parent cancellation - new (cb_buf_) stop_cb_type(env->stop_token, stop_forwarder{&stop_src_}); - cb_active_ = true; - - // Start the inner op with our interposed stop_token - inner_env_ = { - env->executor, stop_src_.get_token(), env->frame_allocator}; - return inner_.await_suspend(h, &inner_env_); - } - - decltype(auto) await_resume() - { - // Cancel whichever is still pending (idempotent) - stop_src_.request_stop(); - destroy_parent_cb(); - return inner_.await_resume(); - } - - void destroy_parent_cb() noexcept - { - if (cb_active_) - { - std::launder(reinterpret_cast(cb_buf_)) - ->~stop_cb_type(); - cb_active_ = false; - } - } -}; - -} // namespace boost::corosio::detail - -#endif diff --git a/include/boost/corosio/detail/timeout_awaitable.hpp b/include/boost/corosio/detail/timeout_awaitable.hpp new file mode 100644 index 000000000..6e2e558cc --- /dev/null +++ b/include/boost/corosio/detail/timeout_awaitable.hpp @@ -0,0 +1,244 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_TIMEOUT_AWAITABLE_HPP +#define BOOST_COROSIO_DETAIL_TIMEOUT_AWAITABLE_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Races an inner IoAwaitable against a timer via a shared + stop_source. await_suspend arms the timer by launching a + fire-and-forget timeout_coro, then starts the inner op with + an interposed stop_token. Whichever completes first signals + the stop_source, cancelling the other. + + Parent cancellation is forwarded through a stop_callback + stored in a placement-new buffer (stop_callback is not + movable, but the awaitable must be movable for + transform_awaiter). The buffer is inert during moves + (before await_suspend) and constructed in-place once the + awaitable is pinned on the coroutine frame. + + The timeout_coro can outlive this awaitable — it owns its + env and self-destroys via suspend_never. The timer lives in + std::optional and is constructed lazily in await_suspend, + once the awaiting coroutine's executor context is known. */ + +namespace boost::corosio::detail { + +// Local stand-in for capy::detail's io_result trait: corosio must not +// reach into capy::detail, but the result-mapping switch in +// await_resume needs to distinguish io_result from other return types. +template +struct is_io_result : std::false_type +{ +}; + +template +struct is_io_result> : std::true_type +{ +}; + +template +inline constexpr bool is_io_result_v = is_io_result::value; + +/** Awaitable adapter that cancels an inner operation after a deadline. + + Races the inner awaitable against a timer. A shared stop_source + ties them together: whichever completes first cancels the other. + Parent cancellation is forwarded via stop_callback. + + The timer is constructed internally in `await_suspend` from the + execution context in `io_env`. + + @tparam A The inner IoAwaitable type (decayed). +*/ +template +struct timeout_awaitable +{ + struct stop_forwarder + { + std::stop_source* src_; + void operator()() const noexcept + { + src_->request_stop(); + } + }; + + using time_point = std::chrono::steady_clock::time_point; + using stop_cb_type = std::stop_callback; + + A inner_; + std::optional timer_; + time_point deadline_; + std::chrono::nanoseconds dur_{}; + bool has_deadline_ = true; + std::stop_source stop_src_; + std::stop_token parent_token_; + capy::io_env inner_env_; + alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)]; + bool cb_active_ = false; + + /// Construct without a timer, deadline given as an absolute time. + timeout_awaitable(A&& inner, time_point deadline) + : inner_(std::move(inner)) + , deadline_(deadline) + { + } + + /// Construct without a timer, deadline measured from suspension. + timeout_awaitable(A&& inner, std::chrono::nanoseconds dur) + : inner_(std::move(inner)) + , dur_(dur) + , has_deadline_(false) + { + } + + ~timeout_awaitable() + { + destroy_parent_cb(); + } + + // Only moved before await_suspend, when cb_active_ is false + timeout_awaitable(timeout_awaitable&& o) noexcept( + std::is_nothrow_move_constructible_v) + : inner_(std::move(o.inner_)) + , timer_(std::move(o.timer_)) + , deadline_(o.deadline_) + , dur_(o.dur_) + , has_deadline_(o.has_deadline_) + , stop_src_(std::move(o.stop_src_)) + { + } + + timeout_awaitable(timeout_awaitable const&) = delete; + timeout_awaitable& operator=(timeout_awaitable const&) = delete; + timeout_awaitable& operator=(timeout_awaitable&&) = delete; + + bool await_ready() const noexcept + { + return false; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + parent_token_ = env->stop_token; + + // The deadline timer is built here from the awaiting + // coroutine's executor context, the first point at which it + // is known. await_suspend is driven through a noexcept + // wrapper, so a failure cannot be surfaced as a catchable + // exception. An executor whose context is not an io_context + // cannot supply a timer service; silently running the + // operation with no deadline would be a worse failure than + // aborting, so translate the service-lookup error into a + // clear precondition diagnostic. This terminates by design + // (a usage error) rather than dropping the requested timeout. + try + { + timer_.emplace(env->executor.context()); + } + catch (std::logic_error const&) + { + throw_logic_error( + "timeout requires an io_context-backed executor"); + } + + if (has_deadline_) + timer_->expires_at(deadline_); + else + timer_->expires_after(dur_); + + // Launch fire-and-forget timeout (starts suspended) + auto timeout = make_timeout(*timer_, stop_src_); + timeout.h_.promise().set_env_owned( + {env->executor, stop_src_.get_token(), env->frame_allocator}); + // Runs synchronously until timer.wait() suspends + timeout.h_.resume(); + // timeout goes out of scope; destructor is a no-op, + // the coroutine self-destroys via suspend_never + + // Forward parent cancellation + new (cb_buf_) stop_cb_type(env->stop_token, stop_forwarder{&stop_src_}); + cb_active_ = true; + + // Start the inner op with our interposed stop_token + inner_env_ = { + env->executor, stop_src_.get_token(), env->frame_allocator}; + return inner_.await_suspend(h, &inner_env_); + } + + decltype(auto) await_resume() + { + // Read before request_stop: afterwards stop_requested() + // can no longer distinguish who fired first. This must also + // happen before inner_.await_resume() rather than after: when + // the inner awaitable is itself a timeout_awaitable (nested + // timeout()), our own request_stop() below is visible through + // its parent_token_ (aliasing our stop_src_), and would + // otherwise make its read of "parent" look like a + // cancellation that never happened. + bool const parent = parent_token_.stop_requested(); + bool const fired = stop_src_.stop_requested(); + + // If inner_.await_resume() throws below, request_stop() is + // skipped; the still-armed timeout coroutine is then drained + // by timer_'s destructor rather than by us. + auto r = inner_.await_resume(); + + // Cancel whichever is still pending (idempotent) + stop_src_.request_stop(); + destroy_parent_cb(); + + // Deadline won: stop_src_ is assumed to be the only + // cancellation source, whose only writers are the timer + // coroutine and the parent forwarder, so fired && !parent + // identifies a timeout. A third-party cancellation of the + // inner op (e.g. a socket cancel issued from elsewhere) + // landing in the same window as the deadline firing is + // reported as a timeout. + if (fired && !parent && + r.ec == capy::cond::canceled) + { + std::remove_cvref_t t{}; + t.ec = make_error_code(capy::error::timeout); + return t; + } + return r; + } + + void destroy_parent_cb() noexcept + { + if (cb_active_) + { + std::launder(reinterpret_cast(cb_buf_)) + ->~stop_cb_type(); + cb_active_ = false; + } + } +}; + +} // namespace boost::corosio::detail + +#endif diff --git a/include/boost/corosio/detail/timeout_coro.hpp b/include/boost/corosio/detail/timeout_coro.hpp index 905af5397..01ea4b355 100644 --- a/include/boost/corosio/detail/timeout_coro.hpp +++ b/include/boost/corosio/detail/timeout_coro.hpp @@ -32,11 +32,16 @@ The promise reuses task<>'s transform_awaiter pattern (including the MSVC symmetric-transfer workaround) to inject io_env into - IoAwaitable co_await expressions. */ + IoAwaitable co_await expressions. + + The detached frame allocates from the awaiting chain's frame + allocator and can outlive that chain, extending the allocator's + required lifetime until the io_context drains. This is safe with + the default recycling resource. */ namespace boost::corosio::detail { -/** Fire-and-forget coroutine for the timeout side of cancel_at. +/** Fire-and-forget coroutine backing `timeout()`. The coroutine awaits a timer and signals a stop_source if the timer fires without being cancelled. It self-destroys at @@ -52,7 +57,7 @@ struct timeout_coro /** Store an owned copy of the environment. - The timeout coroutine can outlive the cancel_at_awaitable + The timeout coroutine can outlive the timeout_awaitable that created it, so it must own its env rather than pointing to external storage. */ @@ -161,7 +166,7 @@ struct timeout_coro Wait on the timer. If it fires without cancellation, signal the stop source to cancel the paired inner operation. - @tparam Timer Timer type (`timer` or `native_timer`). + @tparam Timer The timer type, typically `detail::timer`. @param t The timer to wait on (must have expiry set). @param src Stop source to signal on timeout. diff --git a/include/boost/corosio/timer.hpp b/include/boost/corosio/detail/timer.hpp similarity index 56% rename from include/boost/corosio/timer.hpp rename to include/boost/corosio/detail/timer.hpp index ecd43d1dc..e3ccb5e04 100644 --- a/include/boost/corosio/timer.hpp +++ b/include/boost/corosio/detail/timer.hpp @@ -8,20 +8,38 @@ // Official repository: https://github.com/cppalliance/corosio // -#ifndef BOOST_COROSIO_TIMER_HPP -#define BOOST_COROSIO_TIMER_HPP +#ifndef BOOST_COROSIO_DETAIL_TIMER_HPP +#define BOOST_COROSIO_DETAIL_TIMER_HPP #include -#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include +#include #include +#include +#include +#include #include -namespace boost::corosio { +namespace boost::corosio::detail { + +// Defined in timer_service.hpp, which includes this header. The +// implementation::wait() body needs waiter_node and timer_service to +// be complete, so it is declared here and defined there; intrusive_list +// only stores waiter_node pointers, so the forward declaration suffices +// for implementation's data layout. +class timer_service; +struct waiter_node; /** An asynchronous timer for coroutine I/O. @@ -47,11 +65,106 @@ namespace boost::corosio { the nearest expiry drives the reactor's poll timeout, and expirations are processed in the run loop. */ -class BOOST_COROSIO_DECL timer : public io_timer +class BOOST_COROSIO_DECL timer : public io_object { + struct wait_awaitable + { + timer& t_; + mutable std::error_code ec_; + capy::continuation cont_; + + explicit wait_awaitable(timer& t) noexcept : t_(t) {} + + bool await_ready() const noexcept + { + return false; + } + + // Cancellation surfaces through ec_: the stop_token path in + // wait() completes the waiter with error::canceled written to + // ec_, so there is no separate token to consult here. + capy::io_result<> await_resume() const noexcept + { + return {ec_}; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + -> std::coroutine_handle<> + { + cont_.h = h; + auto& impl = t_.get(); + // Inline fast path: already expired and not in the heap + if (impl.heap_index_ == implementation::npos && + (impl.expiry_ == (time_point::min)() || + impl.expiry_ <= clock_type::now())) + { + ec_ = {}; + auto d = env->executor; + d.post(cont_); + return std::noop_coroutine(); + } + return impl.wait(h, env->executor, env->stop_token, &ec_, &cont_); + } + }; + public: - /// Alias for backward compatibility. - using implementation = io_timer::implementation; + /** Backend state and wait entry point for a timer. + + Holds per-timer state (expiry, heap position, waiter list) and + the `wait` entry point used by the awaitable returned from + @ref timer::wait. There is exactly one concrete timer backend, + so `wait` is a plain member function rather than a virtual + dispatch point. + */ + struct implementation : io_object::implementation + { + /// Sentinel value indicating the timer is not in the heap. + static constexpr std::size_t npos = + (std::numeric_limits::max)(); + + /// The absolute expiry time point. + std::chrono::steady_clock::time_point expiry_{}; + + /// Index in the timer service's min-heap, or `npos`. + std::size_t heap_index_ = npos; + + /// True if `wait()` has been called since last cancel. + bool might_have_pending_waits_ = false; + + /// The timer service that owns this implementation. + timer_service* svc_ = nullptr; + + /// Coroutines currently waiting on this timer's expiry. + intrusive_list waiters_; + + /// Free list linkage, reused when this impl is recycled. + implementation* next_free_ = nullptr; + + /// Construct bound to the given timer service. + explicit implementation(timer_service& svc) noexcept : svc_(&svc) {} + + /// Initiate an asynchronous wait for the timer to expire. + // Exported at member level: dllexport on the enclosing timer + // class does not extend to nested classes, and header-inline + // callers (wait_awaitable::await_suspend) reference this + // symbol from outside the corosio DLL. + BOOST_COROSIO_DECL + std::coroutine_handle<> wait( + std::coroutine_handle<>, + capy::executor_ref, + std::stop_token, + std::error_code*, + capy::continuation*); + }; + + /// The clock type used for time operations. + using clock_type = std::chrono::steady_clock; + + /// The time point type for absolute expiry times. + using time_point = clock_type::time_point; + + /// The duration type for relative expiry times. + using duration = clock_type::duration; /** Destructor. @@ -172,6 +285,20 @@ class BOOST_COROSIO_DECL timer : public io_timer timer(timer const&) = delete; timer& operator=(timer const&) = delete; + /** Cancel all pending asynchronous wait operations. + + All outstanding operations complete with an error code that + compares equal to `capy::cond::canceled`. + + @return The number of operations that were cancelled. + */ + std::size_t cancel() + { + if (!get().might_have_pending_waits_) + return 0; + return do_cancel(); + } + /** Cancel one pending asynchronous wait operation. The oldest pending wait is cancelled (FIFO order). It @@ -187,6 +314,16 @@ class BOOST_COROSIO_DECL timer : public io_timer return do_cancel_one(); } + /** Return the timer's expiry time as an absolute time. + + @return The expiry time point. If no expiry has been set, + returns a default-constructed time_point. + */ + time_point expiry() const noexcept + { + return get().expiry_; + } + /** Set the timer's expiry time as an absolute time. Any pending asynchronous wait operations will be cancelled. @@ -242,11 +379,34 @@ class BOOST_COROSIO_DECL timer : public io_timer return expires_after(std::chrono::duration_cast(d)); } + /** Wait for the timer to expire. + + Multiple coroutines may wait on the same timer concurrently. + When the timer expires, all waiters complete with success. + + The operation supports cancellation via `std::stop_token` through + the affine awaitable protocol. If the associated stop token is + triggered, only that waiter completes with an error that + compares equal to `capy::cond::canceled`; other waiters are + unaffected. + + This timer must outlive the returned awaitable. + + @return An awaitable that completes with `io_result<>`. + */ + auto wait() + { + return wait_awaitable(*this); + } + protected: - explicit timer(handle h) noexcept : io_timer(std::move(h)) {} + explicit timer(handle h) noexcept : io_object(std::move(h)) {} private: - std::size_t do_cancel() override; + // Defined in src/corosio/src/timer.cpp, which includes both this + // header and timer_service.hpp, so the timer_service_* free + // functions are visible there. + std::size_t do_cancel(); std::size_t do_cancel_one(); std::size_t do_update_expiry(); @@ -257,6 +417,6 @@ class BOOST_COROSIO_DECL timer : public io_timer } }; -} // namespace boost::corosio +} // namespace boost::corosio::detail #endif diff --git a/include/boost/corosio/detail/timer_service.hpp b/include/boost/corosio/detail/timer_service.hpp index c02ef6755..cf7c2ed46 100644 --- a/include/boost/corosio/detail/timer_service.hpp +++ b/include/boost/corosio/detail/timer_service.hpp @@ -11,7 +11,7 @@ #ifndef BOOST_COROSIO_DETAIL_TIMER_SERVICE_HPP #define BOOST_COROSIO_DETAIL_TIMER_SERVICE_HPP -#include +#include #include #include #include @@ -46,8 +46,8 @@ struct scheduler; error output, stop_token, embedded completion_op. Each concurrent co_await t.wait() allocates one waiter_node. - timer_service::implementation holds per-timer state: expiry, - heap index, and an intrusive_list of waiter_nodes. Multiple + timer::implementation holds per-timer state: expiry, heap + index, and an intrusive_list of waiter_nodes. Multiple coroutines can wait on the same timer simultaneously. timer_service owns a min-heap of active timers, a free list @@ -112,13 +112,11 @@ class BOOST_COROSIO_DECL timer_service final } }; - struct implementation; - private: struct heap_entry { time_point time_; - implementation* timer_; + timer::implementation* timer_; }; scheduler* sched_ = nullptr; @@ -126,7 +124,7 @@ class BOOST_COROSIO_DECL timer_service final BOOST_COROSIO_MSVC_WARNING_DISABLE(4251) // std:: members, dll-interface mutable std::mutex mutex_; std::vector heap_; - implementation* free_list_ = nullptr; + timer::implementation* free_list_ = nullptr; waiter_node* waiter_free_list_ = nullptr; callback on_earliest_changed_; bool shutting_down_ = false; @@ -184,7 +182,7 @@ class BOOST_COROSIO_DECL timer_service final inline void destroy(io_object::implementation* p) override; /// Cancel and recycle a timer implementation. - inline void destroy_impl(implementation& impl); + inline void destroy_impl(timer::implementation& impl); /// Create or recycle a waiter node. inline waiter_node* create_waiter(); @@ -193,19 +191,20 @@ class BOOST_COROSIO_DECL timer_service final inline void destroy_waiter(waiter_node* w); /// Update the timer expiry, cancelling existing waiters. - inline std::size_t update_timer(implementation& impl, time_point new_time); + inline std::size_t update_timer( + timer::implementation& impl, time_point new_time); /// Insert a waiter into the timer's waiter list and the heap. - inline void insert_waiter(implementation& impl, waiter_node* w); + inline void insert_waiter(timer::implementation& impl, waiter_node* w); /// Cancel all waiters on a timer. - inline std::size_t cancel_timer(implementation& impl); + inline std::size_t cancel_timer(timer::implementation& impl); /// Cancel a single waiter ( stop_token callback path ). inline void cancel_waiter(waiter_node* w); /// Cancel one waiter on a timer. - inline std::size_t cancel_one_waiter(implementation& impl); + inline std::size_t cancel_one_waiter(timer::implementation& impl); /// Complete all waiters whose timers have expired. inline std::size_t process_expired(); @@ -218,7 +217,7 @@ class BOOST_COROSIO_DECL timer_service final cached_nearest_ns_.store(ns, std::memory_order_release); } - inline void remove_timer_impl(implementation& impl); + inline void remove_timer_impl(timer::implementation& impl); inline void up_heap(std::size_t index); inline void down_heap(std::size_t index); inline void swap_heap(std::size_t i1, std::size_t i2); @@ -249,7 +248,7 @@ struct BOOST_COROSIO_SYMBOL_VISIBLE waiter_node }; // nullptr once removed from timer's waiter list (concurrency marker) - timer_service::implementation* impl_ = nullptr; + timer::implementation* impl_ = nullptr; timer_service* svc_ = nullptr; std::coroutine_handle<> h_; capy::continuation* cont_ = nullptr; @@ -267,37 +266,15 @@ struct BOOST_COROSIO_SYMBOL_VISIBLE waiter_node } }; -struct timer_service::implementation final : timer::implementation -{ - using clock_type = std::chrono::steady_clock; - using time_point = clock_type::time_point; - using duration = clock_type::duration; - - timer_service* svc_ = nullptr; - intrusive_list waiters_; - - // Free list linkage (reused when impl is on free_list) - implementation* next_free_ = nullptr; - - inline explicit implementation(timer_service& svc) noexcept; - - inline std::coroutine_handle<> wait( - std::coroutine_handle<>, - capy::executor_ref, - std::stop_token, - std::error_code*, - capy::continuation*) override; -}; - // Thread-local caches avoid hot-path mutex acquisitions: // 1. Impl cache — single-slot, validated by comparing svc_ // 2. Waiter cache — single-slot, no service affinity // All caches are cleared by timer_service_invalidate_cache() during shutdown. -inline thread_local_ptr tl_cached_impl; +inline thread_local_ptr tl_cached_impl; inline thread_local_ptr tl_cached_waiter; -inline timer_service::implementation* +inline timer::implementation* try_pop_tl_cache(timer_service* svc) noexcept { auto* impl = tl_cached_impl.get(); @@ -313,7 +290,7 @@ try_pop_tl_cache(timer_service* svc) noexcept } inline bool -try_push_tl_cache(timer_service::implementation* impl) noexcept +try_push_tl_cache(timer::implementation* impl) noexcept { if (!tl_cached_impl.get()) { @@ -358,12 +335,6 @@ timer_service_invalidate_cache() noexcept // timer_service out-of-class member function definitions -inline timer_service::implementation::implementation( - timer_service& svc) noexcept - : svc_(&svc) -{ -} - inline void timer_service::shutdown() { @@ -374,7 +345,7 @@ timer_service::shutdown() // coroutine-owned timer destructors (triggered by h.destroy() // below) cannot re-enter remove_timer_impl() and mutate the // vector during iteration. - std::vector impls; + std::vector impls; impls.reserve(heap_.size()); for (auto& entry : heap_) { @@ -424,7 +395,7 @@ timer_service::shutdown() inline io_object::implementation* timer_service::construct() { - implementation* impl = try_pop_tl_cache(this); + timer::implementation* impl = try_pop_tl_cache(this); if (impl) { impl->svc_ = this; @@ -445,7 +416,7 @@ timer_service::construct() } else { - impl = new implementation(*this); + impl = new timer::implementation(*this); } return impl; } @@ -453,11 +424,11 @@ timer_service::construct() inline void timer_service::destroy(io_object::implementation* p) { - destroy_impl(static_cast(*p)); + destroy_impl(static_cast(*p)); } inline void -timer_service::destroy_impl(implementation& impl) +timer_service::destroy_impl(timer::implementation& impl) { // During shutdown the impl is owned by the shutdown loop. // Re-entering here (from a coroutine-owned timer destructor @@ -513,7 +484,7 @@ timer_service::destroy_waiter(waiter_node* w) } inline std::size_t -timer_service::update_timer(implementation& impl, time_point new_time) +timer_service::update_timer(timer::implementation& impl, time_point new_time) { bool in_heap = (impl.heap_index_ != (std::numeric_limits::max)()); @@ -563,11 +534,15 @@ timer_service::update_timer(implementation& impl, time_point new_time) } inline void -timer_service::insert_waiter(implementation& impl, waiter_node* w) +timer_service::insert_waiter(timer::implementation& impl, waiter_node* w) { - bool notify = false; + bool notify = false; + bool lost_cancel = false; { std::lock_guard lock(mutex_); + // Publish: from here the waiter is visible to the fire path and + // to its own stop callback (impl_ non-null enables cancel_waiter). + w->impl_ = &impl; if (impl.heap_index_ == (std::numeric_limits::max)()) { impl.heap_index_ = heap_.size(); @@ -577,13 +552,35 @@ timer_service::insert_waiter(implementation& impl, waiter_node* w) refresh_cached_nearest(); } impl.waiters_.push_back(w); + + // Lost-cancel re-check: a stop requested after the canceller was + // armed in wait() but before this publication found impl_ null + // and returned a no-op. Observe it now and undo the insertion. + if (w->token_.stop_requested()) + { + w->impl_ = nullptr; + impl.waiters_.remove(w); + if (impl.waiters_.empty()) + { + remove_timer_impl(impl); + impl.might_have_pending_waits_ = false; + } + refresh_cached_nearest(); + lost_cancel = true; + notify = false; // insertion undone; nearest unchanged + } } if (notify) on_earliest_changed_(); + if (lost_cancel) + { + w->ec_value_ = make_error_code(capy::error::canceled); + sched_->post(&w->op_); + } } inline std::size_t -timer_service::cancel_timer(implementation& impl) +timer_service::cancel_timer(timer::implementation& impl) { if (!impl.might_have_pending_waits_) return 0; @@ -646,7 +643,7 @@ timer_service::cancel_waiter(waiter_node* w) } inline std::size_t -timer_service::cancel_one_waiter(implementation& impl) +timer_service::cancel_one_waiter(timer::implementation& impl) { if (!impl.might_have_pending_waits_) return 0; @@ -683,7 +680,7 @@ timer_service::process_expired() while (!heap_.empty() && heap_[0].time_ <= now) { - implementation* t = heap_[0].timer_; + timer::implementation* t = heap_[0].timer_; remove_timer_impl(*t); while (auto* w = t->waiters_.pop_front()) { @@ -708,7 +705,7 @@ timer_service::process_expired() } inline void -timer_service::remove_timer_impl(implementation& impl) +timer_service::remove_timer_impl(timer::implementation& impl) { std::size_t index = impl.heap_index_; if (index >= heap_.size()) @@ -851,67 +848,33 @@ waiter_node::completion_op::destroy() #pragma GCC diagnostic pop #endif -inline std::coroutine_handle<> -timer_service::implementation::wait( - std::coroutine_handle<> h, - capy::executor_ref d, - std::stop_token token, - std::error_code* ec, - capy::continuation* cont) -{ - // Already-expired fast path — no waiter_node, no mutex. - // Post instead of dispatch so the coroutine yields to the - // scheduler, allowing other queued work to run. - if (heap_index_ == (std::numeric_limits::max)()) - { - if (expiry_ == (time_point::min)() || expiry_ <= clock_type::now()) - { - if (ec) - *ec = {}; - d.post(*cont); - return std::noop_coroutine(); - } - } - - auto* w = svc_->create_waiter(); - w->impl_ = this; - w->svc_ = svc_; - w->h_ = h; - w->cont_ = cont; - w->d_ = d; - w->token_ = std::move(token); - w->ec_out_ = ec; - - svc_->insert_waiter(*this, w); - might_have_pending_waits_ = true; - svc_->get_scheduler().work_started(); - - if (w->token_.stop_possible()) - w->stop_cb_.emplace(w->token_, waiter_node::canceller{w}); - - return std::noop_coroutine(); -} +// timer::implementation::wait() is defined in timer.cpp, not here. +// It must be a non-inline definition in a translation unit that is +// always pulled into the link whenever detail::timer is used (every +// consumer needs timer's constructors from that same object file). +// An inline definition in this header would only be emitted in +// translation units that happen to also include this header, which +// is not guaranteed for every caller of wait_awaitable::await_suspend +// in timer.hpp (e.g. code that only reaches timer.hpp through +// delay.hpp, without transitively including a scheduler header). // Free functions inline std::size_t -timer_service_update_expiry(timer::implementation& base) +timer_service_update_expiry(timer::implementation& impl) { - auto& impl = static_cast(base); return impl.svc_->update_timer(impl, impl.expiry_); } inline std::size_t -timer_service_cancel(timer::implementation& base) noexcept +timer_service_cancel(timer::implementation& impl) noexcept { - auto& impl = static_cast(base); return impl.svc_->cancel_timer(impl); } inline std::size_t -timer_service_cancel_one(timer::implementation& base) noexcept +timer_service_cancel_one(timer::implementation& impl) noexcept { - auto& impl = static_cast(base); return impl.svc_->cancel_one_waiter(impl); } diff --git a/include/boost/corosio/io/io_timer.hpp b/include/boost/corosio/io/io_timer.hpp deleted file mode 100644 index f69d3799f..000000000 --- a/include/boost/corosio/io/io_timer.hpp +++ /dev/null @@ -1,204 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_IO_IO_TIMER_HPP -#define BOOST_COROSIO_IO_IO_TIMER_HPP - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace boost::corosio { - -/** Abstract base for asynchronous timers. - - Provides the common timer interface: `wait`, `cancel`, and - `expiry`. Concrete classes like @ref timer add the ability - to set expiry times and cancel individual waiters. - - @par Thread Safety - Distinct objects: Safe. - Shared objects: Unsafe. - - @see timer, io_object -*/ -class BOOST_COROSIO_DECL io_timer : public io_object -{ - struct wait_awaitable - { - io_timer& t_; - std::stop_token token_; - mutable std::error_code ec_; - capy::continuation cont_; - - explicit wait_awaitable(io_timer& t) noexcept : t_(t) {} - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } - - capy::io_result<> await_resume() const noexcept - { - if (token_.stop_requested()) - return {capy::error::canceled}; - return {ec_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; - cont_.h = h; - auto& impl = t_.get(); - // Inline fast path: already expired and not in the heap - if (impl.heap_index_ == implementation::npos && - (impl.expiry_ == (time_point::min)() || - impl.expiry_ <= clock_type::now())) - { - ec_ = {}; - token_ = {}; // match normal path so await_resume - // returns ec_, not a stale stop check - auto d = env->executor; - d.post(cont_); - return std::noop_coroutine(); - } - return impl.wait(h, env->executor, std::move(token_), &ec_, &cont_); - } - }; - -public: - /** Backend interface for timer wait operations. - - Holds per-timer state (expiry, heap position) and provides - the virtual `wait` entry point that concrete timer services - override. - */ - struct implementation : io_object::implementation - { - /// Sentinel value indicating the timer is not in the heap. - static constexpr std::size_t npos = - (std::numeric_limits::max)(); - - /// The absolute expiry time point. - std::chrono::steady_clock::time_point expiry_{}; - - /// Index in the timer service's min-heap, or `npos`. - std::size_t heap_index_ = npos; - - /// True if `wait()` has been called since last cancel. - bool might_have_pending_waits_ = false; - - /// Initiate an asynchronous wait for the timer to expire. - virtual std::coroutine_handle<> wait( - std::coroutine_handle<>, - capy::executor_ref, - std::stop_token, - std::error_code*, - capy::continuation*) = 0; - }; - - /// The clock type used for time operations. - using clock_type = std::chrono::steady_clock; - - /// The time point type for absolute expiry times. - using time_point = clock_type::time_point; - - /// The duration type for relative expiry times. - using duration = clock_type::duration; - - /** Cancel all pending asynchronous wait operations. - - All outstanding operations complete with an error code that - compares equal to `capy::cond::canceled`. - - @return The number of operations that were cancelled. - */ - std::size_t cancel() - { - if (!get().might_have_pending_waits_) - return 0; - return do_cancel(); - } - - /** Return the timer's expiry time as an absolute time. - - @return The expiry time point. If no expiry has been set, - returns a default-constructed time_point. - */ - time_point expiry() const noexcept - { - return get().expiry_; - } - - /** Wait for the timer to expire. - - Multiple coroutines may wait on the same timer concurrently. - When the timer expires, all waiters complete with success. - - The operation supports cancellation via `std::stop_token` through - the affine awaitable protocol. If the associated stop token is - triggered, only that waiter completes with an error that - compares equal to `capy::cond::canceled`; other waiters are - unaffected. - - This timer must outlive the returned awaitable. - - @return An awaitable that completes with `io_result<>`. - */ - auto wait() - { - return wait_awaitable(*this); - } - -protected: - /** Dispatch cancel to the concrete implementation. - - @return The number of operations that were cancelled. - */ - virtual std::size_t do_cancel() = 0; - - explicit io_timer(handle h) noexcept : io_object(std::move(h)) {} - - /// Move construct. - io_timer(io_timer&& other) noexcept : io_object(std::move(other)) {} - - /// Move assign. - io_timer& operator=(io_timer&& other) noexcept - { - if (this != &other) - h_ = std::move(other.h_); - return *this; - } - - io_timer(io_timer const&) = delete; - io_timer& operator=(io_timer const&) = delete; - - /// Return the underlying implementation. - implementation& get() const noexcept - { - return *static_cast(h_.get()); - } -}; - -} // namespace boost::corosio - -#endif diff --git a/include/boost/corosio/native/native.hpp b/include/boost/corosio/native/native.hpp index a5b277e3c..affe8fa62 100644 --- a/include/boost/corosio/native/native.hpp +++ b/include/boost/corosio/native/native.hpp @@ -10,14 +10,12 @@ /** @file native.hpp Include all native (devirtualized) public headers: - I/O context, sockets, acceptor, resolver, signal set, - timer, and cancellation helpers. + I/O context, sockets, acceptor, resolver, and signal set. */ #ifndef BOOST_COROSIO_NATIVE_NATIVE_HPP #define BOOST_COROSIO_NATIVE_NATIVE_HPP -#include #include #include #include @@ -28,7 +26,6 @@ #include #include #include -#include #include #endif diff --git a/include/boost/corosio/native/native_cancel.hpp b/include/boost/corosio/native/native_cancel.hpp deleted file mode 100644 index b6e61bc12..000000000 --- a/include/boost/corosio/native/native_cancel.hpp +++ /dev/null @@ -1,237 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_NATIVE_CANCEL_HPP -#define BOOST_COROSIO_NATIVE_NATIVE_CANCEL_HPP - -#include -#include -#include - -#include -#include - -namespace boost::corosio { - -/** Cancel an operation if it does not complete by a deadline. - - Overload for @ref native_timer that devirtualizes the internal - timer wait, allowing the compiler to inline the timer path. - Otherwise identical to the @ref timer overload. - - If the deadline is reached first, the inner operation completes - with an error comparing equal to `capy::cond::canceled`. If the - inner operation completes first, the timer is cancelled. Parent - cancellation is forwarded to both. - - The timer's expiry is overwritten by this call. The timer must - outlive the returned awaitable. Do not issue overlapping waits - on the same timer. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The deadline expires and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @par Example - @code - native_timer t( ioc ); - auto [ec, n] = co_await cancel_at( - sock.read_some( buf ), t, - clock::now() + 5s ); - if (ec == capy::cond::canceled) - // timed out or parent cancelled - @endcode - - @tparam Backend A backend tag value (e.g., `epoll`). - - @param op The inner I/O awaitable to wrap. - @param t The native timer to use for the deadline. Must outlive - the returned awaitable. - @param deadline The absolute time point at which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_after, native_timer -*/ -template -auto -cancel_at( - capy::IoAwaitable auto&& op, - native_timer& t, - timer::time_point deadline) -{ - return detail::cancel_at_awaitable< - std::decay_t, native_timer>( - std::forward(op), t, deadline); -} - -/** Cancel an operation if it does not complete within a duration. - - Overload for @ref native_timer. Equivalent to - `cancel_at( op, t, clock::now() + timeout )`. - - The timer's expiry is overwritten by this call. The timer must - outlive the returned awaitable. Do not issue overlapping waits - on the same timer. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The timeout elapses and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @par Example - @code - native_timer t( ioc ); - auto [ec, n] = co_await cancel_after( - sock.read_some( buf ), t, 5s ); - if (ec == capy::cond::canceled) - // timed out - @endcode - - @tparam Backend A backend tag value (e.g., `epoll`). - - @param op The inner I/O awaitable to wrap. - @param t The native timer to use for the timeout. Must outlive - the returned awaitable. - @param timeout The relative duration after which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_at, native_timer -*/ -template -auto -cancel_after( - capy::IoAwaitable auto&& op, - native_timer& t, - timer::duration timeout) -{ - return cancel_at( - std::forward(op), t, timer::clock_type::now() + timeout); -} - -/** Cancel an operation if it does not complete by a deadline. - - Convenience overload that creates a @ref native_timer internally, - devirtualizing the timer wait. Otherwise identical to the - explicit-timer overload. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The deadline expires and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @note Creates a timer per call. Use the explicit-timer overload - to amortize allocation across multiple timeouts. - - @note The awaiting coroutine's executor must be backed by an - io_context (the deadline timer is built from it). Awaiting this - on a non-io_context executor is a precondition violation and - aborts; use the explicit-timer overload to construct the timer - yourself if you need a catchable error. - - @par Example - @code - auto [ec, n] = co_await cancel_at( - sock.read_some( buf ), - clock::now() + 5s ); - if (ec == capy::cond::canceled) - // timed out or parent cancelled - @endcode - - @tparam Backend A backend tag value (e.g., `epoll`). - - @param op The inner I/O awaitable to wrap. - @param deadline The absolute time point at which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_after, native_timer -*/ -template -auto -cancel_at(capy::IoAwaitable auto&& op, timer::time_point deadline) -{ - return detail::cancel_at_awaitable< - std::decay_t, native_timer, true>( - std::forward(op), deadline); -} - -/** Cancel an operation if it does not complete within a duration. - - Convenience overload that creates a @ref native_timer internally. - Equivalent to `cancel_at( op, clock::now() + timeout )`. - - @par Completion Conditions - The returned awaitable resumes when either: - @li The inner operation completes (successfully or with error). - @li The timeout elapses and the inner operation is cancelled. - @li The caller's stop token is triggered, cancelling both. - - @par Error Conditions - @li On timeout or parent cancellation, the inner operation - completes with an error equal to `capy::cond::canceled`. - @li All other errors are propagated from the inner operation. - - @note Creates a timer per call. Use the explicit-timer overload - to amortize allocation across multiple timeouts. - - @note The awaiting coroutine's executor must be backed by an - io_context (the deadline timer is built from it). Awaiting this - on a non-io_context executor is a precondition violation and - aborts; use the explicit-timer overload to construct the timer - yourself if you need a catchable error. - - @par Example - @code - auto [ec, n] = co_await cancel_after( - sock.read_some( buf ), 5s ); - if (ec == capy::cond::canceled) - // timed out - @endcode - - @tparam Backend A backend tag value (e.g., `epoll`). - - @param op The inner I/O awaitable to wrap. - @param timeout The relative duration after which to cancel. - - @return An awaitable whose result matches @p op's result type. - - @see cancel_at, native_timer -*/ -template -auto -cancel_after(capy::IoAwaitable auto&& op, timer::duration timeout) -{ - return cancel_at( - std::forward(op), timer::clock_type::now() + timeout); -} - -} // namespace boost::corosio - -#endif diff --git a/include/boost/corosio/native/native_timer.hpp b/include/boost/corosio/native/native_timer.hpp deleted file mode 100644 index 93c5a76e5..000000000 --- a/include/boost/corosio/native/native_timer.hpp +++ /dev/null @@ -1,208 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_NATIVE_TIMER_HPP -#define BOOST_COROSIO_NATIVE_NATIVE_TIMER_HPP - -#include -#include -#include - -namespace boost::corosio { - -/** An asynchronous timer with devirtualized wait operations. - - This class template inherits from @ref timer and shadows the - `wait` operation with a version that calls the backend - implementation directly, allowing the compiler to inline - through the entire call chain. - - Non-async operations (`cancel`, `expires_at`, `expires_after`) - remain unchanged and dispatch through the compiled library. - - A `native_timer` IS-A `timer` and can be passed to any function - expecting `timer&`. - - @tparam Backend A backend tag value (e.g., `epoll`). - The timer implementation is backend-independent; the - tag selects the concrete impl type for devirtualization. - - @par Thread Safety - Same as @ref timer. - - @see timer, epoll_t, iocp_t -*/ -template -class native_timer : public timer -{ - using impl_type = detail::timer_service::implementation; - - impl_type& get_impl() noexcept - { - return *static_cast(h_.get()); - } - - struct native_wait_awaitable - { - native_timer& self_; - std::stop_token token_; - mutable std::error_code ec_; - capy::continuation cont_; - - explicit native_wait_awaitable(native_timer& self) noexcept - : self_(self) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } - - capy::io_result<> await_resume() const noexcept - { - if (token_.stop_requested()) - return {capy::error::canceled}; - return {ec_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; - cont_.h = h; - auto& impl = self_.get_impl(); - // Fast path: already expired and not in the heap - if (impl.heap_index_ == timer::implementation::npos && - (impl.expiry_ == (time_point::min)() || - impl.expiry_ <= clock_type::now())) - { - ec_ = {}; - auto d = env->executor; - d.post(cont_); - return std::noop_coroutine(); - } - return impl.wait(h, env->executor, std::move(token_), &ec_, &cont_); - } - }; - -public: - /** Construct a native timer from an execution context. - - @param ctx The execution context that will own this timer. - */ - explicit native_timer(capy::execution_context& ctx) : timer(ctx) {} - - /** Construct a native timer with an initial absolute expiry. - - @param ctx The execution context that will own this timer. - @param t The initial expiry time point. - */ - native_timer(capy::execution_context& ctx, time_point t) : timer(ctx, t) {} - - /** Construct a native timer with an initial relative expiry. - - @param ctx The execution context that will own this timer. - @param d The initial expiry duration relative to now. - */ - template - native_timer( - capy::execution_context& ctx, std::chrono::duration d) - : timer(ctx, d) - { - } - - /** Construct a native timer from an executor. - - The timer is associated with the executor's context, which must - be a corosio io_context. - - @param ex The executor whose context will own this timer. - - @throws std::logic_error if the executor's context is not an - io_context. - */ - template - requires(!std::same_as, native_timer>) && - capy::Executor - explicit native_timer(Ex const& ex) : native_timer(ex.context()) - { - } - - /** Construct a native timer from an executor with an absolute expiry. - - @param ex The executor whose context will own this timer. - @param t The initial expiry time point. - - @throws std::logic_error if the executor's context is not an - io_context. - */ - template - requires capy::Executor - native_timer(Ex const& ex, time_point t) : native_timer(ex.context(), t) - { - } - - /** Construct a native timer from an executor with a relative expiry. - - @param ex The executor whose context will own this timer. - @param d The initial expiry duration relative to now. - - @throws std::logic_error if the executor's context is not an - io_context. - */ - template - requires capy::Executor - native_timer(Ex const& ex, std::chrono::duration d) - : native_timer(ex.context(), d) - { - } - - /** Move construct. - - @param other The timer to move from. - - @pre No awaitables returned by @p other's methods exist. - @pre The execution context associated with @p other must - outlive this timer. - */ - native_timer(native_timer&&) noexcept = default; - - /** Move assign. - - @param other The timer to move from. - - @pre No awaitables returned by either `*this` or @p other's - methods exist. - @pre The execution context associated with @p other must - outlive this timer. - */ - native_timer& operator=(native_timer&&) noexcept = default; - - native_timer(native_timer const&) = delete; - native_timer& operator=(native_timer const&) = delete; - - /** Wait for the timer to expire. - - Calls the backend implementation directly, bypassing virtual - dispatch. Otherwise identical to @ref timer::wait. - - @return An awaitable yielding `io_result<>`. - - This timer must outlive the returned awaitable. - */ - auto wait() - { - return native_wait_awaitable(*this); - } -}; - -} // namespace boost::corosio - -#endif diff --git a/include/boost/corosio/timeout.hpp b/include/boost/corosio/timeout.hpp new file mode 100644 index 000000000..17d281910 --- /dev/null +++ b/include/boost/corosio/timeout.hpp @@ -0,0 +1,103 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_TIMEOUT_HPP +#define BOOST_COROSIO_TIMEOUT_HPP + +#include +#include +#include + +#include +#include +#include + +namespace boost::corosio { + +/** Race an io_result-returning awaitable against a deadline. + + Starts the awaitable with an interposed stop token and arms a + timer. If the awaitable finishes first, its result is returned + as-is (success, error, or exception). If the deadline passes + first, the awaitable is cancelled and an `io_result` with + `ec == capy::error::timeout` and default-initialized payload + is produced. + + Exceptions from the inner awaitable always propagate; they are + never swallowed by the timer. + + @par Preconditions + The awaiting coroutine's executor must belong to an + `io_context`; any other execution context terminates with a + diagnostic. + + @par Cancellation + If the parent's stop token is activated, the inner awaitable + is cancelled and its cancellation result is returned. Requesting + stop from another thread requires a multi-threaded-capable + io_context; a context running in single_threaded mode + (auto-enabled at concurrency_hint == 1) does not permit + cross-thread cancellation. + + @par Example + @code + auto [ec, n] = co_await timeout(sock.read_some(buf), 50ms); + if (ec == capy::cond::timeout) { + // handle timeout + } + @endcode + + @param a The awaitable to race against the deadline. + @param dur The maximum duration to wait, measured from + suspension. + + @return An awaitable yielding `io_result` matching the inner + awaitable's result type. + + @see delay +*/ +template + requires detail::is_io_result_v> && + std::is_default_constructible_v> +[[nodiscard]] auto timeout(A a, std::chrono::duration dur) +{ + using namespace std::chrono; + // Clamp against overflow: a huge source duration would wrap when + // cast to nanoseconds. + auto ns = dur > duration_cast>(nanoseconds::max()) + ? nanoseconds::max() + : duration_cast(dur); + return detail::timeout_awaitable(std::move(a), ns); +} + +/** Race an io_result-returning awaitable against an absolute deadline. + + Behaves as the duration overload with the deadline fixed at + `tp` instead of measured from suspension. + + @param a The awaitable to race against the deadline. + @param tp The steady-clock time point at which the awaitable + is cancelled. + + @return An awaitable yielding `io_result` matching the inner + awaitable's result type. + + @see delay +*/ +template + requires detail::is_io_result_v> && + std::is_default_constructible_v> +[[nodiscard]] auto timeout(A a, std::chrono::steady_clock::time_point tp) +{ + return detail::timeout_awaitable(std::move(a), tp); +} + +} // namespace boost::corosio + +#endif diff --git a/perf/bench/CMakeLists.txt b/perf/bench/CMakeLists.txt index 1e47790bc..625c9a9b6 100644 --- a/perf/bench/CMakeLists.txt +++ b/perf/bench/CMakeLists.txt @@ -27,7 +27,6 @@ add_executable(corosio_bench corosio/socket_throughput_bench.cpp corosio/socket_latency_bench.cpp corosio/http_server_bench.cpp - corosio/timer_bench.cpp corosio/accept_churn_bench.cpp corosio/fan_out_bench.cpp corosio/local_socket_throughput_bench.cpp @@ -57,10 +56,8 @@ if (TARGET Boost::asio) ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/socket_throughput_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/socket_latency_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/http_server_bench.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/asio/coroutine/timer_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/coroutine/accept_churn_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/coroutine/fan_out_bench.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/timer_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/accept_churn_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/fan_out_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/coroutine/local_socket_throughput_bench.cpp diff --git a/perf/bench/asio/callback/benchmarks.hpp b/perf/bench/asio/callback/benchmarks.hpp index 31bfee4b8..2f7aebe50 100644 --- a/perf/bench/asio/callback/benchmarks.hpp +++ b/perf/bench/asio/callback/benchmarks.hpp @@ -26,9 +26,6 @@ bench::benchmark_suite make_socket_latency_suite(); /// Create the HTTP server benchmark suite. bench::benchmark_suite make_http_server_suite(); -/// Create the timer benchmark suite. -bench::benchmark_suite make_timer_suite(); - /// Create the accept churn benchmark suite. bench::benchmark_suite make_accept_churn_suite(); diff --git a/perf/bench/asio/callback/timer_bench.cpp b/perf/bench/asio/callback/timer_bench.cpp deleted file mode 100644 index d689d4e43..000000000 --- a/perf/bench/asio/callback/timer_bench.cpp +++ /dev/null @@ -1,273 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include "benchmarks.hpp" -#include "../socket_utils.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace asio = boost::asio; -using asio_bench::timer_type; - -namespace asio_callback_bench { -namespace { - -// Tight create/schedule/cancel/destroy loop. Same timer internals as the -// coroutine variant — isolates timer management cost without coroutine overhead. -void -bench_schedule_cancel(bench::state& state) -{ - asio::io_context ioc; - int64_t counter = 0; - int constexpr batch_size = 1000; - - perf::stopwatch sw; - auto deadline = std::chrono::steady_clock::now() + - std::chrono::duration(state.duration()); - - while (std::chrono::steady_clock::now() < deadline) - { - for (int i = 0; i < batch_size; ++i) - { - timer_type t(ioc.get_executor()); - t.expires_after(std::chrono::hours(1)); - t.cancel(); - ++counter; - } - - ioc.poll(); - ioc.restart(); - } - - ioc.run(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -struct fire_rate_op -{ - timer_type timer; - std::atomic& running; - int64_t& counter; - - fire_rate_op(asio::io_context& ioc, std::atomic& r, int64_t& c) - : timer(ioc.get_executor()) - , running(r) - , counter(c) - { - } - - void start() - { - if (!running.load(std::memory_order_relaxed)) - return; - timer.expires_after(std::chrono::nanoseconds(0)); - timer.async_wait([this](boost::system::error_code ec) { - if (ec) - return; - ++counter; - start(); - }); - } -}; - -// Zero-delay timer re-armed from its own callback. Compared against the -// coroutine variant, the difference isolates coroutine suspend/resume overhead. -void -bench_fire_rate(bench::state& state) -{ - asio::io_context ioc; - std::atomic running{true}; - int64_t counter = 0; - - fire_rate_op op(ioc, running, counter); - - perf::stopwatch sw; - - op.start(); - - std::thread timer([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - timer.join(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -void -bench_schedule_cancel_lockless(bench::state& state) -{ - asio::io_context ioc(BOOST_ASIO_CONCURRENCY_HINT_UNSAFE); - int64_t counter = 0; - int constexpr batch_size = 1000; - - perf::stopwatch sw; - auto deadline = std::chrono::steady_clock::now() + - std::chrono::duration(state.duration()); - - while (std::chrono::steady_clock::now() < deadline) - { - for (int i = 0; i < batch_size; ++i) - { - timer_type t(ioc.get_executor()); - t.expires_after(std::chrono::hours(1)); - t.cancel(); - ++counter; - } - - ioc.poll(); - ioc.restart(); - } - - ioc.run(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -void -bench_fire_rate_lockless(bench::state& state) -{ - asio::io_context ioc(BOOST_ASIO_CONCURRENCY_HINT_UNSAFE); - std::atomic running{true}; - int64_t counter = 0; - - fire_rate_op op(ioc, running, counter); - - perf::stopwatch sw; - - op.start(); - - std::thread timer([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - timer.join(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -struct concurrent_timer_op -{ - timer_type timer; - std::atomic& running; - std::chrono::microseconds interval; - int64_t& fire_count; - perf::statistics& stats; - perf::stopwatch sw; - - concurrent_timer_op( - asio::io_context& ioc, - std::atomic& r, - std::chrono::microseconds iv, - int64_t& fc, - perf::statistics& st) - : timer(ioc.get_executor()) - , running(r) - , interval(iv) - , fire_count(fc) - , stats(st) - { - } - - void start() - { - if (!running.load(std::memory_order_relaxed)) - return; - sw.reset(); - timer.expires_after(interval); - timer.async_wait([this](boost::system::error_code ec) { - if (ec) - return; - stats.add(sw.elapsed_ns()); - ++fire_count; - start(); - }); - } -}; - -// N timers with staggered intervals (100us-1000us) firing concurrently. -// Stresses the timer queue under contention and reveals wake accuracy -// degradation as the number of pending timers grows. -void -bench_concurrent_timers(bench::state& state) -{ - int num_timers = static_cast(state.range(0)); - state.counters["num_timers"] = num_timers; - - asio::io_context ioc; - std::atomic running{true}; - std::vector fire_counts(num_timers, 0); - - // Each op writes latency directly to state.latency() - std::vector> ops; - ops.reserve(num_timers); - - perf::stopwatch total_sw; - - for (int i = 0; i < num_timers; ++i) - { - auto interval = std::chrono::microseconds( - 100 + (900 * i) / (num_timers > 1 ? num_timers - 1 : 1)); - ops.push_back( - std::make_unique( - ioc, running, interval, fire_counts[i], state.latency())); - ops.back()->start(); - } - - std::thread stopper([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - stopper.join(); - - state.set_elapsed(total_sw.elapsed_seconds()); - - int64_t total_fires = 0; - for (auto c : fire_counts) - total_fires += c; - state.add_items(total_fires); -} - -} // anonymous namespace - -bench::benchmark_suite -make_timer_suite() -{ - return bench::benchmark_suite("timer") - .add("schedule_cancel", bench_schedule_cancel) - .add("schedule_cancel_lockless", bench_schedule_cancel_lockless) - .add("fire_rate", bench_fire_rate) - .add("fire_rate_lockless", bench_fire_rate_lockless) - .add("concurrent", bench_concurrent_timers) - .args({10, 100, 1000}); -} - -} // namespace asio_callback_bench diff --git a/perf/bench/asio/coroutine/benchmarks.hpp b/perf/bench/asio/coroutine/benchmarks.hpp index a5c4efece..b3a9f291a 100644 --- a/perf/bench/asio/coroutine/benchmarks.hpp +++ b/perf/bench/asio/coroutine/benchmarks.hpp @@ -27,9 +27,6 @@ bench::benchmark_suite make_socket_latency_suite(); /// Create the HTTP server benchmark suite. bench::benchmark_suite make_http_server_suite(); -/// Create the timer benchmark suite. -bench::benchmark_suite make_timer_suite(); - /// Create the accept churn benchmark suite. bench::benchmark_suite make_accept_churn_suite(); diff --git a/perf/bench/asio/coroutine/fan_out_bench.cpp b/perf/bench/asio/coroutine/fan_out_bench.cpp index fa375f95f..f02e33138 100644 --- a/perf/bench/asio/coroutine/fan_out_bench.cpp +++ b/perf/bench/asio/coroutine/fan_out_bench.cpp @@ -10,6 +10,7 @@ #include "benchmarks.hpp" #include "../socket_utils.hpp" +#include #include #include #include @@ -50,8 +51,24 @@ echo_server(tcp_socket& sock) } } +// Notifies a parked timer (expires_at(time_point::max())) when all +// children have arrived: the last arriver's cancel() is the wakeup. +// Mirrors corosio's fan_out_latch for readability parity between the +// two benchmark sides. +struct fan_out_notifier +{ + std::atomic& remaining; + timer_type& timer; + + void arrive() + { + if (remaining.fetch_sub(1, std::memory_order_acq_rel) == 1) + timer.cancel(); + } +}; + asio::awaitable -sub_request(tcp_socket& client, std::atomic& remaining) +sub_request(tcp_socket& client, fan_out_notifier notifier) { char send_buf[64] = {}; char recv_buf[64]; @@ -67,7 +84,7 @@ sub_request(tcp_socket& client, std::atomic& remaining) { } - remaining.fetch_sub(1, std::memory_order_release); + notifier.arrive(); } // Parent spawns N sub-requests, waits for all N to complete, then repeats @@ -98,28 +115,30 @@ bench_fork_join(bench::state& state) auto parent = [&]() -> asio::awaitable { timer_type t(ioc); - try + while (running.load(std::memory_order_relaxed)) { - while (running.load(std::memory_order_relaxed)) + auto lp = state.lap(); + + std::atomic remaining{fan_out}; + // Park the timer at "never"; the last child's cancel() is + // the wakeup. + t.expires_at(std::chrono::steady_clock::time_point::max()); + for (int i = 0; i < fan_out; ++i) + asio::co_spawn( + ioc, + sub_request(clients[i], fan_out_notifier{remaining, t}), + asio::detached); + + // Guard closes the cancel-before-wait race: single run + // thread, so no child can cancel between the load and + // async_wait registering. + while (remaining.load(std::memory_order_acquire) > 0) { - auto lp = state.lap(); - - std::atomic remaining{fan_out}; - for (int i = 0; i < fan_out; ++i) - asio::co_spawn( - ioc, sub_request(clients[i], remaining), - asio::detached); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } + auto [ec] = + co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } } - catch (std::exception const&) - { - } for (auto& c : clients) c.close(); @@ -174,57 +193,57 @@ bench_nested(bench::state& state) std::atomic running{true}; auto group_task = [&](int base_idx, int n, - std::atomic& groups_remaining) + fan_out_notifier groups_notifier) -> asio::awaitable { std::atomic subs_remaining{n}; + timer_type t(ioc); + // Park the timer at "never"; the last child's cancel() is the + // wakeup. + t.expires_at(std::chrono::steady_clock::time_point::max()); for (int i = 0; i < n; ++i) asio::co_spawn( - ioc, sub_request(clients[base_idx + i], subs_remaining), + ioc, + sub_request( + clients[base_idx + i], fan_out_notifier{subs_remaining, t}), asio::detached); - timer_type t(ioc); - try - { - while (subs_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } - } - catch (std::exception const&) + // Guard closes the cancel-before-wait race: single run thread, + // so no child can cancel between the load and async_wait + // registering. + while (subs_remaining.load(std::memory_order_acquire) > 0) { + auto [ec] = co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } - groups_remaining.fetch_sub(1, std::memory_order_release); + groups_notifier.arrive(); }; auto parent = [&]() -> asio::awaitable { timer_type t(ioc); - try + while (running.load(std::memory_order_relaxed)) { - while (running.load(std::memory_order_relaxed)) + auto lp = state.lap(); + + std::atomic groups_remaining{groups}; + // Groups wait concurrently, so each group_task carries its + // own timer; this one only notifies on all-groups-done. + t.expires_at(std::chrono::steady_clock::time_point::max()); + for (int g = 0; g < groups; ++g) + asio::co_spawn( + ioc, + group_task( + g * subs_per_group, subs_per_group, + fan_out_notifier{groups_remaining, t}), + asio::detached); + + while (groups_remaining.load(std::memory_order_acquire) > 0) { - auto lp = state.lap(); - - std::atomic groups_remaining{groups}; - for (int g = 0; g < groups; ++g) - asio::co_spawn( - ioc, - group_task( - g * subs_per_group, subs_per_group, - groups_remaining), - asio::detached); - - while (groups_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } + auto [ec] = + co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } } - catch (std::exception const&) - { - } for (auto& c : clients) c.close(); @@ -284,28 +303,31 @@ bench_concurrent_parents(bench::state& state) int base = parent_idx * fan_out; timer_type t(ioc); - try + while (running.load(std::memory_order_relaxed)) { - while (running.load(std::memory_order_relaxed)) + auto lp = state.lap(); + + std::atomic remaining{fan_out}; + // Park the timer at "never"; the last child's cancel() is + // the wakeup. + t.expires_at(std::chrono::steady_clock::time_point::max()); + for (int i = 0; i < fan_out; ++i) + asio::co_spawn( + ioc, + sub_request( + clients[base + i], fan_out_notifier{remaining, t}), + asio::detached); + + // Guard closes the cancel-before-wait race: single run + // thread, so no child can cancel between the load and + // async_wait registering. + while (remaining.load(std::memory_order_acquire) > 0) { - auto lp = state.lap(); - - std::atomic remaining{fan_out}; - for (int i = 0; i < fan_out; ++i) - asio::co_spawn( - ioc, sub_request(clients[base + i], remaining), - asio::detached); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } + auto [ec] = + co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } } - catch (std::exception const&) - { - } if (parents_done.fetch_add(1, std::memory_order_acq_rel) == num_parents - 1) @@ -361,28 +383,30 @@ bench_fork_join_lockless(bench::state& state) auto parent = [&]() -> asio::awaitable { timer_type t(ioc); - try + while (running.load(std::memory_order_relaxed)) { - while (running.load(std::memory_order_relaxed)) + auto lp = state.lap(); + + std::atomic remaining{fan_out}; + // Park the timer at "never"; the last child's cancel() is + // the wakeup. + t.expires_at(std::chrono::steady_clock::time_point::max()); + for (int i = 0; i < fan_out; ++i) + asio::co_spawn( + ioc, + sub_request(clients[i], fan_out_notifier{remaining, t}), + asio::detached); + + // Guard closes the cancel-before-wait race: single run + // thread, so no child can cancel between the load and + // async_wait registering. + while (remaining.load(std::memory_order_acquire) > 0) { - auto lp = state.lap(); - - std::atomic remaining{fan_out}; - for (int i = 0; i < fan_out; ++i) - asio::co_spawn( - ioc, sub_request(clients[i], remaining), - asio::detached); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } + auto [ec] = + co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } } - catch (std::exception const&) - { - } for (auto& c : clients) c.close(); @@ -436,57 +460,57 @@ bench_nested_lockless(bench::state& state) std::atomic running{true}; auto group_task = [&](int base_idx, int n, - std::atomic& groups_remaining) + fan_out_notifier groups_notifier) -> asio::awaitable { std::atomic subs_remaining{n}; + timer_type t(ioc); + // Park the timer at "never"; the last child's cancel() is the + // wakeup. + t.expires_at(std::chrono::steady_clock::time_point::max()); for (int i = 0; i < n; ++i) asio::co_spawn( - ioc, sub_request(clients[base_idx + i], subs_remaining), + ioc, + sub_request( + clients[base_idx + i], fan_out_notifier{subs_remaining, t}), asio::detached); - timer_type t(ioc); - try - { - while (subs_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } - } - catch (std::exception const&) + // Guard closes the cancel-before-wait race: single run thread, + // so no child can cancel between the load and async_wait + // registering. + while (subs_remaining.load(std::memory_order_acquire) > 0) { + auto [ec] = co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } - groups_remaining.fetch_sub(1, std::memory_order_release); + groups_notifier.arrive(); }; auto parent = [&]() -> asio::awaitable { timer_type t(ioc); - try + while (running.load(std::memory_order_relaxed)) { - while (running.load(std::memory_order_relaxed)) + auto lp = state.lap(); + + std::atomic groups_remaining{groups}; + // Groups wait concurrently, so each group_task carries its + // own timer; this one only notifies on all-groups-done. + t.expires_at(std::chrono::steady_clock::time_point::max()); + for (int g = 0; g < groups; ++g) + asio::co_spawn( + ioc, + group_task( + g * subs_per_group, subs_per_group, + fan_out_notifier{groups_remaining, t}), + asio::detached); + + while (groups_remaining.load(std::memory_order_acquire) > 0) { - auto lp = state.lap(); - - std::atomic groups_remaining{groups}; - for (int g = 0; g < groups; ++g) - asio::co_spawn( - ioc, - group_task( - g * subs_per_group, subs_per_group, - groups_remaining), - asio::detached); - - while (groups_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } + auto [ec] = + co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } } - catch (std::exception const&) - { - } for (auto& c : clients) c.close(); @@ -545,28 +569,31 @@ bench_concurrent_parents_lockless(bench::state& state) int base = parent_idx * fan_out; timer_type t(ioc); - try + while (running.load(std::memory_order_relaxed)) { - while (running.load(std::memory_order_relaxed)) + auto lp = state.lap(); + + std::atomic remaining{fan_out}; + // Park the timer at "never"; the last child's cancel() is + // the wakeup. + t.expires_at(std::chrono::steady_clock::time_point::max()); + for (int i = 0; i < fan_out; ++i) + asio::co_spawn( + ioc, + sub_request( + clients[base + i], fan_out_notifier{remaining, t}), + asio::detached); + + // Guard closes the cancel-before-wait race: single run + // thread, so no child can cancel between the load and + // async_wait registering. + while (remaining.load(std::memory_order_acquire) > 0) { - auto lp = state.lap(); - - std::atomic remaining{fan_out}; - for (int i = 0; i < fan_out; ++i) - asio::co_spawn( - ioc, sub_request(clients[base + i], remaining), - asio::detached); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - } + auto [ec] = + co_await t.async_wait(asio::as_tuple(asio::deferred)); + (void)ec; } } - catch (std::exception const&) - { - } if (parents_done.fetch_add(1, std::memory_order_acq_rel) == num_parents - 1) diff --git a/perf/bench/asio/coroutine/timer_bench.cpp b/perf/bench/asio/coroutine/timer_bench.cpp deleted file mode 100644 index a764ff3f4..000000000 --- a/perf/bench/asio/coroutine/timer_bench.cpp +++ /dev/null @@ -1,250 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include "benchmarks.hpp" -#include "../socket_utils.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace asio = boost::asio; - -namespace asio_bench { -namespace { - -// Tight create/schedule/cancel/destroy loop. Asio manages timers in a -// per-context ordered list without timerfd, so this is bounded by -// list insertion cost and steady_clock::now() calls. -void -bench_schedule_cancel(bench::state& state) -{ - asio::io_context ioc; - int64_t counter = 0; - int constexpr batch_size = 1000; - - perf::stopwatch sw; - auto deadline = std::chrono::steady_clock::now() + - std::chrono::duration(state.duration()); - - while (std::chrono::steady_clock::now() < deadline) - { - for (int i = 0; i < batch_size; ++i) - { - timer_type t(ioc); - t.expires_after(std::chrono::hours(1)); - t.cancel(); - ++counter; - } - - ioc.poll(); - ioc.restart(); - } - - ioc.run(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -// Single coroutine firing a zero-delay timer in a tight loop. Measures the -// scheduler's timer completion path — Asio passes the nearest expiry as -// the epoll_wait timeout, avoiding a timerfd syscall per fire. -void -bench_fire_rate(bench::state& state) -{ - asio::io_context ioc; - std::atomic running{true}; - int64_t counter = 0; - - auto task = [&]() -> asio::awaitable { - timer_type t(ioc); - try - { - while (running.load(std::memory_order_relaxed)) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - ++counter; - } - } - catch (std::exception const&) - { - } - }; - - perf::stopwatch sw; - - asio::co_spawn(ioc, task(), asio::detached); - - std::thread timer([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - timer.join(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -void -bench_schedule_cancel_lockless(bench::state& state) -{ - asio::io_context ioc(BOOST_ASIO_CONCURRENCY_HINT_UNSAFE); - int64_t counter = 0; - int constexpr batch_size = 1000; - - perf::stopwatch sw; - auto deadline = std::chrono::steady_clock::now() + - std::chrono::duration(state.duration()); - - while (std::chrono::steady_clock::now() < deadline) - { - for (int i = 0; i < batch_size; ++i) - { - timer_type t(ioc); - t.expires_after(std::chrono::hours(1)); - t.cancel(); - ++counter; - } - - ioc.poll(); - ioc.restart(); - } - - ioc.run(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -void -bench_fire_rate_lockless(bench::state& state) -{ - asio::io_context ioc(BOOST_ASIO_CONCURRENCY_HINT_UNSAFE); - std::atomic running{true}; - int64_t counter = 0; - - auto task = [&]() -> asio::awaitable { - timer_type t(ioc); - try - { - while (running.load(std::memory_order_relaxed)) - { - t.expires_after(std::chrono::nanoseconds(0)); - co_await t.async_wait(asio::deferred); - ++counter; - } - } - catch (std::exception const&) - { - } - }; - - perf::stopwatch sw; - - asio::co_spawn(ioc, task(), asio::detached); - - std::thread timer([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - timer.join(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -// N timers with staggered intervals (100us–1000us) firing concurrently. -// Stresses the timer queue under contention and reveals wake accuracy -// degradation as the number of pending timers grows. -void -bench_concurrent_timers(bench::state& state) -{ - int num_timers = static_cast(state.range(0)); - state.counters["num_timers"] = num_timers; - - asio::io_context ioc; - std::atomic running{true}; - std::vector fire_counts(num_timers, 0); - - auto timer_task = [&](int idx, std::chrono::microseconds interval) - -> asio::awaitable { - timer_type t(ioc); - try - { - while (running.load(std::memory_order_relaxed)) - { - perf::stopwatch sw; - t.expires_after(interval); - co_await t.async_wait(asio::deferred); - state.latency().add(sw.elapsed_ns()); - ++fire_counts[idx]; - } - } - catch (std::exception const&) - { - } - }; - - perf::stopwatch total_sw; - - for (int i = 0; i < num_timers; ++i) - { - auto interval = std::chrono::microseconds( - 100 + (900 * i) / (num_timers > 1 ? num_timers - 1 : 1)); - asio::co_spawn(ioc, timer_task(i, interval), asio::detached); - } - - std::thread stopper([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - stopper.join(); - - state.set_elapsed(total_sw.elapsed_seconds()); - - int64_t total_fires = 0; - for (auto c : fire_counts) - total_fires += c; - state.add_items(total_fires); -} - -} // anonymous namespace - -bench::benchmark_suite -make_timer_suite() -{ - return bench::benchmark_suite("timer") - .add("schedule_cancel", bench_schedule_cancel) - .add("schedule_cancel_lockless", bench_schedule_cancel_lockless) - .add("fire_rate", bench_fire_rate) - .add("fire_rate_lockless", bench_fire_rate_lockless) - .add("concurrent", bench_concurrent_timers) - .args({10, 100, 1000}); -} - -} // namespace asio_bench diff --git a/perf/bench/corosio/benchmarks.hpp b/perf/bench/corosio/benchmarks.hpp index d6d6ce1c1..4128ee8ad 100644 --- a/perf/bench/corosio/benchmarks.hpp +++ b/perf/bench/corosio/benchmarks.hpp @@ -43,13 +43,6 @@ bench::benchmark_suite make_socket_latency_suite(); template bench::benchmark_suite make_http_server_suite(); -/** Create the timer benchmark suite. - - @tparam Backend A backend tag value (e.g., `epoll`). -*/ -template -bench::benchmark_suite make_timer_suite(); - /** Create the accept churn benchmark suite. @tparam Backend A backend tag value (e.g., `epoll`). diff --git a/perf/bench/corosio/fan_out_bench.cpp b/perf/bench/corosio/fan_out_bench.cpp index fc189909f..582ab7a71 100644 --- a/perf/bench/corosio/fan_out_bench.cpp +++ b/perf/bench/corosio/fan_out_bench.cpp @@ -14,8 +14,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -51,10 +51,30 @@ echo_server(corosio::native_tcp_socket& sock) } } +// Completion latch: N children arrive, the last one wakes the single +// waiting parent. The waker's wake() is safe from any thread; today +// every arrival happens on the one ioc.run() thread. Construct a +// fresh latch per lap so a wakeup can never leak across rounds. A +// future multi-threaded variant needs the parent to wait on a strand +// (async_waker requires serialized resumption). +struct fan_out_latch +{ + std::atomic remaining; + capy::async_waker done; + + explicit fan_out_latch(int n) : remaining(n) {} + + void arrive() + { + if (remaining.fetch_sub(1, std::memory_order_acq_rel) == 1) + done.wake(); + } +}; + template capy::task<> sub_request( - corosio::native_tcp_socket& client, std::atomic& remaining) + corosio::native_tcp_socket& client, fan_out_latch& latch) { char send_buf[64] = {}; char recv_buf[64]; @@ -63,7 +83,7 @@ sub_request( co_await capy::write(client, capy::const_buffer(send_buf, 64)); if (wec) { - remaining.fetch_sub(1, std::memory_order_release); + latch.arrive(); co_return; } @@ -71,7 +91,7 @@ sub_request( co_await capy::read(client, capy::mutable_buffer(recv_buf, 64)); (void)rec; (void)rn; - remaining.fetch_sub(1, std::memory_order_release); + latch.arrive(); } // Parent spawns N sub-requests, waits for all N to complete, then repeats @@ -80,7 +100,6 @@ void bench_fork_join(bench::state& state) { using socket_type = corosio::native_tcp_socket; - using timer_type = corosio::native_timer; int fan_out = static_cast(state.range(0)); state.counters["fan_out"] = fan_out; @@ -106,22 +125,17 @@ bench_fork_join(bench::state& state) capy::run_async(ioc.get_executor())(echo_server(servers[i])); auto parent = [&]() -> capy::task<> { - timer_type t(ioc); while (state.running()) { auto lp = state.lap(); - std::atomic remaining{fan_out}; + fan_out_latch latch{fan_out}; for (int i = 0; i < fan_out; ++i) capy::run_async(ioc.get_executor())( - sub_request(clients[i], remaining)); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + sub_request(clients[i], latch)); + + auto [ec] = co_await latch.done.wait(); + (void)ec; } for (auto& c : clients) @@ -152,7 +166,6 @@ void bench_nested(bench::state& state) { using socket_type = corosio::native_tcp_socket; - using timer_type = corosio::native_timer; int groups = static_cast(state.range(0)); int subs_per_group = 4; @@ -182,40 +195,30 @@ bench_nested(bench::state& state) capy::run_async(ioc.get_executor())(echo_server(servers[i])); auto group_task = [&](int base_idx, int n, - std::atomic& groups_remaining) -> capy::task<> { - std::atomic subs_remaining{n}; + fan_out_latch& groups_latch) -> capy::task<> { + fan_out_latch subs_latch{n}; for (int i = 0; i < n; ++i) capy::run_async(ioc.get_executor())( - sub_request(clients[base_idx + i], subs_remaining)); + sub_request(clients[base_idx + i], subs_latch)); - timer_type t(ioc); - while (subs_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + auto [ec] = co_await subs_latch.done.wait(); + (void)ec; - groups_remaining.fetch_sub(1, std::memory_order_release); + groups_latch.arrive(); }; auto parent = [&]() -> capy::task<> { - timer_type t(ioc); while (state.running()) { auto lp = state.lap(); - std::atomic groups_remaining{groups}; + fan_out_latch groups_latch{groups}; for (int g = 0; g < groups; ++g) capy::run_async(ioc.get_executor())(group_task( - g * subs_per_group, subs_per_group, groups_remaining)); - - while (groups_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + g * subs_per_group, subs_per_group, groups_latch)); + + auto [ec] = co_await groups_latch.done.wait(); + (void)ec; } for (auto& c : clients) @@ -246,7 +249,6 @@ void bench_concurrent_parents(bench::state& state) { using socket_type = corosio::native_tcp_socket; - using timer_type = corosio::native_timer; int num_parents = static_cast(state.range(0)); int fan_out = 16; @@ -279,23 +281,18 @@ bench_concurrent_parents(bench::state& state) auto parent_task = [&](int parent_idx) -> capy::task<> { int base = parent_idx * fan_out; - timer_type t(ioc); while (state.running()) { auto lp = state.lap(); - std::atomic remaining{fan_out}; + fan_out_latch latch{fan_out}; for (int i = 0; i < fan_out; ++i) capy::run_async(ioc.get_executor())( - sub_request(clients[base + i], remaining)); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + sub_request(clients[base + i], latch)); + + auto [ec] = co_await latch.done.wait(); + (void)ec; } if (parents_done.fetch_add(1, std::memory_order_acq_rel) == @@ -330,7 +327,6 @@ void bench_fork_join_lockless(bench::state& state) { using socket_type = corosio::native_tcp_socket; - using timer_type = corosio::native_timer; int fan_out = static_cast(state.range(0)); state.counters["fan_out"] = fan_out; @@ -358,22 +354,17 @@ bench_fork_join_lockless(bench::state& state) capy::run_async(ioc.get_executor())(echo_server(servers[i])); auto parent = [&]() -> capy::task<> { - timer_type t(ioc); while (state.running()) { auto lp = state.lap(); - std::atomic remaining{fan_out}; + fan_out_latch latch{fan_out}; for (int i = 0; i < fan_out; ++i) capy::run_async(ioc.get_executor())( - sub_request(clients[i], remaining)); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + sub_request(clients[i], latch)); + + auto [ec] = co_await latch.done.wait(); + (void)ec; } for (auto& c : clients) @@ -403,7 +394,6 @@ void bench_nested_lockless(bench::state& state) { using socket_type = corosio::native_tcp_socket; - using timer_type = corosio::native_timer; int groups = static_cast(state.range(0)); int subs_per_group = 4; @@ -435,40 +425,30 @@ bench_nested_lockless(bench::state& state) capy::run_async(ioc.get_executor())(echo_server(servers[i])); auto group_task = [&](int base_idx, int n, - std::atomic& groups_remaining) -> capy::task<> { - std::atomic subs_remaining{n}; + fan_out_latch& groups_latch) -> capy::task<> { + fan_out_latch subs_latch{n}; for (int i = 0; i < n; ++i) capy::run_async(ioc.get_executor())( - sub_request(clients[base_idx + i], subs_remaining)); + sub_request(clients[base_idx + i], subs_latch)); - timer_type t(ioc); - while (subs_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + auto [ec] = co_await subs_latch.done.wait(); + (void)ec; - groups_remaining.fetch_sub(1, std::memory_order_release); + groups_latch.arrive(); }; auto parent = [&]() -> capy::task<> { - timer_type t(ioc); while (state.running()) { auto lp = state.lap(); - std::atomic groups_remaining{groups}; + fan_out_latch groups_latch{groups}; for (int g = 0; g < groups; ++g) capy::run_async(ioc.get_executor())(group_task( - g * subs_per_group, subs_per_group, groups_remaining)); - - while (groups_remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + g * subs_per_group, subs_per_group, groups_latch)); + + auto [ec] = co_await groups_latch.done.wait(); + (void)ec; } for (auto& c : clients) @@ -498,7 +478,6 @@ void bench_concurrent_parents_lockless(bench::state& state) { using socket_type = corosio::native_tcp_socket; - using timer_type = corosio::native_timer; int num_parents = static_cast(state.range(0)); int fan_out = 16; @@ -533,23 +512,18 @@ bench_concurrent_parents_lockless(bench::state& state) auto parent_task = [&](int parent_idx) -> capy::task<> { int base = parent_idx * fan_out; - timer_type t(ioc); while (state.running()) { auto lp = state.lap(); - std::atomic remaining{fan_out}; + fan_out_latch latch{fan_out}; for (int i = 0; i < fan_out; ++i) capy::run_async(ioc.get_executor())( - sub_request(clients[base + i], remaining)); - - while (remaining.load(std::memory_order_acquire) > 0) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - (void)ec; - } + sub_request(clients[base + i], latch)); + + auto [ec] = co_await latch.done.wait(); + (void)ec; } if (parents_done.fetch_add(1, std::memory_order_acq_rel) == diff --git a/perf/bench/corosio/timer_bench.cpp b/perf/bench/corosio/timer_bench.cpp deleted file mode 100644 index 0d950cf36..000000000 --- a/perf/bench/corosio/timer_bench.cpp +++ /dev/null @@ -1,257 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include "benchmarks.hpp" - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "../../common/native_includes.hpp" - -namespace corosio = boost::corosio; -namespace capy = boost::capy; - -namespace corosio_bench { -namespace { - -// Tight create/schedule/cancel/destroy loop — dominated by timer service -// internals (mutex, heap insert/remove, timerfd_settime when earliest changes). -template -void -bench_schedule_cancel(bench::state& state) -{ - using timer_type = corosio::native_timer; - - corosio::native_io_context ioc; - int64_t counter = 0; - int constexpr batch_size = 1000; - - perf::stopwatch sw; - auto deadline = std::chrono::steady_clock::now() + - std::chrono::duration(state.duration()); - - while (std::chrono::steady_clock::now() < deadline) - { - for (int i = 0; i < batch_size; ++i) - { - timer_type t(ioc); - t.expires_after(std::chrono::hours(1)); - t.cancel(); - ++counter; - } - - ioc.poll(); - ioc.restart(); - } - - ioc.run(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -// Single coroutine firing a zero-delay timer in a tight loop. Measures the -// scheduler's timer completion path without contention. -template -void -bench_fire_rate(bench::state& state) -{ - using timer_type = corosio::native_timer; - - corosio::native_io_context ioc; - std::atomic running{true}; - int64_t counter = 0; - - auto task = [&]() -> capy::task<> { - timer_type t(ioc); - while (running.load(std::memory_order_relaxed)) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - if (ec) - co_return; - ++counter; - } - }; - - perf::stopwatch sw; - - capy::run_async(ioc.get_executor())(task()); - - std::thread timer([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - timer.join(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -template -void -bench_schedule_cancel_lockless(bench::state& state) -{ - using timer_type = corosio::native_timer; - - corosio::io_context_options opts; - opts.single_threaded = true; - corosio::native_io_context ioc(opts, 1); - int64_t counter = 0; - int constexpr batch_size = 1000; - - perf::stopwatch sw; - auto deadline = std::chrono::steady_clock::now() + - std::chrono::duration(state.duration()); - - while (std::chrono::steady_clock::now() < deadline) - { - for (int i = 0; i < batch_size; ++i) - { - timer_type t(ioc); - t.expires_after(std::chrono::hours(1)); - t.cancel(); - ++counter; - } - - ioc.poll(); - ioc.restart(); - } - - ioc.run(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -template -void -bench_fire_rate_lockless(bench::state& state) -{ - using timer_type = corosio::native_timer; - - corosio::io_context_options opts; - opts.single_threaded = true; - corosio::native_io_context ioc(opts, 1); - std::atomic running{true}; - int64_t counter = 0; - - auto task = [&]() -> capy::task<> { - timer_type t(ioc); - while (running.load(std::memory_order_relaxed)) - { - t.expires_after(std::chrono::nanoseconds(0)); - auto [ec] = co_await t.wait(); - if (ec) - co_return; - ++counter; - } - }; - - perf::stopwatch sw; - - capy::run_async(ioc.get_executor())(task()); - - std::thread timer([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - timer.join(); - - state.set_elapsed(sw.elapsed_seconds()); - state.add_items(counter); -} - -// N timers with staggered intervals (100us–1000us) firing concurrently. -// Stresses the timer heap under contention. -template -void -bench_concurrent_timers(bench::state& state) -{ - using timer_type = corosio::native_timer; - - int num_timers = static_cast(state.range(0)); - state.counters["num_timers"] = num_timers; - - corosio::native_io_context ioc; - std::atomic running{true}; - std::vector fire_counts(num_timers, 0); - - auto timer_task = [&](int idx, - std::chrono::microseconds interval) -> capy::task<> { - timer_type t(ioc); - while (running.load(std::memory_order_relaxed)) - { - perf::stopwatch sw; - t.expires_after(interval); - auto [ec] = co_await t.wait(); - if (ec) - co_return; - state.latency().add(sw.elapsed_ns()); - ++fire_counts[idx]; - } - }; - - perf::stopwatch total_sw; - - for (int i = 0; i < num_timers; ++i) - { - auto interval = std::chrono::microseconds( - 100 + (900 * i) / (num_timers > 1 ? num_timers - 1 : 1)); - capy::run_async(ioc.get_executor())(timer_task(i, interval)); - } - - std::thread stopper([&]() { - std::this_thread::sleep_for( - std::chrono::duration(state.duration())); - running.store(false, std::memory_order_relaxed); - }); - - ioc.run(); - stopper.join(); - - state.set_elapsed(total_sw.elapsed_seconds()); - - int64_t total_fires = 0; - for (auto c : fire_counts) - total_fires += c; - state.add_items(total_fires); -} - -} // anonymous namespace - -template -bench::benchmark_suite -make_timer_suite() -{ - return bench::benchmark_suite("timer") - .add("schedule_cancel", bench_schedule_cancel) - .add("schedule_cancel_lockless", bench_schedule_cancel_lockless) - .add("fire_rate", bench_fire_rate) - .add("fire_rate_lockless", bench_fire_rate_lockless) - .add("concurrent", bench_concurrent_timers) - .args({10, 100, 1000}); -} - -} // namespace corosio_bench - -COROSIO_SUITE_INSTANTIATE(corosio_bench::make_timer_suite) diff --git a/perf/bench/main.cpp b/perf/bench/main.cpp index 1f18ae199..f98f0802b 100644 --- a/perf/bench/main.cpp +++ b/perf/bench/main.cpp @@ -77,7 +77,6 @@ add_corosio_suites(bench::benchmark_runner& runner, BackendTag) runner.add_suite("corosio", corosio_bench::make_socket_throughput_suite()); runner.add_suite("corosio", corosio_bench::make_socket_latency_suite()); runner.add_suite("corosio", corosio_bench::make_http_server_suite()); - runner.add_suite("corosio", corosio_bench::make_timer_suite()); runner.add_suite("corosio", corosio_bench::make_accept_churn_suite()); runner.add_suite("corosio", corosio_bench::make_fan_out_suite()); #if BOOST_COROSIO_POSIX @@ -94,7 +93,6 @@ add_asio_suites(bench::benchmark_runner& runner) runner.add_suite("asio", asio_bench::make_socket_throughput_suite()); runner.add_suite("asio", asio_bench::make_socket_latency_suite()); runner.add_suite("asio", asio_bench::make_http_server_suite()); - runner.add_suite("asio", asio_bench::make_timer_suite()); runner.add_suite("asio", asio_bench::make_accept_churn_suite()); runner.add_suite("asio", asio_bench::make_fan_out_suite()); runner.add_suite("asio", asio_bench::make_local_socket_throughput_suite()); @@ -108,7 +106,6 @@ add_asio_callback_suites(bench::benchmark_runner& runner) runner.add_suite("asio_callback", asio_callback_bench::make_socket_throughput_suite()); runner.add_suite("asio_callback", asio_callback_bench::make_socket_latency_suite()); runner.add_suite("asio_callback", asio_callback_bench::make_http_server_suite()); - runner.add_suite("asio_callback", asio_callback_bench::make_timer_suite()); runner.add_suite("asio_callback", asio_callback_bench::make_accept_churn_suite()); runner.add_suite("asio_callback", asio_callback_bench::make_fan_out_suite()); runner.add_suite("asio_callback", asio_callback_bench::make_local_socket_throughput_suite()); diff --git a/perf/common/native_includes.hpp b/perf/common/native_includes.hpp index f3111a837..224d57cc7 100644 --- a/perf/common/native_includes.hpp +++ b/perf/common/native_includes.hpp @@ -12,7 +12,6 @@ #include #include -#include #include // Suite factory instantiation — returns benchmark_suite by value diff --git a/src/corosio/src/timer.cpp b/src/corosio/src/timer.cpp index b05b712af..f61cb2146 100644 --- a/src/corosio/src/timer.cpp +++ b/src/corosio/src/timer.cpp @@ -8,15 +8,15 @@ // Official repository: https://github.com/cppalliance/corosio // -#include +#include #include -namespace boost::corosio { +namespace boost::corosio::detail { timer::~timer() = default; timer::timer(capy::execution_context& ctx) - : io_timer(create_handle(ctx)) + : io_object(create_handle(ctx)) { } @@ -25,7 +25,7 @@ timer::timer(capy::execution_context& ctx, time_point t) : timer(ctx) expires_at(t); } -timer::timer(timer&& other) noexcept : io_timer(std::move(other)) {} +timer::timer(timer&& other) noexcept : io_object(std::move(other)) {} timer& timer::operator=(timer&& other) noexcept @@ -53,4 +53,59 @@ timer::do_update_expiry() return detail::timer_service_update_expiry(get()); } -} // namespace boost::corosio +// Not inline: wait_awaitable::await_suspend (defined in timer.hpp) calls +// this from translation units that may never include timer_service.hpp, +// so this must be the one strong definition the linker can always find +// wherever a detail::timer is used (every such user also needs timer's +// constructors, defined in this same translation unit). +std::coroutine_handle<> +timer::implementation::wait( + std::coroutine_handle<> h, + capy::executor_ref d, + std::stop_token token, + std::error_code* ec, + capy::continuation* cont) +{ + // Already-expired fast path — no waiter_node, no mutex. + // Post instead of dispatch so the coroutine yields to the + // scheduler, allowing other queued work to run. + if (heap_index_ == npos) + { + if (expiry_ == (time_point::min)() || expiry_ <= clock_type::now()) + { + if (ec) + *ec = {}; + d.post(*cont); + return std::noop_coroutine(); + } + } + + // Publication-last invariant: fully initialize the waiter, count + // its work, and arm cancellation BEFORE insert_waiter() publishes + // it into the heap/list where a concurrent run() thread can fire + // it. impl_ stays null until insert_waiter() sets it under the + // mutex, so a stop callback that fires early (cancel_waiter) sees a + // null impl_ and is a safe no-op. To avoid losing such an early + // cancel, insert_waiter() re-checks stop_requested() under the lock + // and completes as canceled if it fires in this window. + auto* w = svc_->create_waiter(); + w->impl_ = nullptr; + w->svc_ = svc_; + w->h_ = h; + w->cont_ = cont; + w->d_ = d; + w->token_ = std::move(token); + w->ec_out_ = ec; + + might_have_pending_waits_ = true; + svc_->get_scheduler().work_started(); + + if (w->token_.stop_possible()) + w->stop_cb_.emplace(w->token_, waiter_node::canceller{w}); + + svc_->insert_waiter(*this, w); + + return std::noop_coroutine(); +} + +} // namespace boost::corosio::detail diff --git a/test/unit/cancel.cpp b/test/unit/cancel.cpp deleted file mode 100644 index 66231ebbc..000000000 --- a/test/unit/cancel.cpp +++ /dev/null @@ -1,314 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include - -#include -#include - -#include "context.hpp" -#include "test_suite.hpp" - -namespace boost::corosio { - -template -struct cancel_test -{ - void testTimeoutFires() - { - io_context ioc(Backend); - timer inner_timer(ioc); - timer timeout_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), timeout_timer, - std::chrono::milliseconds(10)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testInnerCompletesFirst() - { - io_context ioc(Backend); - timer inner_timer(ioc); - timer timeout_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::milliseconds(10)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), timeout_timer, std::chrono::seconds(1)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - void testZeroTimeout() - { - io_context ioc(Backend); - timer inner_timer(ioc); - timer timeout_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), timeout_timer, - std::chrono::milliseconds(0)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testParentCancellation() - { - io_context ioc(Backend); - timer inner_timer(ioc); - timer timeout_timer(ioc); - timer delay(ioc); - - std::stop_source stop_src; - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), timeout_timer, std::chrono::seconds(60)); - result_ec = ec; - completed = true; - }; - - auto canceller = [&]() -> capy::task<> { - delay.expires_after(std::chrono::milliseconds(10)); - (void)co_await delay.wait(); - stop_src.request_stop(); - }; - - capy::run_async(ioc.get_executor(), stop_src.get_token())(task()); - capy::run_async(ioc.get_executor())(canceller()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testAlreadyExpiredDeadline() - { - io_context ioc(Backend); - timer inner_timer(ioc); - timer timeout_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_at( - inner_timer.wait(), timeout_timer, - timer::clock_type::now() - std::chrono::seconds(1)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testCancelAt() - { - io_context ioc(Backend); - timer inner_timer(ioc); - timer timeout_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - auto deadline = - timer::clock_type::now() + std::chrono::milliseconds(10); - - auto task = [&]() -> capy::task<> { - auto [ec] = - co_await cancel_at(inner_timer.wait(), timeout_timer, deadline); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testTimerReuse() - { - io_context ioc(Backend); - timer inner_timer(ioc); - timer timeout_timer(ioc); - - int completed = 0; - - auto task = [&]() -> capy::task<> { - // First: inner completes before timeout - inner_timer.expires_after(std::chrono::milliseconds(10)); - auto [ec1] = co_await cancel_after( - inner_timer.wait(), timeout_timer, std::chrono::seconds(1)); - BOOST_TEST(!ec1); - ++completed; - - // Second: timeout fires - inner_timer.expires_after(std::chrono::seconds(60)); - auto [ec2] = co_await cancel_after( - inner_timer.wait(), timeout_timer, - std::chrono::milliseconds(10)); - BOOST_TEST(ec2 == capy::cond::canceled); - ++completed; - - // Third: inner completes again - inner_timer.expires_after(std::chrono::milliseconds(10)); - auto [ec3] = co_await cancel_after( - inner_timer.wait(), timeout_timer, std::chrono::seconds(1)); - BOOST_TEST(!ec3); - ++completed; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST_EQ(completed, 3); - } - - // -- Convenience overloads (no user-supplied timer) -- - - void testConvenienceTimeoutFires() - { - io_context ioc(Backend); - timer inner_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), std::chrono::milliseconds(10)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testConvenienceInnerCompletesFirst() - { - io_context ioc(Backend); - timer inner_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::milliseconds(10)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), std::chrono::seconds(1)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - void testConvenienceCancelAt() - { - io_context ioc(Backend); - timer inner_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - auto deadline = - timer::clock_type::now() + std::chrono::milliseconds(10); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_at(inner_timer.wait(), deadline); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void run() - { - testTimeoutFires(); - testInnerCompletesFirst(); - testZeroTimeout(); - testParentCancellation(); - testAlreadyExpiredDeadline(); - testCancelAt(); - testTimerReuse(); - testConvenienceTimeoutFires(); - testConvenienceInnerCompletesFirst(); - testConvenienceCancelAt(); - } -}; - -COROSIO_BACKEND_TESTS(cancel_test, "boost.corosio.cancel") - -} // namespace boost::corosio diff --git a/test/unit/connect.cpp b/test/unit/connect.cpp index 6de45b45d..9bc43ff7e 100644 --- a/test/unit/connect.cpp +++ b/test/unit/connect.cpp @@ -10,11 +10,11 @@ // Test that header file is self-contained. #include +#include #include #include #include #include -#include #include #include @@ -264,9 +264,7 @@ struct connect_test // Must also cancel the acceptor since nothing will ever connect. auto cancel_task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); acc.cancel(); }; @@ -416,9 +414,7 @@ struct connect_test }; auto cancel_task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); client.cancel(); }; diff --git a/test/unit/datagram_paths.cpp b/test/unit/datagram_paths.cpp index 4214c6e9e..529a3706c 100644 --- a/test/unit/datagram_paths.cpp +++ b/test/unit/datagram_paths.cpp @@ -15,9 +15,9 @@ #include +#include #include #include -#include #include #include #include @@ -179,9 +179,7 @@ struct datagram_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -215,9 +213,7 @@ struct datagram_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -353,9 +349,7 @@ struct datagram_paths_test }; auto reader = [&]() -> capy::task<> { // Let the writer fill the kernel queue and park first. - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); char buf[2048]; while (!writer_done || received < sent) @@ -436,9 +430,7 @@ struct datagram_paths_test writer_done = true; }; auto reader = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); char buf[2048]; local_endpoint source; @@ -497,9 +489,7 @@ struct datagram_paths_test recv_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -538,9 +528,7 @@ struct datagram_paths_test recv_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -592,9 +580,7 @@ struct datagram_paths_test send_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -658,9 +644,7 @@ struct datagram_paths_test send_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -698,9 +682,7 @@ struct datagram_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -731,9 +713,7 @@ struct datagram_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; diff --git a/test/unit/delay.cpp b/test/unit/delay.cpp new file mode 100644 index 000000000..80e7ba8e5 --- /dev/null +++ b/test/unit/delay.cpp @@ -0,0 +1,415 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "context.hpp" +#include "test_suite.hpp" + +namespace boost::corosio { + +template +struct delay_test +{ + void testDurationCompletes() + { + io_context ioc(Backend); + bool ok = false; + + auto t = [](bool& ok_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::milliseconds(5)); + ok_out = !ec; + }; + capy::run_async(ioc.get_executor())(t(ok)); + + ioc.run(); + BOOST_TEST(ok); + } + + void testTimePointCompletes() + { + io_context ioc(Backend); + bool ok = false; + + auto t = [](bool& ok_out) -> capy::task<> { + auto tp = std::chrono::steady_clock::now() + + std::chrono::milliseconds(5); + auto [ec] = co_await delay(tp); + ok_out = !ec; + }; + capy::run_async(ioc.get_executor())(t(ok)); + + ioc.run(); + BOOST_TEST(ok); + } + + void testZeroDurationCompletesImmediately() + { + io_context ioc(Backend); + bool ok = false; + + auto t = [](bool& ok_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::milliseconds(0)); + ok_out = !ec; + }; + capy::run_async(ioc.get_executor())(t(ok)); + + ioc.run(); + BOOST_TEST(ok); + } + + void testPastTimePointCompletesImmediately() + { + io_context ioc(Backend); + bool ok = false; + + auto t = [](bool& ok_out) -> capy::task<> { + auto tp = std::chrono::steady_clock::now() - + std::chrono::seconds(1); + auto [ec] = co_await delay(tp); + ok_out = !ec; + }; + capy::run_async(ioc.get_executor())(t(ok)); + + ioc.run(); + BOOST_TEST(ok); + } + + void testCancellation() + { + io_context ioc(Backend); + std::stop_source src; + bool canceled = false; + + auto t = [](bool& canceled_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::seconds(10)); + canceled_out = (ec == capy::cond::canceled); + }; + capy::run_async(ioc.get_executor(), src.get_token())(t(canceled)); + + // Let the delay suspend, then cancel + ioc.run_one(); + src.request_stop(); + ioc.run(); + BOOST_TEST(canceled); + } + + void testAlreadyStoppedCompletesCanceled() + { + io_context ioc(Backend); + std::stop_source src; + src.request_stop(); + bool canceled = false; + + auto t = [](bool& canceled_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::seconds(10)); + canceled_out = (ec == capy::cond::canceled); + }; + capy::run_async(ioc.get_executor(), src.get_token())(t(canceled)); + + ioc.run(); + BOOST_TEST(canceled); + } + + void testZeroDurationWithStopRequested() + { + io_context ioc(Backend); + std::stop_source src; + src.request_stop(); + bool canceled = false; + + auto t = [](bool& canceled_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::milliseconds(0)); + canceled_out = (ec == capy::cond::canceled); + }; + capy::run_async(ioc.get_executor(), src.get_token())(t(canceled)); + + ioc.run(); + BOOST_TEST(canceled); + } + + void testPastTimePointWithStopRequested() + { + io_context ioc(Backend); + std::stop_source src; + src.request_stop(); + bool canceled = false; + + auto t = [](bool& canceled_out) -> capy::task<> { + auto tp = std::chrono::steady_clock::now() - + std::chrono::seconds(1); + auto [ec] = co_await delay(tp); + canceled_out = (ec == capy::cond::canceled); + }; + capy::run_async(ioc.get_executor(), src.get_token())(t(canceled)); + + ioc.run(); + BOOST_TEST(canceled); + } + + void testSingleThreadedHint() + { + // concurrency_hint == 1 enables single-threaded fast paths; + // delay must behave identically (spec sub-item d). + io_context ioc(Backend, 1u); + bool ok = false; + + auto t = [](bool& ok_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::milliseconds(5)); + ok_out = !ec; + }; + capy::run_async(ioc.get_executor())(t(ok)); + + ioc.run(); + BOOST_TEST(ok); + } + + void testSequentialDelays() + { + io_context ioc(Backend); + int count = 0; + + auto t = [](int& count_out) -> capy::task<> { + for(int i = 0; i < 3; ++i) + { + auto [ec] = co_await delay(std::chrono::milliseconds(1)); + if(!ec) + ++count_out; + } + }; + capy::run_async(ioc.get_executor())(t(count)); + + ioc.run(); + BOOST_TEST_EQ(count, 3); + } + + // Issue: an executor whose context is not an io_context cannot + // supply a timer service. await_suspend is normally reached only + // through a noexcept coroutine-resumption path, where the + // resulting std::logic_error would terminate rather than throw; + // calling it directly here (outside that path) exercises the + // same precondition check catchably. + void testNonIoContextThrows() + { + capy::thread_pool pool(1); + auto ex = pool.get_executor(); + delay_awaitable da(std::chrono::milliseconds(1)); + capy::io_env env{ex, {}, {}}; + BOOST_TEST_THROWS( + da.await_suspend(std::noop_coroutine(), &env), + std::logic_error); + } + + void testDelayActuallyWaits() + { + io_context ioc(Backend); + std::chrono::steady_clock::duration elapsed{}; + + auto t = [](std::chrono::steady_clock::duration& out) -> capy::task<> { + auto start = std::chrono::steady_clock::now(); + auto [ec] = co_await delay(std::chrono::milliseconds(50)); + out = std::chrono::steady_clock::now() - start; + (void)ec; + }; + capy::run_async(ioc.get_executor())(t(elapsed)); + + ioc.run(); + BOOST_TEST(elapsed >= std::chrono::milliseconds(50)); + } + + void testConcurrentDelaysHeapRemoval() + { + // Several concurrent delays at interleaved deadlines; the + // middle ones are canceled through their own stop tokens, + // exercising heap middle-removal. All must complete with the + // expected disposition. + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + std::stop_source s1, s2, s3, s4, s5; + int ok = 0; + int canceled = 0; + + // Deadlines interleaved (5,4,3,2,1 ms); cancel the middle + // three (3,4,5 ms) so removals land in the heap interior. + auto d = [](int ms, int& ok_out, int& cancel_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::milliseconds(ms)); + if (ec == capy::cond::canceled) + ++cancel_out; + else if (!ec) + ++ok_out; + }; + + capy::run_async(ex, s1.get_token())(d(1, ok, canceled)); + capy::run_async(ex, s2.get_token())(d(2, ok, canceled)); + capy::run_async(ex, s3.get_token())(d(3, ok, canceled)); + capy::run_async(ex, s4.get_token())(d(4, ok, canceled)); + capy::run_async(ex, s5.get_token())(d(5, ok, canceled)); + + // Let all five suspend into the heap, then cancel the middle + // three before any fires. + ioc.poll(); + s3.request_stop(); + s4.request_stop(); + s5.request_stop(); + + ioc.run(); + BOOST_TEST_EQ(canceled, 3); + BOOST_TEST_EQ(ok, 2); + } + + void testShutdownWithSuspendedDelay() + { + // Destroying the io_context while a delay is suspended must + // drain the heap waiter and destroy the coroutine frame + // cleanly (guard runs). + int destroyed = 0; + + { + io_context ioc(Backend); + + auto task = [](int& counter) -> capy::task<> { + struct guard + { + int& c_; + ~guard() { ++c_; } + }; + guard g{counter}; + auto [ec] = co_await delay(std::chrono::hours(1)); + (void)ec; + }; + + capy::run_async(ioc.get_executor())(task(destroyed)); + ioc.poll(); + // io_context destructs with the delay still suspended; + // timer_service::shutdown() drains the heap waiter. + } + + BOOST_TEST_EQ(destroyed, 1); + } + + void testShutdownDrainsHeapWaiters() + { + // Several delays at different deadlines populate the heap; all + // are suspended when the io_context is destroyed. + int destroyed = 0; + + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + auto task = [](int ms, int& counter) -> capy::task<> { + struct guard + { + int& c_; + ~guard() { ++c_; } + }; + guard g{counter}; + auto [ec] = co_await delay(std::chrono::hours(ms)); + (void)ec; + }; + + capy::run_async(ex)(task(1, destroyed)); + capy::run_async(ex)(task(2, destroyed)); + capy::run_async(ex)(task(3, destroyed)); + ioc.poll(); + } + + BOOST_TEST_EQ(destroyed, 3); + } + + void testInitiationStopRace() + { + // Stress the initiation-ordering race in wait(): foreign + // threads request stop on delay waiters while run() threads + // may fire them, hammering the publication window. Every + // co_await must complete (success or canceled) and the + // context must drain (no lost work, no hang). + constexpr int N = 200; + + for (int iter = 0; iter < 5; ++iter) + { + io_context ioc(Backend, 2u); // multi-threaded, not hint 1 + auto ex = ioc.get_executor(); + + std::atomic completed{0}; + std::vector srcs(N); + + auto task = + [](std::atomic& done, int ms) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::milliseconds(ms)); + (void)ec; // success or canceled — both acceptable + done.fetch_add(1, std::memory_order_relaxed); + }; + + for (int i = 0; i < N; ++i) + capy::run_async(ex, srcs[i].get_token())( + task(completed, i % 3)); // 0-2ms staggered + + std::thread stopper([&] { + for (int i = 0; i < N; ++i) + srcs[i].request_stop(); + }); + // External run() threads populate per-thread timer caches; + // clear them before exit (thread_local_ptr does not free on + // thread teardown) so no cached impl/waiter is leaked. + auto worker = [&] { + ioc.run(); + detail::timer_service_invalidate_cache(); + }; + std::thread r1(worker); + std::thread r2(worker); + + r1.join(); + r2.join(); + stopper.join(); + + BOOST_TEST_EQ(completed.load(), N); + } + } + + void run() + { + testDurationCompletes(); + testTimePointCompletes(); + testZeroDurationCompletesImmediately(); + testPastTimePointCompletesImmediately(); + testCancellation(); + testAlreadyStoppedCompletesCanceled(); + testZeroDurationWithStopRequested(); + testPastTimePointWithStopRequested(); + testSingleThreadedHint(); + testSequentialDelays(); + testNonIoContextThrows(); + testDelayActuallyWaits(); + testConcurrentDelaysHeapRemoval(); + testShutdownWithSuspendedDelay(); + testShutdownDrainsHeapWaiters(); + testInitiationStopRace(); + } +}; + +COROSIO_BACKEND_TESTS(delay_test, "boost.corosio.delay") + +} // namespace boost::corosio diff --git a/test/unit/local_datagram_socket.cpp b/test/unit/local_datagram_socket.cpp index 0a8cfa768..adf32874f 100644 --- a/test/unit/local_datagram_socket.cpp +++ b/test/unit/local_datagram_socket.cpp @@ -18,9 +18,9 @@ // Keep the entire suite POSIX-gated until Windows kernel support lands. #if BOOST_COROSIO_POSIX +#include #include #include -#include #include #include #include @@ -782,9 +782,7 @@ struct local_datagram_socket_test }(s2, recv_ec, recv_done)); auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); s2.cancel(); }; capy::run_async(ex)(canceller()); diff --git a/test/unit/local_stream_socket.cpp b/test/unit/local_stream_socket.cpp index 38893b5c7..9421fa403 100644 --- a/test/unit/local_stream_socket.cpp +++ b/test/unit/local_stream_socket.cpp @@ -10,10 +10,10 @@ // Test that header file is self-contained. #include +#include #include #include #include -#include #include #include #include @@ -563,9 +563,7 @@ struct local_stream_socket_test // Schedule a cancel after a brief delay auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); acc.cancel(); }; capy::run_async(ex)(canceller()); @@ -603,9 +601,7 @@ struct local_stream_socket_test accept_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; diff --git a/test/unit/native/native_cancel.cpp b/test/unit/native/native_cancel.cpp deleted file mode 100644 index 98f7865de..000000000 --- a/test/unit/native/native_cancel.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include - -#include - -#include "context.hpp" -#include "test_suite.hpp" - -namespace boost::corosio { - -template -struct native_cancel_test -{ - void testConvenienceTimeoutFires() - { - io_context ioc(Backend); - timer inner_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), std::chrono::milliseconds(10)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testConvenienceInnerCompletesFirst() - { - io_context ioc(Backend); - timer inner_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::milliseconds(10)); - - auto task = [&]() -> capy::task<> { - auto [ec] = co_await cancel_after( - inner_timer.wait(), std::chrono::seconds(1)); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - void testConvenienceCancelAt() - { - io_context ioc(Backend); - timer inner_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - inner_timer.expires_after(std::chrono::seconds(60)); - auto deadline = - timer::clock_type::now() + std::chrono::milliseconds(10); - - auto task = [&]() -> capy::task<> { - auto [ec] = - co_await cancel_at(inner_timer.wait(), deadline); - result_ec = ec; - completed = true; - }; - capy::run_async(ioc.get_executor())(task()); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void run() - { - testConvenienceTimeoutFires(); - testConvenienceInnerCompletesFirst(); - testConvenienceCancelAt(); - } -}; - -COROSIO_BACKEND_TESTS(native_cancel_test, "boost.corosio.native_cancel") - -} // namespace boost::corosio diff --git a/test/unit/native/native_timer.cpp b/test/unit/native/native_timer.cpp deleted file mode 100644 index 64ba34b01..000000000 --- a/test/unit/native/native_timer.cpp +++ /dev/null @@ -1,130 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include -#include - -#include -#include - -#include -#include -#include - -#include "context.hpp" -#include "test_suite.hpp" - -namespace boost::corosio { - -template -struct native_timer_test -{ - static_assert( - !std::is_same_v< - decltype(std::declval&>().wait()), - decltype(std::declval().wait())>, - "native_timer::wait must shadow io_timer::wait"); - - void testTimerConstruct() - { - io_context ctx(Backend); - native_timer t(ctx); - BOOST_TEST_PASS(); - } - - void testTimerConstructDuration() - { - io_context ctx(Backend); - native_timer t(ctx, std::chrono::milliseconds(100)); - BOOST_TEST(t.expiry() > timer::time_point{}); - } - - // Issue #231: native_timer mirrors timer's executor constructors. - void testTimerConstructFromExecutor() - { - io_context ctx(Backend); - native_timer t(ctx.get_executor()); - BOOST_TEST_PASS(); - - native_timer t2( - ctx.get_executor(), std::chrono::milliseconds(100)); - BOOST_TEST(t2.expiry() > timer::time_point{}); - } - - void testTimerWait() - { - io_context ctx(Backend); - native_timer t(ctx); - t.expires_after(std::chrono::milliseconds(10)); - - bool done = false; - std::error_code result_ec; - - auto task = [](native_timer& t_ref, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ctx.get_executor())(task(t, result_ec, done)); - - ctx.run(); - BOOST_TEST(done); - BOOST_TEST(!result_ec); - } - - void testTimerWaitExpired() - { - io_context ctx(Backend); - native_timer t(ctx); - t.expires_at(timer::clock_type::now() - std::chrono::seconds(1)); - - bool done = false; - std::error_code result_ec; - - auto task = [](native_timer& t_ref, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ctx.get_executor())(task(t, result_ec, done)); - - ctx.run(); - BOOST_TEST(done); - BOOST_TEST(!result_ec); - } - - void testTimerPolymorphicSlice() - { - io_context ctx(Backend); - native_timer nt(ctx); - nt.expires_after(std::chrono::milliseconds(100)); - - timer& base = nt; - BOOST_TEST(base.expiry() == nt.expiry()); - - io_timer& io_base = nt; - BOOST_TEST(io_base.expiry() == nt.expiry()); - } - - void run() - { - testTimerConstruct(); - testTimerConstructDuration(); - testTimerConstructFromExecutor(); - testTimerWait(); - testTimerWaitExpired(); - testTimerPolymorphicSlice(); - } -}; - -COROSIO_BACKEND_TESTS(native_timer_test, "boost.corosio.native.timer") - -} // namespace boost::corosio diff --git a/test/unit/native/native_udp_socket.cpp b/test/unit/native/native_udp_socket.cpp index e048ff3e8..090b75b0f 100644 --- a/test/unit/native/native_udp_socket.cpp +++ b/test/unit/native/native_udp_socket.cpp @@ -8,10 +8,10 @@ // #include +#include #include #include #include -#include #include #include @@ -155,9 +155,6 @@ struct native_udp_socket_test BOOST_TEST_EQ(ec, std::error_code{}); auto task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - bool recv_done = false; std::error_code recv_ec; @@ -171,12 +168,11 @@ struct native_udp_socket_test }; capy::run_async(ioc.get_executor())(nested()); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); sock.cancel(); - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + // Let the cancellation settle before checking the result. + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(recv_done); BOOST_TEST(recv_ec == capy::cond::canceled); @@ -197,9 +193,6 @@ struct native_udp_socket_test BOOST_TEST_EQ(ec, std::error_code{}); auto task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - bool recv_done = false; std::error_code recv_ec; @@ -213,12 +206,11 @@ struct native_udp_socket_test }; capy::run_async(ioc.get_executor())(nested()); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); sock.close(); - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + // Let the close settle before checking the result. + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(recv_done); BOOST_TEST(recv_ec == capy::cond::canceled); diff --git a/test/unit/reactor_paths.cpp b/test/unit/reactor_paths.cpp index efb0d408a..5dd504d3c 100644 --- a/test/unit/reactor_paths.cpp +++ b/test/unit/reactor_paths.cpp @@ -15,12 +15,12 @@ #include +#include #include #include #include #include #include -#include #include #include @@ -91,9 +91,7 @@ struct reactor_paths_test }; auto peer_writer = [&]() -> capy::task<> { // Brief delay so the read side parks first. - timer t(ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(10)); auto [ec, n] = co_await s2.write_some( capy::const_buffer(payload.data(), payload.size())); (void)ec; @@ -141,9 +139,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(500)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(500)); sock.cancel(); }; @@ -186,7 +182,7 @@ struct reactor_paths_test } // wait_type::error should complete when peer closes (reactor delivers - // HUP via the err/ready_events path). Bounded by a cancel timer because + // HUP via the err/ready_events path). Bounded by a cancel delay because // not every backend reports HUP as an error condition. void testWaitForError() { @@ -204,15 +200,11 @@ struct reactor_paths_test wait_done = true; }; auto closer = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); s2.close(); // Bound the wait: cancel s1 after another delay if the peer // close did not surface as an error condition. - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(200)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(200)); s1.cancel(); }; @@ -243,9 +235,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); s1.cancel(); }; @@ -285,9 +275,7 @@ struct reactor_paths_test read_n = n; }; auto writer = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(10)); auto [ec, n] = co_await s2.write_some( capy::const_buffer(payload.data(), payload.size())); (void)ec; @@ -479,9 +467,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); acc.cancel(); }; @@ -541,9 +527,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); sock.cancel(); }; @@ -776,9 +760,7 @@ struct reactor_paths_test wait_done = true; }; auto closer = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); sock.close(); }; @@ -870,9 +852,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -902,9 +882,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -960,9 +938,7 @@ struct reactor_paths_test recv_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -999,9 +975,7 @@ struct reactor_paths_test recv_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -1036,9 +1010,7 @@ struct reactor_paths_test accept_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; @@ -1187,9 +1159,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); s1.cancel(); }; @@ -1254,9 +1224,7 @@ struct reactor_paths_test read_n = n; }; auto writer = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(10)); auto [ec, n] = co_await s2.write_some( capy::const_buffer(payload.data(), payload.size())); (void)ec; @@ -1331,9 +1299,7 @@ struct reactor_paths_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); s1.cancel(); }; @@ -1482,9 +1448,7 @@ struct reactor_paths_test read_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; diff --git a/test/unit/resolver.cpp b/test/unit/resolver.cpp index f0cbb7a59..39fac0baf 100644 --- a/test/unit/resolver.cpp +++ b/test/unit/resolver.cpp @@ -17,7 +17,6 @@ #endif #include -#include #include #include #include @@ -465,7 +464,6 @@ struct resolver_test { io_context ioc; resolver r(ioc); - timer cancel_timer(ioc); bool completed = false; std::error_code result_ec; diff --git a/test/unit/signal_set.cpp b/test/unit/signal_set.cpp index 194d8f30a..0941117bb 100644 --- a/test/unit/signal_set.cpp +++ b/test/unit/signal_set.cpp @@ -12,7 +12,7 @@ // Test that header file is self-contained. #include -#include +#include #include #include @@ -208,7 +208,6 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT); - timer t(ioc); bool completed = false; int received_signal = 0; @@ -225,12 +224,11 @@ struct signal_set_test wait_task(s, result_ec, received_signal, completed)); // Raise signal after a short delay - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> { - (void)co_await t_ref.wait(); + auto raise_task = []() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); std::raise(SIGINT); }; - capy::run_async(ioc.get_executor())(raise_task(t)); + capy::run_async(ioc.get_executor())(raise_task()); ioc.run(); BOOST_TEST(completed); @@ -242,7 +240,6 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGTERM); - timer t(ioc); bool completed = false; int received_signal = 0; @@ -257,12 +254,11 @@ struct signal_set_test capy::run_async(ioc.get_executor())( wait_task(s, received_signal, completed)); - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> { - (void)co_await t_ref.wait(); + auto raise_task = []() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); std::raise(SIGTERM); }; - capy::run_async(ioc.get_executor())(raise_task(t)); + capy::run_async(ioc.get_executor())(raise_task()); ioc.run(); BOOST_TEST(completed); @@ -275,7 +271,6 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT); - timer cancel_timer(ioc); bool completed = false; std::error_code result_ec; @@ -289,12 +284,11 @@ struct signal_set_test }; capy::run_async(ioc.get_executor())(wait_task(s, result_ec, completed)); - cancel_timer.expires_after(std::chrono::milliseconds(10)); - auto cancel_task = [](timer& t_ref, signal_set& s_ref) -> capy::task<> { - (void)co_await t_ref.wait(); + auto cancel_task = [](signal_set& s_ref) -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); s_ref.cancel(); }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, s)); + capy::run_async(ioc.get_executor())(cancel_task(s)); ioc.run(); BOOST_TEST(completed); @@ -399,7 +393,6 @@ struct signal_set_test io_context ioc(Backend); signal_set s1(ioc, SIGINT); signal_set s2(ioc, SIGINT); - timer t(ioc); bool s1_completed = false; bool s2_completed = false; @@ -418,12 +411,11 @@ struct signal_set_test capy::run_async(ioc.get_executor())( wait_task(s2, s2_signal, s2_completed)); - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> { - (void)co_await t_ref.wait(); + auto raise_task = []() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); std::raise(SIGINT); }; - capy::run_async(ioc.get_executor())(raise_task(t)); + capy::run_async(ioc.get_executor())(raise_task()); ioc.run(); BOOST_TEST(s1_completed); @@ -436,7 +428,6 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT, SIGTERM); - timer t(ioc); bool completed = false; int received_signal = 0; @@ -452,12 +443,11 @@ struct signal_set_test wait_task(s, received_signal, completed)); // Raise SIGTERM (not SIGINT) - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> { - (void)co_await t_ref.wait(); + auto raise_task = []() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); std::raise(SIGTERM); }; - capy::run_async(ioc.get_executor())(raise_task(t)); + capy::run_async(ioc.get_executor())(raise_task()); ioc.run(); BOOST_TEST(completed); @@ -498,15 +488,12 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT); - timer t(ioc); int wait_count = 0; - auto task = [](signal_set& s_ref, timer& t_ref, - int& count_out) -> capy::task<> { + auto task = [](signal_set& s_ref, int& count_out) -> capy::task<> { // First wait - t_ref.expires_after(std::chrono::milliseconds(5)); - (void)co_await t_ref.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(5)); std::raise(SIGINT); auto [ec1, sig1] = co_await s_ref.wait(); @@ -515,8 +502,7 @@ struct signal_set_test ++count_out; // Second wait - t_ref.expires_after(std::chrono::milliseconds(5)); - (void)co_await t_ref.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(5)); std::raise(SIGINT); auto [ec2, sig2] = co_await s_ref.wait(); @@ -524,7 +510,7 @@ struct signal_set_test BOOST_TEST_EQ(sig2, SIGINT); ++count_out; }; - capy::run_async(ioc.get_executor())(task(s, t, wait_count)); + capy::run_async(ioc.get_executor())(task(s, wait_count)); ioc.run(); BOOST_TEST_EQ(wait_count, 2); @@ -569,12 +555,10 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT); - timer t(ioc); int delivered = 0; - auto task = [](signal_set& s_ref, timer& t_ref, - int& count_out) -> capy::task<> { + auto task = [](signal_set& s_ref, int& count_out) -> capy::task<> { for (int i = 0; i < 20; ++i) { // Raise before waiting: exercises the queued (undelivered) @@ -587,8 +571,7 @@ struct signal_set_test // Raise after a delay while waiting: exercises the live-waiter // path where the drain posts a completion. - t_ref.expires_after(std::chrono::milliseconds(1)); - (void)co_await t_ref.wait(); + (void)co_await delay(std::chrono::milliseconds(1)); std::raise(SIGINT); auto [ec2, sig2] = co_await s_ref.wait(); BOOST_TEST(!ec2); @@ -596,7 +579,7 @@ struct signal_set_test ++count_out; } }; - capy::run_async(ioc.get_executor())(task(s, t, delivered)); + capy::run_async(ioc.get_executor())(task(s, delivered)); ioc.run(); BOOST_TEST_EQ(delivered, 40); @@ -608,20 +591,17 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT); - timer t(ioc); bool result_ok = false; - auto task = [](signal_set& s_ref, timer& t_ref, - bool& ok_out) -> capy::task<> { - t_ref.expires_after(std::chrono::milliseconds(5)); - (void)co_await t_ref.wait(); + auto task = [](signal_set& s_ref, bool& ok_out) -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(5)); std::raise(SIGINT); auto result = co_await s_ref.wait(); ok_out = !result.ec; }; - capy::run_async(ioc.get_executor())(task(s, t, result_ok)); + capy::run_async(ioc.get_executor())(task(s, result_ok)); ioc.run(); BOOST_TEST(result_ok); @@ -631,7 +611,6 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT); - timer cancel_timer(ioc); bool result_ok = true; std::error_code result_ec; @@ -644,12 +623,11 @@ struct signal_set_test }; capy::run_async(ioc.get_executor())(wait_task(s, result_ok, result_ec)); - cancel_timer.expires_after(std::chrono::milliseconds(10)); - auto cancel_task = [](timer& t_ref, signal_set& s_ref) -> capy::task<> { - (void)co_await t_ref.wait(); + auto cancel_task = [](signal_set& s_ref) -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); s_ref.cancel(); }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, s)); + capy::run_async(ioc.get_executor())(cancel_task(s)); ioc.run(); BOOST_TEST(!result_ok); @@ -660,15 +638,13 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc, SIGINT); - timer t(ioc); std::error_code captured_ec; int captured_signal = 0; - auto task = [](signal_set& s_ref, timer& t_ref, std::error_code& ec_out, + auto task = [](signal_set& s_ref, std::error_code& ec_out, int& sig_out) -> capy::task<> { - t_ref.expires_after(std::chrono::milliseconds(5)); - (void)co_await t_ref.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(5)); std::raise(SIGINT); auto [ec, signum] = co_await s_ref.wait(); @@ -676,7 +652,7 @@ struct signal_set_test sig_out = signum; }; capy::run_async(ioc.get_executor())( - task(s, t, captured_ec, captured_signal)); + task(s, captured_ec, captured_signal)); ioc.run(); BOOST_TEST(!captured_ec); @@ -831,7 +807,6 @@ struct signal_set_test { io_context ioc(Backend); signal_set s(ioc); - timer t(ioc); // Add signal with restart flag and verify wait still works BOOST_TEST(!s.add(SIGINT, signal_set::restart)); @@ -849,12 +824,11 @@ struct signal_set_test capy::run_async(ioc.get_executor())( wait_task(s, received_signal, completed)); - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> { - (void)co_await t_ref.wait(); + auto raise_task = []() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); std::raise(SIGINT); }; - capy::run_async(ioc.get_executor())(raise_task(t)); + capy::run_async(ioc.get_executor())(raise_task()); ioc.run(); BOOST_TEST(completed); diff --git a/test/unit/socket_stress.cpp b/test/unit/socket_stress.cpp index 94bf31f2d..2fa7a8597 100644 --- a/test/unit/socket_stress.cpp +++ b/test/unit/socket_stress.cpp @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include @@ -178,9 +178,8 @@ struct stop_token_stress_test else if (i % 3 == 1) { // Brief delay then cancel - timer delay(ioc); - delay.expires_after(std::chrono::microseconds(1)); - (void)co_await delay.wait(); + (void)co_await corosio::delay( + std::chrono::microseconds(1)); stop_src.request_stop(); } else @@ -198,9 +197,8 @@ struct stop_token_stress_test { if (read_done.load(std::memory_order_acquire)) break; - timer t(ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + (void)co_await corosio::delay( + std::chrono::milliseconds(10)); } if (!read_done.load(std::memory_order_acquire)) @@ -213,9 +211,8 @@ struct stop_token_stress_test BOOST_TEST( read_done.load(std::memory_order_acquire)); stop_src.request_stop(); - timer t(ioc); - t.expires_after(std::chrono::milliseconds(100)); - (void)co_await t.wait(); + (void)co_await corosio::delay( + std::chrono::milliseconds(100)); } ++iterations; @@ -236,9 +233,7 @@ struct stop_token_stress_test // Timer to stop the test auto stopper = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::seconds(duration)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::seconds(duration)); stop_flag.store(true, std::memory_order_relaxed); }; @@ -328,9 +323,7 @@ struct sync_completion_stress_test // Timer to stop the test auto stopper = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::seconds(duration)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::seconds(duration)); stop_flag.store(true, std::memory_order_relaxed); }; @@ -411,9 +404,8 @@ struct cancel_close_stress_test case 0: { // Yield to let the posted read_coro start - timer yield_t(ioc); - yield_t.expires_after(std::chrono::microseconds(1)); - (void)co_await yield_t.wait(); + (void)co_await corosio::delay( + std::chrono::microseconds(1)); // Cancel via tcp_socket.cancel() s2.cancel(); ++cancels; @@ -447,9 +439,8 @@ struct cancel_close_stress_test { if (read_done.load(std::memory_order_acquire)) break; - timer t(ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + (void)co_await corosio::delay( + std::chrono::milliseconds(10)); } if (!read_done.load(std::memory_order_acquire)) @@ -463,9 +454,8 @@ struct cancel_close_stress_test read_done.load(std::memory_order_acquire)); // Force cancel s2.cancel(); - timer t(ioc); - t.expires_after(std::chrono::milliseconds(100)); - (void)co_await t.wait(); + (void)co_await corosio::delay( + std::chrono::milliseconds(100)); } ++iterations; @@ -482,9 +472,7 @@ struct cancel_close_stress_test // Timer to stop the test auto stopper = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::seconds(duration)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::seconds(duration)); stop_flag.store(true, std::memory_order_relaxed); }; @@ -588,9 +576,7 @@ struct concurrent_ops_stress_test // Timer to stop the test auto stopper = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::seconds(duration)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::seconds(duration)); stop_flag.store(true, std::memory_order_relaxed); // Close all sockets to unblock pending operations @@ -680,9 +666,7 @@ struct accept_stress_test client.close(); // Small delay to avoid overwhelming the accept queue - timer delay(ioc); - delay.expires_after(std::chrono::microseconds(100)); - (void)co_await delay.wait(); + (void)co_await corosio::delay(std::chrono::microseconds(100)); } }; @@ -691,9 +675,7 @@ struct accept_stress_test // Timer to stop the test auto stopper = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::seconds(duration)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::seconds(duration)); stop_flag.store(true, std::memory_order_relaxed); acc.close(); }; diff --git a/test/unit/tcp_acceptor.cpp b/test/unit/tcp_acceptor.cpp index a0a95d6c7..60b10f7d8 100644 --- a/test/unit/tcp_acceptor.cpp +++ b/test/unit/tcp_acceptor.cpp @@ -13,7 +13,7 @@ #include #include -#include +#include #include #include @@ -138,10 +138,6 @@ struct tcp_acceptor_test tcp_socket peer(ioc); auto task = [&]() -> capy::task<> { - // Start a timer to cancel the accept - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - // Launch accept that will block (no incoming connections) // Store lambda in variable to ensure it outlives the coroutine. auto nested_coro = [&acc, &peer, &accept_done, @@ -152,14 +148,12 @@ struct tcp_acceptor_test }; capy::run_async(ioc.get_executor())(nested_coro()); - // Wait for timer then cancel - (void)co_await t.wait(); + // Wait then cancel + (void)co_await corosio::delay(std::chrono::milliseconds(50)); acc.cancel(); // Wait for accept to complete - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(accept_done); BOOST_TEST(accept_ec == capy::cond::canceled); @@ -194,9 +188,6 @@ struct tcp_acceptor_test // the nested coroutine and close operation auto task = [&ioc, &acc, &peer, &accept_done, &accept_ec]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - // Store lambda in variable to ensure it outlives the coroutine. // Lambda coroutines capture 'this' by reference, so the lambda // must remain alive while the coroutine is suspended. @@ -209,12 +200,10 @@ struct tcp_acceptor_test capy::run_async(ioc.get_executor())(nested_coro()); // Wait then close the acceptor - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); acc.close(); - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(accept_done); BOOST_TEST(accept_ec == capy::cond::canceled); @@ -444,9 +433,7 @@ struct tcp_acceptor_test // Cancel lingering accept after connect completes auto cancel_task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(200)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(200)); acc.cancel(); }; capy::run_async(ex)(cancel_task()); @@ -771,9 +758,7 @@ struct tcp_acceptor_test accept_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); ss.request_stop(); }; diff --git a/test/unit/tcp_server.cpp b/test/unit/tcp_server.cpp index 84300644c..dfa4e8f25 100644 --- a/test/unit/tcp_server.cpp +++ b/test/unit/tcp_server.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include #include @@ -90,18 +90,16 @@ struct tcp_server_test srv.start(); // Client task: request stop after brief delay - auto client_task = [](io_context* ioc, test_server* srv, + auto client_task = [](test_server* srv, std::atomic* client_done) -> capy::task<> { // Brief delay to ensure server accept loop is running - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(10)); // Request stop - server should exit accept loop srv->stop(); client_done->store(true); - }(&ioc, &srv, &client_done); + }(&srv, &client_done); capy::run_async(ioc.get_executor())(std::move(client_task)); @@ -180,12 +178,10 @@ struct tcp_server_test srv.start(); srv.start(); // Second call should be no-op - auto task = [](io_context* ioc, test_server* srv) -> capy::task<> { - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + auto task = [](test_server* srv) -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); srv->stop(); - }(&ioc, &srv); + }(&srv); capy::run_async(ioc.get_executor())(std::move(task)); ioc.run(); @@ -201,15 +197,13 @@ struct tcp_server_test srv.start(); - auto task = [](io_context* ioc, test_server* srv) -> capy::task<> { - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(10)); - (void)co_await t.wait(); + auto task = [](test_server* srv) -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(10)); // Calling stop() twice should be safe srv->stop(); srv->stop(); // Second call should be no-op - }(&ioc, &srv); + }(&srv); capy::run_async(ioc.get_executor())(std::move(task)); ioc.run(); @@ -266,13 +260,10 @@ struct tcp_server_test client.close(); }(&ioc, port, &connections_handled); - auto stop_task1 = [](io_context* ioc, - test_server* srv) -> capy::task<> { - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + auto stop_task1 = [](test_server* srv) -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(50)); srv->stop(); - }(&ioc, &srv); + }(&srv); capy::run_async(ioc.get_executor())(std::move(task1)); capy::run_async(ioc.get_executor())(std::move(stop_task1)); @@ -307,13 +298,10 @@ struct tcp_server_test client.close(); }(&ioc, port, &connections_handled); - auto stop_task2 = [](io_context* ioc, - test_server* srv) -> capy::task<> { - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + auto stop_task2 = [](test_server* srv) -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(50)); srv->stop(); - }(&ioc, &srv); + }(&srv); capy::run_async(ioc.get_executor())(std::move(task2)); capy::run_async(ioc.get_executor())(std::move(stop_task2)); @@ -550,9 +538,7 @@ struct tcp_server_test endpoint(ipv4_address::loopback(), port)); (void)cec; client.close(); - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); } srv->stop(); }(&ioc, port, &srv); @@ -642,21 +628,16 @@ struct tcp_server_test if (!e3) connected->fetch_add(1); // Give the server time to register the connections. - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); // Disconnect middle first, then tail, then head: // exercises remove from each list position. c2.close(); - timer t1(*ioc); t1.expires_after(std::chrono::milliseconds(20)); - (void)co_await t1.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); c3.close(); - timer t2(*ioc); t2.expires_after(std::chrono::milliseconds(20)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); c1.close(); - timer t3(*ioc); t3.expires_after(std::chrono::milliseconds(20)); - (void)co_await t3.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(20)); srv->stop(); }(&ioc, port, &connected, &srv); @@ -745,9 +726,7 @@ struct tcp_server_test client.close(); // Give server time to handle the connection, then stop. - timer t(*ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); srv->stop(); }(&ioc, port, &srv); diff --git a/test/unit/tcp_socket.cpp b/test/unit/tcp_socket.cpp index b5303d14c..63cb36f52 100644 --- a/test/unit/tcp_socket.cpp +++ b/test/unit/tcp_socket.cpp @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include #include @@ -624,9 +624,7 @@ struct tcp_socket_test b.close(); // Give OS time to process the close - timer t(a.context()); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); // Writing to closed peer should eventually fail. // We need to write enough data to fill the tcp_socket buffer and @@ -659,11 +657,7 @@ struct tcp_socket_test auto [s1, s2] = test::make_socket_pair(ioc); - auto task = [&](tcp_socket& a, tcp_socket& b) -> capy::task<> { - // Start a timer to cancel the read - timer t(a.context()); - t.expires_after(std::chrono::milliseconds(50)); - + auto task = [&](tcp_socket&, tcp_socket& b) -> capy::task<> { // Launch read that will block (no data available) bool read_done = false; std::error_code read_ec; @@ -680,14 +674,12 @@ struct tcp_socket_test }; capy::run_async(ioc.get_executor())(nested_coro()); - // Wait for timer then cancel - (void)co_await t.wait(); + // Wait for the read to be underway then cancel it + (void)co_await corosio::delay(std::chrono::milliseconds(50)); b.cancel(); // Wait for read to complete - timer t2(a.context()); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(read_done); BOOST_TEST(read_ec == capy::cond::canceled); @@ -705,10 +697,7 @@ struct tcp_socket_test auto [s1, s2] = test::make_socket_pair(ioc); - auto task = [&](tcp_socket& a, tcp_socket& b) -> capy::task<> { - timer t(a.context()); - t.expires_after(std::chrono::milliseconds(50)); - + auto task = [&](tcp_socket&, tcp_socket& b) -> capy::task<> { bool read_done = false; std::error_code read_ec; @@ -725,12 +714,10 @@ struct tcp_socket_test capy::run_async(ioc.get_executor())(nested_coro()); // Wait then close the tcp_socket - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); b.close(); - timer t2(a.context()); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(read_done); // Close should cancel pending operations @@ -783,9 +770,8 @@ struct tcp_socket_test // Failsafe task - detects if stop_token cancellation didn't work auto failsafe_task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(1000)); - auto [ec] = co_await t.wait(); + auto [ec] = + co_await corosio::delay(std::chrono::milliseconds(1000)); // Only trigger failsafe if reader hasn't completed yet. // If read_done is true, stop_token cancellation worked. if (!ec && !read_done) diff --git a/test/unit/test_utils.hpp b/test/unit/test_utils.hpp index dbc06ed80..1d2fa5297 100644 --- a/test/unit/test_utils.hpp +++ b/test/unit/test_utils.hpp @@ -11,9 +11,9 @@ #ifndef BOOST_COROSIO_TEST_TLS_TEST_UTILS_HPP #define BOOST_COROSIO_TEST_TLS_TEST_UTILS_HPP +#include #include #include -#include #include #include #include @@ -884,9 +884,9 @@ run_tls_test_no_shutdown( /** Run a TLS test expecting handshake failure. - Uses a timer to handle the case where one side fails and the other - blocks waiting for data. When the timer fires, sockets are closed - to unblock any pending operations. + Uses a failsafe deadline to handle the case where one side fails and + the other blocks waiting for data. When the deadline elapses, sockets + are closed to unblock any pending operations. @param ioc The io_context to use @param client_ctx TLS context for the client @@ -914,14 +914,15 @@ run_tls_test_fail( bool client_done = false; bool server_done = false; - // Timer to unblock stuck handshakes (failsafe only) - timer timeout(ioc); - timeout.expires_after(std::chrono::milliseconds(200 * failsafe_scale)); + // Failsafe deadline to unblock stuck handshakes; the still-running + // side requests stop on it once both sides finish. + std::stop_source failsafe_stop; // Store lambdas in named variables before invoking - anonymous lambda + immediate // invocation pattern [...](){}() can cause capture corruption with run_async auto client_task = [&client, &client_failed, &client_done, &server_done, - &timeout, &s1, &s2, client_ec_out]() -> capy::task<> { + &failsafe_stop, &s1, &s2, + client_ec_out]() -> capy::task<> { auto [ec] = co_await client.handshake( std::remove_reference_t::client); if (client_ec_out) @@ -943,11 +944,11 @@ run_tls_test_fail( } client_done = true; if (server_done) - timeout.cancel(); + failsafe_stop.request_stop(); }; auto server_task = [&server, &server_failed, &server_done, &client_done, - &timeout, &s1, &s2]() -> capy::task<> { + &failsafe_stop, &s1, &s2]() -> capy::task<> { auto [ec] = co_await server.handshake( std::remove_reference_t::server); if (ec) @@ -967,16 +968,17 @@ run_tls_test_fail( } server_done = true; if (client_done) - timeout.cancel(); + failsafe_stop.request_stop(); }; bool failsafe_hit = false; - auto timeout_task = [&timeout, &failsafe_hit, &s1, &s2]() -> capy::task<> { - auto [ec] = co_await timeout.wait(); + auto timeout_task = [&failsafe_hit, &s1, &s2]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(200 * failsafe_scale)); if (!ec) { failsafe_hit = true; - // Timer expired - cancel pending operations then close sockets + // Deadline elapsed - cancel pending operations then close sockets if (s1.is_open()) { s1.cancel(); @@ -992,7 +994,8 @@ run_tls_test_fail( capy::run_async(ioc.get_executor())(client_task()); capy::run_async(ioc.get_executor())(server_task()); - capy::run_async(ioc.get_executor())(timeout_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + timeout_task()); ioc.run(); BOOST_TEST(!failsafe_hit); // failsafe timeout should not be hit @@ -1066,14 +1069,13 @@ run_tls_shutdown_test( // Server closing the socket causes client's shutdown to complete. bool done = false; - // Failsafe timer in case of bugs - timer failsafe(ioc); - failsafe.expires_after(std::chrono::milliseconds(200 * failsafe_scale)); + // Failsafe deadline in case of bugs + std::stop_source failsafe_stop; - auto client_shutdown = [&client, &done, &failsafe]() -> capy::task<> { + auto client_shutdown = [&client, &done, &failsafe_stop]() -> capy::task<> { auto [ec] = co_await client.shutdown(); done = true; - failsafe.cancel(); + failsafe_stop.request_stop(); BOOST_TEST( !ec || ec == capy::cond::stream_truncated || ec == capy::cond::eof || ec == capy::cond::canceled); @@ -1092,9 +1094,9 @@ run_tls_shutdown_test( }; bool failsafe_hit = false; - auto failsafe_task = [&failsafe, &failsafe_hit, &done, &s1, - &s2]() -> capy::task<> { - auto [ec] = co_await failsafe.wait(); + auto failsafe_task = [&failsafe_hit, &done, &s1, &s2]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(200 * failsafe_scale)); if (!ec && !done) { failsafe_hit = true; @@ -1113,7 +1115,8 @@ run_tls_shutdown_test( capy::run_async(ioc.get_executor())(client_shutdown()); capy::run_async(ioc.get_executor())(server_read_then_close()); - capy::run_async(ioc.get_executor())(failsafe_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + failsafe_task()); ioc.run(); BOOST_TEST(!failsafe_hit); // failsafe timeout should not be hit @@ -1180,10 +1183,8 @@ run_tls_truncation_test( bool read_done = false; bool failsafe_hit = false; - // Timeout to prevent deadlock - timer timeout(ioc); - // IOCP peer-close propagation can be bursty under TLS backends. - timeout.expires_after(std::chrono::milliseconds(750 * failsafe_scale)); + // Deadline to prevent deadlock + std::stop_source failsafe_stop; auto client_close = [&s1, &s2]() -> capy::task<> { // Cancel and close underlying socket without TLS shutdown (IOCP needs cancel) @@ -1196,23 +1197,25 @@ run_tls_truncation_test( }; auto server_read_truncated = [&server, &read_done, - &timeout]() -> capy::task<> { + &failsafe_stop]() -> capy::task<> { char buf[32]; auto [ec, n] = co_await server.read_some(capy::mutable_buffer(buf, sizeof(buf))); read_done = true; - timeout.cancel(); + failsafe_stop.request_stop(); // Under IOCP + TLS backends, abrupt peer close may surface as an error // or as a zero-byte completion after cancellation/close unblocks the read. BOOST_TEST(!!ec || n == 0); }; - auto timeout_task = [&timeout, &failsafe_hit, &s1, &s2]() -> capy::task<> { - auto [ec] = co_await timeout.wait(); + auto timeout_task = [&failsafe_hit, &s1, &s2]() -> capy::task<> { + // IOCP peer-close propagation can be bursty under TLS backends. + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(750 * failsafe_scale)); if (!ec) { failsafe_hit = true; - // Timer expired - cancel pending operations (check if still open) + // Deadline elapsed - cancel pending operations (check if still open) if (s1.is_open()) { s1.cancel(); @@ -1228,7 +1231,8 @@ run_tls_truncation_test( capy::run_async(ioc.get_executor())(client_close()); capy::run_async(ioc.get_executor())(server_read_truncated()); - capy::run_async(ioc.get_executor())(timeout_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + timeout_task()); ioc.run(); BOOST_TEST(read_done); @@ -1460,16 +1464,16 @@ run_connection_reset_test( bool client_failed = false; // Timeout protection - timer timeout(ioc); - timeout.expires_after(std::chrono::milliseconds(200 * failsafe_scale)); + std::stop_source failsafe_stop; - auto client_task = [&client, &client_failed, &timeout]() -> capy::task<> { + auto client_task = [&client, &client_failed, + &failsafe_stop]() -> capy::task<> { auto [ec] = co_await client.handshake( std::remove_reference_t::client); // Should fail because server closed socket if (ec) client_failed = true; - timeout.cancel(); + failsafe_stop.request_stop(); }; // Server closes socket immediately (simulates connection reset) @@ -1481,8 +1485,9 @@ run_connection_reset_test( }; bool failsafe_hit = false; - auto timeout_task = [&timeout, &failsafe_hit, &s1]() -> capy::task<> { - auto [ec] = co_await timeout.wait(); + auto timeout_task = [&failsafe_hit, &s1]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(200 * failsafe_scale)); if (!ec && s1.is_open()) { failsafe_hit = true; @@ -1493,7 +1498,8 @@ run_connection_reset_test( capy::run_async(ioc.get_executor())(client_task()); capy::run_async(ioc.get_executor())(server_task()); - capy::run_async(ioc.get_executor())(timeout_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + timeout_task()); ioc.run(); @@ -1541,19 +1547,18 @@ run_stop_token_handshake_test( std::stop_source stop_src; bool client_got_error = false; - // Failsafe timeout to prevent infinite hang if cancellation doesn't work + // Failsafe deadline to prevent infinite hang if cancellation doesn't work // 2000ms allows headroom for CI with coverage instrumentation - timer failsafe(ioc); - failsafe.expires_after(std::chrono::milliseconds(2000 * failsafe_scale)); + std::stop_source failsafe_stop; // Client handshake - will be cancelled while waiting for ServerHello auto client_task = [&client, &client_got_error, - &failsafe]() -> capy::task<> { + &failsafe_stop]() -> capy::task<> { auto [ec] = co_await client.handshake( std::remove_reference_t::client); if (ec) client_got_error = true; - failsafe.cancel(); + failsafe_stop.request_stop(); }; // Server waits for ClientHello then cancels - deterministic synchronization @@ -1566,9 +1571,9 @@ run_stop_token_handshake_test( }; bool failsafe_hit = false; - auto failsafe_task = [&failsafe, &failsafe_hit, &s1, - &s2]() -> capy::task<> { - auto [ec] = co_await failsafe.wait(); + auto failsafe_task = [&failsafe_hit, &s1, &s2]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(2000 * failsafe_scale)); if (!ec) { failsafe_hit = true; @@ -1586,7 +1591,8 @@ run_stop_token_handshake_test( }; capy::run_async(ioc.get_executor(), stop_src.get_token())(client_task()); capy::run_async(ioc.get_executor())(server_task()); - capy::run_async(ioc.get_executor())(failsafe_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + failsafe_task()); ioc.run(); BOOST_TEST(!failsafe_hit); // failsafe timeout should not be hit @@ -1644,17 +1650,17 @@ run_stop_token_read_test( std::stop_source stop_src; bool read_got_error = false; - // Failsafe timeout - 2000ms allows headroom for CI with coverage instrumentation - timer failsafe(ioc); - failsafe.expires_after(std::chrono::milliseconds(2000 * failsafe_scale)); + // Failsafe deadline - 2000ms allows headroom for CI with coverage instrumentation + std::stop_source failsafe_stop; - auto client_read = [&client, &read_got_error, &failsafe]() -> capy::task<> { + auto client_read = [&client, &read_got_error, + &failsafe_stop]() -> capy::task<> { char buf[32]; auto [ec, n] = co_await client.read_some(capy::mutable_buffer(buf, sizeof(buf))); if (ec) read_got_error = true; - failsafe.cancel(); + failsafe_stop.request_stop(); }; // Server triggers cancellation immediately - client will block on read @@ -1666,9 +1672,9 @@ run_stop_token_read_test( }; bool failsafe_hit = false; - auto failsafe_task = [&failsafe, &failsafe_hit, &s1, - &s2]() -> capy::task<> { - auto [ec] = co_await failsafe.wait(); + auto failsafe_task = [&failsafe_hit, &s1, &s2]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(2000 * failsafe_scale)); if (!ec) { failsafe_hit = true; @@ -1686,7 +1692,8 @@ run_stop_token_read_test( }; capy::run_async(ioc.get_executor(), stop_src.get_token())(client_read()); capy::run_async(ioc.get_executor())(server_cancel()); - capy::run_async(ioc.get_executor())(failsafe_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + failsafe_task()); ioc.run(); BOOST_TEST(!failsafe_hit); // failsafe timeout should not be hit @@ -1747,12 +1754,11 @@ run_stop_token_write_test( // Large buffer to fill socket buffer and cause blocking std::vector large_buf(std::size_t{1024} * 1024, 'X'); - // Failsafe timeout - 2000ms allows headroom for CI with coverage instrumentation - timer failsafe(ioc); - failsafe.expires_after(std::chrono::milliseconds(2000 * failsafe_scale)); + // Failsafe deadline - 2000ms allows headroom for CI with coverage instrumentation + std::stop_source failsafe_stop; auto client_write = [&client, &large_buf, &write_got_error, - &failsafe]() -> capy::task<> { + &failsafe_stop]() -> capy::task<> { // Write in loop until cancelled or error for (int i = 0; i < 100; ++i) { @@ -1761,11 +1767,11 @@ run_stop_token_write_test( if (ec) { write_got_error = true; - failsafe.cancel(); + failsafe_stop.request_stop(); co_return; } } - failsafe.cancel(); + failsafe_stop.request_stop(); }; // Server waits for data then cancels - deterministic synchronization @@ -1778,9 +1784,9 @@ run_stop_token_write_test( }; bool failsafe_hit = false; - auto failsafe_task = [&failsafe, &failsafe_hit, &s1, - &s2]() -> capy::task<> { - auto [ec] = co_await failsafe.wait(); + auto failsafe_task = [&failsafe_hit, &s1, &s2]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(2000 * failsafe_scale)); if (!ec) { failsafe_hit = true; @@ -1798,7 +1804,8 @@ run_stop_token_write_test( }; capy::run_async(ioc.get_executor(), stop_src.get_token())(client_write()); capy::run_async(ioc.get_executor())(server_cancel()); - capy::run_async(ioc.get_executor())(failsafe_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + failsafe_task()); ioc.run(); BOOST_TEST(!failsafe_hit); // failsafe timeout should not be hit @@ -1838,18 +1845,17 @@ run_socket_cancel_test( bool client_got_error = false; - // Failsafe timeout - 2000ms allows headroom for CI with coverage instrumentation - timer failsafe(ioc); - failsafe.expires_after(std::chrono::milliseconds(2000 * failsafe_scale)); + // Failsafe deadline - 2000ms allows headroom for CI with coverage instrumentation + std::stop_source failsafe_stop; // Client starts handshake - will be cancelled auto client_task = [&client, &client_got_error, - &failsafe]() -> capy::task<> { + &failsafe_stop]() -> capy::task<> { auto [ec] = co_await client.handshake( std::remove_reference_t::client); if (ec) client_got_error = true; - failsafe.cancel(); + failsafe_stop.request_stop(); }; // Server waits for ClientHello then cancels - deterministic synchronization @@ -1862,9 +1868,9 @@ run_socket_cancel_test( }; bool failsafe_hit = false; - auto failsafe_task = [&failsafe, &failsafe_hit, &s1, - &s2]() -> capy::task<> { - auto [ec] = co_await failsafe.wait(); + auto failsafe_task = [&failsafe_hit, &s1, &s2]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(2000 * failsafe_scale)); if (!ec) { failsafe_hit = true; @@ -1882,7 +1888,8 @@ run_socket_cancel_test( }; capy::run_async(ioc.get_executor())(client_task()); capy::run_async(ioc.get_executor())(server_task()); - capy::run_async(ioc.get_executor())(failsafe_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + failsafe_task()); ioc.run(); BOOST_TEST(!failsafe_hit); // failsafe timeout should not be hit diff --git a/test/unit/timeout.cpp b/test/unit/timeout.cpp new file mode 100644 index 000000000..5567f80f6 --- /dev/null +++ b/test/unit/timeout.cpp @@ -0,0 +1,375 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "context.hpp" +#include "test_suite.hpp" + +namespace boost::corosio { + +// Immediately-resuming awaitable yielding io_result, used to +// verify payload preservation through timeout()'s pass-through path. +struct payload_awaitable +{ + int value_; + + explicit payload_awaitable(int value) : value_(value) {} + + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, capy::io_env const*) + { + return h; + } + capy::io_result await_resume() noexcept + { + return {{}, value_}; + } +}; + +// Only completes when cancelled, via delay()'s own (well-tested) +// stop-token handling, returning a non-default payload alongside +// cond::canceled. Used to confirm that timeout()'s deadline-win path +// discards the inner payload rather than merely rewriting `ec` in +// place. +inline capy::task> +stop_only_payload() +{ + auto [ec] = co_await delay(std::chrono::seconds(10)); + co_return capy::io_result{ec, 99}; +} + +template +struct timeout_test +{ + void testInnerWins() + { + io_context ioc(Backend); + bool ok = false; + + auto t = [](bool& ok_out) -> capy::task<> { + // Inner (5ms) completes before the 10s deadline + auto [ec] = co_await timeout( + delay(std::chrono::milliseconds(5)), + std::chrono::seconds(10)); + ok_out = !ec; + }; + capy::run_async(ioc.get_executor())(t(ok)); + + ioc.run(); + BOOST_TEST(ok); + } + + void testDeadlineWins() + { + io_context ioc(Backend); + bool timed_out = false; + + auto t = [](bool& out) -> capy::task<> { + auto [ec] = co_await timeout( + delay(std::chrono::seconds(10)), + std::chrono::milliseconds(5)); + out = (ec == capy::cond::timeout); + }; + capy::run_async(ioc.get_executor())(t(timed_out)); + + ioc.run(); + BOOST_TEST(timed_out); + } + + void testTimePointDeadlineWins() + { + io_context ioc(Backend); + bool timed_out = false; + + auto t = [](bool& out) -> capy::task<> { + auto tp = std::chrono::steady_clock::now() + + std::chrono::milliseconds(5); + auto [ec] = co_await timeout( + delay(std::chrono::seconds(10)), tp); + out = (ec == capy::cond::timeout); + }; + capy::run_async(ioc.get_executor())(t(timed_out)); + + ioc.run(); + BOOST_TEST(timed_out); + } + + void testParentCancelReturnsCanceled() + { + io_context ioc(Backend); + std::stop_source src; + bool canceled = false; + + auto t = [](bool& out) -> capy::task<> { + auto [ec] = co_await timeout( + delay(std::chrono::seconds(10)), + std::chrono::seconds(10)); + out = (ec == capy::cond::canceled); + }; + capy::run_async(ioc.get_executor(), src.get_token())(t(canceled)); + + ioc.run_one(); + src.request_stop(); + ioc.run(); + BOOST_TEST(canceled); + } + + void testInnerErrorPassesThrough() + { + // An inner op that completes with its own (non-canceled) + // error keeps that error even though a deadline is armed. + io_context ioc(Backend); + bool got_error = false; + + struct erroring_awaitable + { + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, capy::io_env const*) + { + return h; + } + capy::io_result<> await_resume() noexcept + { + return {make_error_code( + std::errc::connection_refused)}; + } + }; + + auto t = [](bool& out) -> capy::task<> { + auto [ec] = co_await timeout( + erroring_awaitable{}, std::chrono::seconds(10)); + out = (ec == std::errc::connection_refused); + }; + capy::run_async(ioc.get_executor())(t(got_error)); + + ioc.run(); + BOOST_TEST(got_error); + } + + void testInnerExceptionPropagates() + { + io_context ioc(Backend); + bool caught = false; + + struct throwing_awaitable + { + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, capy::io_env const*) + { + return h; + } + capy::io_result<> await_resume() + { + throw std::runtime_error("boom"); + } + }; + + auto t = [](bool& out) -> capy::task<> { + try + { + (void) co_await timeout( + throwing_awaitable{}, std::chrono::seconds(10)); + } + catch(std::runtime_error const&) + { + out = true; + } + }; + capy::run_async(ioc.get_executor())(t(caught)); + + ioc.run(); + BOOST_TEST(caught); + } + + void testSingleThreadedHint() + { + io_context ioc(Backend, 1u); + bool timed_out = false; + + auto t = [](bool& out) -> capy::task<> { + auto [ec] = co_await timeout( + delay(std::chrono::seconds(10)), + std::chrono::milliseconds(5)); + out = (ec == capy::cond::timeout); + }; + capy::run_async(ioc.get_executor())(t(timed_out)); + + ioc.run(); + BOOST_TEST(timed_out); + } + + void testPayloadPreservedOnInnerWin() + { + io_context ioc(Backend); + bool ok = false; + int value = 0; + + auto t = [](bool& ok_out, int& value_out) -> capy::task<> { + auto [ec, n] = co_await timeout( + payload_awaitable(42), std::chrono::seconds(10)); + ok_out = !ec; + value_out = n; + }; + capy::run_async(ioc.get_executor())(t(ok, value)); + + ioc.run(); + BOOST_TEST(ok); + BOOST_TEST_EQ(value, 42); + } + + void testTimeoutResultHasDefaultPayload() + { + // The inner op only completes when cancelled (returning a + // non-default payload of 99 alongside cond::canceled). When + // the deadline wins, timeout() must produce a fresh result + // with a default-initialized payload, not the inner's 99. + io_context ioc(Backend); + bool timed_out = false; + int value = -1; + + auto t = [](bool& out, int& value_out) -> capy::task<> { + auto [ec, n] = co_await timeout( + stop_only_payload(), + std::chrono::milliseconds(5)); + out = (ec == capy::cond::timeout); + value_out = n; + }; + capy::run_async(ioc.get_executor())(t(timed_out, value)); + + ioc.run(); + BOOST_TEST(timed_out); + BOOST_TEST_EQ(value, 0); + } + + void testNestedTimeouts() + { + // An outer timeout wraps an inner timeout(). The inner + // deadline (5ms) is shorter than the outer (10s), so the + // inner timeout wins the race and its result (ec == + // cond::timeout) passes through the outer unchanged. + io_context ioc(Backend); + bool timed_out = false; + + auto t = [](bool& out) -> capy::task<> { + auto [ec] = co_await timeout( + timeout( + delay(std::chrono::seconds(10)), + std::chrono::milliseconds(5)), + std::chrono::seconds(10)); + out = (ec == capy::cond::timeout); + }; + capy::run_async(ioc.get_executor())(t(timed_out)); + + ioc.run(); + BOOST_TEST(timed_out); + } + + void testTimeoutAlreadyElapsedDeadline() + { + // A deadline already in the past, or a zero duration, still + // races the long inner op and resolves as cond::timeout. + { + io_context ioc(Backend); + bool timed_out = false; + + auto t = [](bool& out) -> capy::task<> { + auto tp = std::chrono::steady_clock::now() - + std::chrono::seconds(1); + auto [ec] = co_await timeout( + delay(std::chrono::seconds(10)), tp); + out = (ec == capy::cond::timeout); + }; + capy::run_async(ioc.get_executor())(t(timed_out)); + + ioc.run(); + BOOST_TEST(timed_out); + } + { + io_context ioc(Backend); + bool timed_out = false; + + auto t = [](bool& out) -> capy::task<> { + auto [ec] = co_await timeout( + delay(std::chrono::seconds(10)), + std::chrono::milliseconds(0)); + out = (ec == capy::cond::timeout); + }; + capy::run_async(ioc.get_executor())(t(timed_out)); + + ioc.run(); + BOOST_TEST(timed_out); + } + } + + void testShutdownWithSuspendedTimeout() + { + // Destroying the io_context while a timeout() is suspended must + // drain both the inner op's waiter and the parked timeout_coro + // from the heap, and destroy the awaiting frame cleanly. + int destroyed = 0; + + { + io_context ioc(Backend); + + auto task = [](int& counter) -> capy::task<> { + struct guard + { + int& c_; + ~guard() { ++c_; } + }; + guard g{counter}; + auto [ec] = co_await timeout( + delay(std::chrono::hours(1)), + std::chrono::hours(1)); + (void)ec; + }; + + capy::run_async(ioc.get_executor())(task(destroyed)); + ioc.poll(); + // io_context destructs with the timeout still suspended. + } + + BOOST_TEST_EQ(destroyed, 1); + } + + void run() + { + testInnerWins(); + testDeadlineWins(); + testTimePointDeadlineWins(); + testParentCancelReturnsCanceled(); + testInnerErrorPassesThrough(); + testInnerExceptionPropagates(); + testSingleThreadedHint(); + testPayloadPreservedOnInnerWin(); + testTimeoutResultHasDefaultPayload(); + testNestedTimeouts(); + testTimeoutAlreadyElapsedDeadline(); + testShutdownWithSuspendedTimeout(); + } +}; + +COROSIO_BACKEND_TESTS(timeout_test, "boost.corosio.timeout") + +} // namespace boost::corosio diff --git a/test/unit/timer.cpp b/test/unit/timer.cpp deleted file mode 100644 index 98d5f9187..000000000 --- a/test/unit/timer.cpp +++ /dev/null @@ -1,1368 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "context.hpp" -#include "test_suite.hpp" - -namespace boost::corosio { - -// Issue #213: a timer accepts any execution_context (so the common -// `timer(obj.context())` idiom works), but a non-io_context now throws -// rather than crashing via an unchecked downcast (see testNonIoContextThrows). -static_assert(std::is_constructible_v); -static_assert(std::is_constructible_v); - -// Timer-specific tests -// Focus: timer construction, expiry, wait, and cancellation -// -// Tests are templated on the context type to run with all available backends. - -template -struct timer_test -{ - // Construction and move semantics - - void testConstruction() - { - io_context ioc(Backend); - timer t(ioc); - - BOOST_TEST_PASS(); - } - - // Issue #213: constructing a timer from an execution_context that is - // not a corosio io_context (no timer service installed) must throw, - // not crash via an unchecked downcast. - void testNonIoContextThrows() - { - struct bare_context : capy::execution_context - { - }; - - bare_context ctx; - BOOST_TEST_THROWS(timer(ctx), std::logic_error); - } - - void testConstructionWithTimePoint() - { - io_context ioc(Backend); - auto tp = timer::clock_type::now() + std::chrono::seconds(10); - timer t(ioc, tp); - - BOOST_TEST(t.expiry() == tp); - } - - // Issue #231: bring timer in line with the other I/O objects, which - // all construct from an executor (delegating to the executor's context). - void testConstructFromExecutor() - { - io_context ioc(Backend); - timer t(ioc.get_executor()); - - BOOST_TEST_PASS(); - } - - void testConstructFromExecutorWithTimePoint() - { - io_context ioc(Backend); - auto tp = timer::clock_type::now() + std::chrono::seconds(10); - timer t(ioc.get_executor(), tp); - - BOOST_TEST(t.expiry() == tp); - } - - void testConstructFromExecutorWithDuration() - { - io_context ioc(Backend); - timer t(ioc.get_executor(), std::chrono::milliseconds(500)); - - BOOST_TEST(t.expiry() > timer::clock_type::now()); - } - - // The executor ctors delegate to the executor's context, so an executor - // whose context is not an io_context (e.g. a thread_pool) must throw, - // not crash — same guarantee as testNonIoContextThrows, new entry path. - void testExecutorNonIoContextThrows() - { - capy::thread_pool pool(1); - BOOST_TEST_THROWS(timer(pool.get_executor()), std::logic_error); - } - - void testConstructionWithDuration() - { - io_context ioc(Backend); - auto before = timer::clock_type::now(); - timer t(ioc, std::chrono::milliseconds(500)); - auto after = timer::clock_type::now(); - - BOOST_TEST(t.expiry() >= before + std::chrono::milliseconds(500)); - BOOST_TEST(t.expiry() <= after + std::chrono::milliseconds(500)); - } - - void testMoveConstruct() - { - io_context ioc(Backend); - timer t1(ioc); - t1.expires_after(std::chrono::milliseconds(100)); - auto expiry = t1.expiry(); - - timer t2(std::move(t1)); - BOOST_TEST(t2.expiry() == expiry); - } - - void testMoveAssign() - { - io_context ioc(Backend); - timer t1(ioc); - timer t2(ioc); - - t1.expires_after(std::chrono::milliseconds(100)); - auto expiry = t1.expiry(); - - t2 = std::move(t1); - BOOST_TEST(t2.expiry() == expiry); - } - - void testMoveAssignCrossContext() - { - io_context ioc1(Backend); - io_context ioc2(Backend); - timer t1(ioc1); - timer t2(ioc2); - - t1.expires_after(std::chrono::milliseconds(100)); - auto expiry = t1.expiry(); - - t2 = std::move(t1); - BOOST_TEST(t2.expiry() == expiry); - } - - // Expiry setting and retrieval - - void testDefaultExpiry() - { - io_context ioc(Backend); - timer t(ioc); - - auto expiry = t.expiry(); - BOOST_TEST(expiry == timer::time_point{}); - } - - void testExpiresAfter() - { - io_context ioc(Backend); - timer t(ioc); - - auto before = timer::clock_type::now(); - t.expires_after(std::chrono::milliseconds(100)); - auto after = timer::clock_type::now(); - - auto expiry = t.expiry(); - BOOST_TEST(expiry >= before + std::chrono::milliseconds(100)); - BOOST_TEST(expiry <= after + std::chrono::milliseconds(100)); - } - - void testExpiresAfterDifferentDurations() - { - io_context ioc(Backend); - timer t(ioc); - - auto before = timer::clock_type::now(); - t.expires_after(std::chrono::seconds(1)); - auto expiry = t.expiry(); - BOOST_TEST(expiry >= before + std::chrono::seconds(1)); - - before = timer::clock_type::now(); - t.expires_after(std::chrono::microseconds(500000)); - expiry = t.expiry(); - BOOST_TEST(expiry >= before + std::chrono::microseconds(500000)); - - before = timer::clock_type::now(); - t.expires_after(std::chrono::hours(0)); - expiry = t.expiry(); - BOOST_TEST(expiry <= before + std::chrono::milliseconds(10)); - } - - void testExpiresAt() - { - io_context ioc(Backend); - timer t(ioc); - - auto target = timer::clock_type::now() + std::chrono::milliseconds(200); - t.expires_at(target); - - BOOST_TEST(t.expiry() == target); - } - - void testExpiresAtPast() - { - io_context ioc(Backend); - timer t(ioc); - - auto target = timer::clock_type::now() - std::chrono::seconds(1); - t.expires_at(target); - - BOOST_TEST(t.expiry() == target); - } - - void testExpiresAtReplace() - { - io_context ioc(Backend); - timer t(ioc); - - auto first = timer::clock_type::now() + std::chrono::seconds(10); - t.expires_at(first); - BOOST_TEST(t.expiry() == first); - - auto second = timer::clock_type::now() + std::chrono::seconds(5); - t.expires_at(second); - BOOST_TEST(t.expiry() == second); - } - - // Async wait tests - - void testWaitBasic() - { - io_context ioc(Backend); - timer t(ioc); - - bool completed = false; - std::error_code result_ec; - - t.expires_after(std::chrono::milliseconds(10)); - - auto task = [](timer& t_ref, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - void testWaitTimingAccuracy() - { - io_context ioc(Backend); - timer t(ioc); - - auto start = timer::clock_type::now(); - timer::duration elapsed; - - t.expires_after(std::chrono::milliseconds(50)); - - auto task = [](timer& t_ref, timer::time_point start_val, - timer::duration& elapsed_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - elapsed_out = timer::clock_type::now() - start_val; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t, start, elapsed)); - - ioc.run(); - - BOOST_TEST(elapsed >= std::chrono::milliseconds(50)); - BOOST_TEST(elapsed < std::chrono::seconds(2)); - } - - void testWaitExpiredTimer() - { - io_context ioc(Backend); - timer t(ioc); - - bool completed = false; - std::error_code result_ec; - - t.expires_at(timer::clock_type::now() - std::chrono::seconds(1)); - - auto task = [](timer& t_ref, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - void testWaitZeroDuration() - { - io_context ioc(Backend); - timer t(ioc); - - bool completed = false; - std::error_code result_ec; - - t.expires_after(std::chrono::milliseconds(0)); - - auto task = [](timer& t_ref, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - // Cancellation tests - - void testCancel() - { - io_context ioc(Backend); - timer t(ioc); - timer cancel_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - t.expires_after(std::chrono::seconds(60)); - cancel_timer.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(wait_task(t, result_ec, completed)); - - auto cancel_task = [](timer& cancel_t_ref, - timer& t_ref) -> capy::task<> { - (void)co_await cancel_t_ref.wait(); - t_ref.cancel(); - }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, t)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testCancelNoWaiters() - { - io_context ioc(Backend); - timer t(ioc); - - t.expires_after(std::chrono::seconds(60)); - - t.cancel(); - BOOST_TEST_PASS(); - } - - void testCancelMultipleTimes() - { - io_context ioc(Backend); - timer t(ioc); - - t.expires_after(std::chrono::seconds(60)); - - t.cancel(); - t.cancel(); - t.cancel(); - BOOST_TEST_PASS(); - } - - void testExpiresAtCancelsWaiter() - { - io_context ioc(Backend); - timer t(ioc); - timer delay_timer(ioc); - - bool completed = false; - std::error_code result_ec; - - t.expires_after(std::chrono::seconds(60)); - delay_timer.expires_after(std::chrono::milliseconds(50)); - - auto wait_task = [](timer& t_ref, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(wait_task(t, result_ec, completed)); - - auto delay_task = [](timer& delay_ref, timer& t_ref) -> capy::task<> { - (void)co_await delay_ref.wait(); - t_ref.expires_after(std::chrono::seconds(30)); - }; - capy::run_async(ioc.get_executor())(delay_task(delay_timer, t)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testStopTokenCancellation() - { - // A pending timer wait should be cancelled when its stop_token - // is signaled after the wait has already suspended. - io_context ioc(Backend); - timer t(ioc); - timer delay(ioc); - - std::stop_source stop_src; - bool wait_done = false; - bool failsafe_hit = false; - std::error_code wait_ec; - - t.expires_after(std::chrono::seconds(60)); - - // Waiter task — bound to stop_token - auto wait_task = [&]() -> capy::task<> { - auto [ec] = co_await t.wait(); - wait_ec = ec; - wait_done = true; - }; - - // Canceller — short delay then signal stop - auto canceller_task = [&]() -> capy::task<> { - delay.expires_after(std::chrono::milliseconds(10)); - (void)co_await delay.wait(); - stop_src.request_stop(); - }; - - // Failsafe — if stop_token didn't cancel the timer, - // fall back to manual cancel so the test doesn't hang - auto failsafe_task = [&]() -> capy::task<> { - timer ft(ioc); - ft.expires_after(std::chrono::milliseconds(1000)); - auto [ec] = co_await ft.wait(); - if (!ec && !wait_done) - { - failsafe_hit = true; - t.cancel(); - } - }; - - capy::run_async(ioc.get_executor(), stop_src.get_token())(wait_task()); - capy::run_async(ioc.get_executor())(canceller_task()); - capy::run_async(ioc.get_executor())(failsafe_task()); - - ioc.run(); - - BOOST_TEST(wait_done); - BOOST_TEST(wait_ec == capy::cond::canceled); - - // If the failsafe was hit, stop_token cancellation didn't work — - // only the manual t.cancel() fallback rescued the test. - BOOST_TEST(!failsafe_hit); - } - - // Multiple timer tests - - void testMultipleTimersDifferentExpiry() - { - io_context ioc(Backend); - timer t1(ioc); - timer t2(ioc); - timer t3(ioc); - - int order = 0; - int t1_order = 0, t2_order = 0, t3_order = 0; - - t1.expires_after(std::chrono::milliseconds(30)); - t2.expires_after(std::chrono::milliseconds(10)); - t3.expires_after(std::chrono::milliseconds(20)); - - auto task = [](timer& t_ref, int& order_ref, - int& t_order_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - t_order_out = ++order_ref; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t1, order, t1_order)); - capy::run_async(ioc.get_executor())(task(t2, order, t2_order)); - capy::run_async(ioc.get_executor())(task(t3, order, t3_order)); - - ioc.run(); - - BOOST_TEST_EQ(t2_order, 1); - BOOST_TEST_EQ(t3_order, 2); - BOOST_TEST_EQ(t1_order, 3); - } - - void testMultipleTimersSameExpiry() - { - io_context ioc(Backend); - timer t1(ioc); - timer t2(ioc); - - bool t1_done = false, t2_done = false; - - auto expiry = timer::clock_type::now() + std::chrono::milliseconds(20); - t1.expires_at(expiry); - t2.expires_at(expiry); - - auto task = [](timer& t_ref, bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t1, t1_done)); - capy::run_async(ioc.get_executor())(task(t2, t2_done)); - - ioc.run(); - - BOOST_TEST(t1_done); - BOOST_TEST(t2_done); - } - - void testReheapifyOnExpiresAtUpdate() - { - // Push several waiters into the heap, suspend them via run_for so - // their wait actually registers, then move the first timer's - // expiry far into the future — exercises the update_timer - // re-heap (down_heap) branch in timer_service. - io_context ioc(Backend); - timer t1(ioc), t2(ioc), t3(ioc), t4(ioc); - - bool d1 = false, d2 = false, d3 = false, d4 = false; - std::error_code e1, e2, e3, e4; - - t1.expires_after(std::chrono::seconds(5)); - t2.expires_after(std::chrono::milliseconds(20)); - t3.expires_after(std::chrono::milliseconds(30)); - t4.expires_after(std::chrono::milliseconds(40)); - - auto task = [](timer& t, bool& done, std::error_code& ec_out) - -> capy::task<> { - auto [ec] = co_await t.wait(); - done = true; - ec_out = ec; - }; - - capy::run_async(ioc.get_executor())(task(t1, d1, e1)); - capy::run_async(ioc.get_executor())(task(t2, d2, e2)); - capy::run_async(ioc.get_executor())(task(t3, d3, e3)); - capy::run_async(ioc.get_executor())(task(t4, d4, e4)); - - // Let the waiters actually register in the heap, then move - // t1's expiry far past t2..t4 — this cancels t1's pending - // waiter and exercises the down-heap branch. - ioc.poll(); // process any inline work - ioc.restart(); - t1.expires_at(timer::clock_type::now() + std::chrono::seconds(60)); - - ioc.run(); - - BOOST_TEST(d2); - BOOST_TEST(d3); - BOOST_TEST(d4); - BOOST_TEST(!e2); - BOOST_TEST(!e3); - BOOST_TEST(!e4); - // t1's waiter was cancelled by the second expires_at. - BOOST_TEST(d1); - BOOST_TEST(e1 == capy::cond::canceled); - } - - void testTimerFreeListReuseAcrossContexts() - { - // Create timers in one context, destroy the context, then create - // a new context — covers the timer_service shutdown free-list - // delete path and the construct() free-list pop path. - for (int i = 0; i < 3; ++i) - { - io_context ioc(Backend); - timer t1(ioc), t2(ioc), t3(ioc); - t1.expires_after(std::chrono::milliseconds(1)); - t2.expires_after(std::chrono::milliseconds(2)); - t3.expires_after(std::chrono::milliseconds(3)); - - bool done = false; - auto task = [&]() -> capy::task<> { - (void)co_await t1.wait(); - (void)co_await t2.wait(); - (void)co_await t3.wait(); - done = true; - }; - capy::run_async(ioc.get_executor())(task()); - ioc.run(); - BOOST_TEST(done); - } - } - - // Multiple waiters on one timer - - void testMultipleWaiters() - { - io_context ioc(Backend); - timer t(ioc); - - bool w1 = false, w2 = false, w3 = false; - std::error_code ec1, ec2, ec3; - - t.expires_after(std::chrono::milliseconds(10)); - - auto task = [](timer& t_ref, std::error_code& ec_out, - bool& done) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done = true; - }; - - capy::run_async(ioc.get_executor())(task(t, ec1, w1)); - capy::run_async(ioc.get_executor())(task(t, ec2, w2)); - capy::run_async(ioc.get_executor())(task(t, ec3, w3)); - - ioc.run(); - - BOOST_TEST(w1); - BOOST_TEST(w2); - BOOST_TEST(w3); - BOOST_TEST(!ec1); - BOOST_TEST(!ec2); - BOOST_TEST(!ec3); - } - - void testMultipleWaitersCancelAll() - { - io_context ioc(Backend); - timer t(ioc); - timer delay(ioc); - - bool w1 = false, w2 = false, w3 = false; - std::error_code ec1, ec2, ec3; - - t.expires_after(std::chrono::seconds(60)); - delay.expires_after(std::chrono::milliseconds(10)); - - auto task = [](timer& t_ref, std::error_code& ec_out, - bool& done) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done = true; - }; - - auto cancel_task = [](timer& delay_ref, timer& t_ref) -> capy::task<> { - (void)co_await delay_ref.wait(); - t_ref.cancel(); - }; - - capy::run_async(ioc.get_executor())(task(t, ec1, w1)); - capy::run_async(ioc.get_executor())(task(t, ec2, w2)); - capy::run_async(ioc.get_executor())(task(t, ec3, w3)); - capy::run_async(ioc.get_executor())(cancel_task(delay, t)); - - ioc.run(); - - BOOST_TEST(w1); - BOOST_TEST(w2); - BOOST_TEST(w3); - BOOST_TEST(ec1 == capy::cond::canceled); - BOOST_TEST(ec2 == capy::cond::canceled); - BOOST_TEST(ec3 == capy::cond::canceled); - } - - void testMultipleWaitersStopTokenCancelsOne() - { - io_context ioc(Backend); - timer t(ioc); - - std::stop_source stop_src; - bool w1 = false, w2 = false; - std::error_code ec1, ec2; - - t.expires_after(std::chrono::milliseconds(500)); - - // w1 has a stop_token — will be cancelled individually - auto wait_task = [&]() -> capy::task<> { - auto [ec] = co_await t.wait(); - ec1 = ec; - w1 = true; - }; - - // w2 has no stop_token — completes when timer fires - auto wait_task2 = [&]() -> capy::task<> { - auto [ec] = co_await t.wait(); - ec2 = ec; - w2 = true; - }; - - // Cancel w1 once both waiters are registered. Posting through the - // executor (rather than racing a wall-clock delay timer against t's - // expiry) makes the ordering deterministic: run_async queues the - // tasks, and the IOCP/reactor loop drains posted completions in FIFO - // order, so both wait tasks run and suspend on t before this task - // calls request_stop(). t's deadline is far enough out that w2 then - // completes via a normal expiry afterwards. - auto cancel_one = [&]() -> capy::task<> { - stop_src.request_stop(); - co_return; - }; - - capy::run_async(ioc.get_executor(), stop_src.get_token())(wait_task()); - capy::run_async(ioc.get_executor())(wait_task2()); - capy::run_async(ioc.get_executor())(cancel_one()); - - ioc.run(); - - BOOST_TEST(w1); - BOOST_TEST(w2); - BOOST_TEST(ec1 == capy::cond::canceled); - BOOST_TEST(!ec2); - } - - // Destruction cancels pending waiters - - void testDestructionCancelsPendingWaiters() - { - io_context ioc(Backend); - timer delay(ioc); - - bool w1 = false, w2 = false; - std::error_code ec1, ec2; - - auto t = std::make_unique(ioc); - t->expires_after(std::chrono::seconds(60)); - - delay.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, std::error_code& ec_out, - bool& done) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done = true; - }; - - auto destroy_task = [&]() -> capy::task<> { - (void)co_await delay.wait(); - t.reset(); - }; - - capy::run_async(ioc.get_executor())(wait_task(*t, ec1, w1)); - capy::run_async(ioc.get_executor())(wait_task(*t, ec2, w2)); - capy::run_async(ioc.get_executor())(destroy_task()); - - ioc.run(); - - BOOST_TEST(w1); - BOOST_TEST(w2); - BOOST_TEST(ec1 == capy::cond::canceled); - BOOST_TEST(ec2 == capy::cond::canceled); - } - - // cancel_one() tests - - void testCancelOne() - { - io_context ioc(Backend); - timer t(ioc); - timer delay(ioc); - - bool w1 = false, w2 = false; - std::error_code ec1, ec2; - - t.expires_after(std::chrono::milliseconds(500)); - delay.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, std::error_code& ec_out, - bool& done) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done = true; - }; - - auto cancel_one_task = [](timer& delay_ref, - timer& t_ref) -> capy::task<> { - (void)co_await delay_ref.wait(); - auto n = t_ref.cancel_one(); - BOOST_TEST_EQ(n, 1u); - }; - - capy::run_async(ioc.get_executor())(wait_task(t, ec1, w1)); - capy::run_async(ioc.get_executor())(wait_task(t, ec2, w2)); - capy::run_async(ioc.get_executor())(cancel_one_task(delay, t)); - - ioc.run(); - - BOOST_TEST(w1); - BOOST_TEST(w2); - // First waiter (FIFO) is cancelled, second fires normally - BOOST_TEST(ec1 == capy::cond::canceled); - BOOST_TEST(!ec2); - } - - void testCancelOneNoWaiters() - { - io_context ioc(Backend); - timer t(ioc); - - t.expires_after(std::chrono::seconds(60)); - - auto n = t.cancel_one(); - BOOST_TEST_EQ(n, 0u); - } - - // Return value tests - - void testCancelReturnsCount() - { - io_context ioc(Backend); - timer t(ioc); - timer delay(ioc); - - bool w1 = false, w2 = false, w3 = false; - std::error_code ec1, ec2, ec3; - - t.expires_after(std::chrono::seconds(60)); - delay.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, std::error_code& ec_out, - bool& done) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done = true; - }; - - std::size_t cancel_count = 0; - auto cancel_task = [&](timer& delay_ref, timer& t_ref) -> capy::task<> { - (void)co_await delay_ref.wait(); - cancel_count = t_ref.cancel(); - }; - - capy::run_async(ioc.get_executor())(wait_task(t, ec1, w1)); - capy::run_async(ioc.get_executor())(wait_task(t, ec2, w2)); - capy::run_async(ioc.get_executor())(wait_task(t, ec3, w3)); - capy::run_async(ioc.get_executor())(cancel_task(delay, t)); - - ioc.run(); - - BOOST_TEST_EQ(cancel_count, 3u); - BOOST_TEST(w1); - BOOST_TEST(w2); - BOOST_TEST(w3); - } - - void testCancelReturnsZeroNoWaiters() - { - io_context ioc(Backend); - timer t(ioc); - - t.expires_after(std::chrono::seconds(60)); - auto n = t.cancel(); - BOOST_TEST_EQ(n, 0u); - } - - void testExpiresAtReturnsCount() - { - io_context ioc(Backend); - timer t(ioc); - - bool w1 = false, w2 = false; - std::error_code ec1, ec2; - - t.expires_after(std::chrono::seconds(60)); - - auto wait_task = [](timer& t_ref, std::error_code& ec_out, - bool& done) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done = true; - }; - - capy::run_async(ioc.get_executor())(wait_task(t, ec1, w1)); - capy::run_async(ioc.get_executor())(wait_task(t, ec2, w2)); - - // Drain so both waiters suspend on t.wait() and register. - while (ioc.poll() > 0) - ; - - auto expires_count = t.expires_at( - timer::clock_type::now() + std::chrono::seconds(30)); - - // Drain so the canceled completions reach the coroutines. - while (ioc.poll() > 0) - ; - - BOOST_TEST_EQ(expires_count, 2u); - BOOST_TEST(w1); - BOOST_TEST(w2); - BOOST_TEST(ec1 == capy::cond::canceled); - BOOST_TEST(ec2 == capy::cond::canceled); - } - - void testExpiresAfterReturnsCount() - { - io_context ioc(Backend); - timer t(ioc); - - bool w1 = false, w2 = false; - std::error_code ec1, ec2; - - t.expires_after(std::chrono::seconds(60)); - - auto wait_task = [](timer& t_ref, std::error_code& ec_out, - bool& done) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done = true; - }; - - capy::run_async(ioc.get_executor())(wait_task(t, ec1, w1)); - capy::run_async(ioc.get_executor())(wait_task(t, ec2, w2)); - - // Drain so both waiters suspend on t.wait() and register. - while (ioc.poll() > 0) - ; - - auto expires_count = t.expires_after(std::chrono::seconds(30)); - - // Drain so the canceled completions reach the coroutines. - while (ioc.poll() > 0) - ; - - BOOST_TEST_EQ(expires_count, 2u); - BOOST_TEST(w1); - BOOST_TEST(w2); - BOOST_TEST(ec1 == capy::cond::canceled); - BOOST_TEST(ec2 == capy::cond::canceled); - } - - // Sequential wait tests - - void testSequentialWaits() - { - io_context ioc(Backend); - timer t(ioc); - - int wait_count = 0; - - auto task = [](timer& t_ref, int& count_out) -> capy::task<> { - t_ref.expires_after(std::chrono::milliseconds(5)); - auto [ec1] = co_await t_ref.wait(); - BOOST_TEST(!ec1); - ++count_out; - - t_ref.expires_after(std::chrono::milliseconds(5)); - auto [ec2] = co_await t_ref.wait(); - BOOST_TEST(!ec2); - ++count_out; - - t_ref.expires_after(std::chrono::milliseconds(5)); - auto [ec3] = co_await t_ref.wait(); - BOOST_TEST(!ec3); - ++count_out; - }; - capy::run_async(ioc.get_executor())(task(t, wait_count)); - - ioc.run(); - BOOST_TEST_EQ(wait_count, 3); - } - - // io_result tests - - void testIoResultSuccess() - { - io_context ioc(Backend); - timer t(ioc); - - bool result_ok = false; - - t.expires_after(std::chrono::milliseconds(5)); - - auto task = [](timer& t_ref, bool& ok_out) -> capy::task<> { - auto result = co_await t_ref.wait(); - ok_out = !result.ec; - }; - capy::run_async(ioc.get_executor())(task(t, result_ok)); - - ioc.run(); - BOOST_TEST(result_ok); - } - - void testIoResultCanceled() - { - io_context ioc(Backend); - timer t(ioc); - timer cancel_timer(ioc); - - bool result_ok = true; - std::error_code result_ec; - - t.expires_after(std::chrono::seconds(60)); - cancel_timer.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, bool& ok_out, - std::error_code& ec_out) -> capy::task<> { - auto result = co_await t_ref.wait(); - ok_out = !result.ec; - ec_out = result.ec; - }; - capy::run_async(ioc.get_executor())(wait_task(t, result_ok, result_ec)); - - auto cancel_task = [](timer& cancel_t_ref, - timer& t_ref) -> capy::task<> { - (void)co_await cancel_t_ref.wait(); - t_ref.cancel(); - }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, t)); - - ioc.run(); - BOOST_TEST(!result_ok); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void testIoResultStructuredBinding() - { - io_context ioc(Backend); - timer t(ioc); - - std::error_code captured_ec; - - t.expires_after(std::chrono::milliseconds(5)); - - auto task = [](timer& t_ref, std::error_code& ec_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - }; - capy::run_async(ioc.get_executor())(task(t, captured_ec)); - - ioc.run(); - BOOST_TEST(!captured_ec); - } - - // Shutdown cleanup - - void testShutdownDestroysTimerWaiters() - { - bool started = false; - bool destroyed = false; - - { - io_context ioc(Backend); - timer t(ioc); - t.expires_after(std::chrono::seconds(3600)); - - auto task = [](timer& t_ref, bool& started_flag, - bool& destroyed_flag) -> capy::task<> { - struct guard - { - bool& flag_; - ~guard() - { - flag_ = true; - } - }; - guard g{destroyed_flag}; - started_flag = true; - auto [ec] = co_await t_ref.wait(); - (void)ec; - }; - - capy::run_async(ioc.get_executor())(task(t, started, destroyed)); - ioc.poll(); - - BOOST_TEST(started); - // io_context destructor triggers shutdown - } - - BOOST_TEST(destroyed); - } - - void testShutdownDrainsHeapWaiters() - { - // Exercises timer_service::shutdown()'s waiter drain loop. - // Normally the timer destructs before io_context, cancelling - // waiters via cancel_timer(). Here we use placement-new so the - // timer outlives io_context — its destructor is skipped, leaving - // waiters in the heap for shutdown() to drain. - int destroyed = 0; - - { - io_context ioc(Backend); - - alignas(timer) unsigned char buf[sizeof(timer)]; - auto* t = new (buf) timer(ioc); - t->expires_after(std::chrono::seconds(3600)); - - auto task = [](timer& t_ref, int& counter) -> capy::task<> { - struct guard - { - int& c_; - ~guard() - { - ++c_; - } - }; - guard g{counter}; - auto [ec] = co_await t_ref.wait(); - (void)ec; - }; - - capy::run_async(ioc.get_executor())(task(*t, destroyed)); - ioc.poll(); - - // io_context destructs. Timer t is still alive in buf. - // timer_service::shutdown() finds the waiter in the heap - // and drains it, destroying the coroutine frame. - // Timer destructor is intentionally skipped (placement-new). - } - - BOOST_TEST_EQ(destroyed, 1); - } - - void testAbruptStopWithPendingTimerOps() - { - bool waiter_started = false; - - { - io_context ioc(Backend); - timer t1(ioc); - timer t2(ioc); - timer t3(ioc); - - t1.expires_after(std::chrono::hours(1)); - t2.expires_after(std::chrono::hours(1)); - t3.expires_after(std::chrono::hours(1)); - - auto waiter = [](timer& t, bool& started) -> capy::task<> { - started = true; - auto [ec] = co_await t.wait(); - (void)ec; - }; - - auto stopper = [](io_context& ctx) -> capy::task<> { - ctx.stop(); - co_return; - }; - - capy::run_async(ioc.get_executor())(waiter(t1, waiter_started)); - capy::run_async(ioc.get_executor())(waiter(t2, waiter_started)); - capy::run_async(ioc.get_executor())(waiter(t3, waiter_started)); - capy::run_async(ioc.get_executor())(stopper(ioc)); - - ioc.run(); - - BOOST_TEST(waiter_started); - // io_context destructs with 3 pending timer waiters - } - // Shutdown completes without hanging - BOOST_TEST_PASS(); - } - - void testShutdownWithTimerOwnedByCoroutine() - { - // Reproduces UB: timer_service::shutdown() iterates heap_, - // calls h.destroy() on a waiter. The coroutine frame owns - // a timer whose destructor re-enters destroy_impl() → - // cancel_timer() → remove_timer_impl(), modifying heap_ - // during the range-for iteration. - // - // All timers must be owned by coroutines (not on the stack) - // so that their heap entries survive until shutdown(). - int destroyed = 0; - - { - io_context ioc(Backend); - - auto owning_task = [](io_context& ctx, - int& counter) -> capy::task<> { - struct guard - { - int& c_; - ~guard() - { - ++c_; - } - }; - guard g{counter}; - timer t(ctx); - t.expires_after(std::chrono::hours(1)); - auto [ec] = co_await t.wait(); - (void)ec; - }; - - auto stopper = [](io_context& ctx) -> capy::task<> { - ctx.stop(); - co_return; - }; - - capy::run_async(ioc.get_executor())(owning_task(ioc, destroyed)); - capy::run_async(ioc.get_executor())(owning_task(ioc, destroyed)); - capy::run_async(ioc.get_executor())(owning_task(ioc, destroyed)); - capy::run_async(ioc.get_executor())(stopper(ioc)); - - ioc.run(); - // ~io_context → timer_service::shutdown() iterates heap_ - // with 3 entries. Destroying the first coroutine handle - // triggers its timer destructor which removes an entry - // via remove_timer_impl(), corrupting the iteration. - } - - BOOST_TEST_EQ(destroyed, 3); - } - - // Edge cases - - void testLongDuration() - { - io_context ioc(Backend); - timer t(ioc); - - t.expires_after(std::chrono::hours(24 * 365)); - - auto expiry = t.expiry(); - BOOST_TEST(expiry > timer::clock_type::now()); - - t.cancel(); - BOOST_TEST_PASS(); - } - - void testNegativeDuration() - { - io_context ioc(Backend); - timer t(ioc); - - bool completed = false; - - t.expires_after(std::chrono::milliseconds(-100)); - - auto task = [](timer& t_ref, bool& done_out) -> capy::task<> { - auto [ec] = co_await t_ref.wait(); - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t, completed)); - - ioc.run(); - BOOST_TEST(completed); - } - - // Type trait tests - - void testTypeAliases() - { - static_assert( - std::is_same_v); - - static_assert( - std::is_same_v< - timer::time_point, std::chrono::steady_clock::time_point>); - - static_assert(std::is_same_v< - timer::duration, std::chrono::steady_clock::duration>); - - BOOST_TEST_PASS(); - } - - void run() - { - // Construction and move semantics - testConstruction(); - testNonIoContextThrows(); - testConstructFromExecutor(); - testConstructFromExecutorWithTimePoint(); - testConstructFromExecutorWithDuration(); - testExecutorNonIoContextThrows(); - testConstructionWithTimePoint(); - testConstructionWithDuration(); - testMoveConstruct(); - testMoveAssign(); - testMoveAssignCrossContext(); - - // Expiry setting and retrieval - testDefaultExpiry(); - testExpiresAfter(); - testExpiresAfterDifferentDurations(); - testExpiresAt(); - testExpiresAtPast(); - testExpiresAtReplace(); - - // Async wait tests - testWaitBasic(); - testWaitTimingAccuracy(); - testWaitExpiredTimer(); - testWaitZeroDuration(); - - // Cancellation tests - testCancel(); - testCancelNoWaiters(); - testCancelMultipleTimes(); - testExpiresAtCancelsWaiter(); - testStopTokenCancellation(); - - // Multiple timer tests - testMultipleTimersDifferentExpiry(); - testMultipleTimersSameExpiry(); - testReheapifyOnExpiresAtUpdate(); - testTimerFreeListReuseAcrossContexts(); - - // Multiple waiters on one timer - testMultipleWaiters(); - testMultipleWaitersCancelAll(); - testMultipleWaitersStopTokenCancelsOne(); - - // Destruction cancels pending waiters - testDestructionCancelsPendingWaiters(); - - // cancel_one() tests - testCancelOne(); - testCancelOneNoWaiters(); - - // Return value tests - testCancelReturnsCount(); - testCancelReturnsZeroNoWaiters(); - testExpiresAtReturnsCount(); - testExpiresAfterReturnsCount(); - - // Sequential wait tests - testSequentialWaits(); - - // io_result tests - testIoResultSuccess(); - testIoResultCanceled(); - testIoResultStructuredBinding(); - - // Shutdown cleanup - testShutdownDestroysTimerWaiters(); - testShutdownDrainsHeapWaiters(); - testAbruptStopWithPendingTimerOps(); - testShutdownWithTimerOwnedByCoroutine(); - - // Edge cases - testLongDuration(); - testNegativeDuration(); - - // Type trait tests - testTypeAliases(); - } -}; - -COROSIO_BACKEND_TESTS(timer_test, "boost.corosio.timer") - -} // namespace boost::corosio diff --git a/test/unit/tls_stream_stress.cpp b/test/unit/tls_stream_stress.cpp index 503a6e777..13b6094b6 100644 --- a/test/unit/tls_stream_stress.cpp +++ b/test/unit/tls_stream_stress.cpp @@ -18,9 +18,9 @@ // // Tests run for a configurable duration (default 1 second). +#include #include #include -#include #include #include #include @@ -278,9 +278,7 @@ struct tls_concurrent_io_stress_impl // Stopper: wait for duration then close all sockets auto stopper = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::seconds(duration)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::seconds(duration)); stop_flag.store(true, std::memory_order_relaxed); sa1.close(); @@ -342,17 +340,16 @@ struct tls_cancel_handshake_stress_impl bool done = false; // Failsafe to prevent hangs - timer failsafe(ioc); - failsafe.expires_after(std::chrono::milliseconds(2000)); + std::stop_source failsafe_stop; // Client handshake - will be cancelled mid-flight auto client_task = [&client, &client_got_error, &done, - &failsafe]() -> capy::task<> { + &failsafe_stop]() -> capy::task<> { auto [ec] = co_await client.handshake(tls_stream::client); if (ec) client_got_error = true; done = true; - failsafe.cancel(); + failsafe_stop.request_stop(); }; // Server: wait for ClientHello then trigger cancellation @@ -363,9 +360,9 @@ struct tls_cancel_handshake_stress_impl }; bool failsafe_hit = false; - auto failsafe_task = [&failsafe, &failsafe_hit, &s1, - &s2]() -> capy::task<> { - auto [ec] = co_await failsafe.wait(); + auto failsafe_task = [&failsafe_hit, &s1, &s2]() -> capy::task<> { + auto [ec] = co_await corosio::delay( + std::chrono::milliseconds(2000)); if (!ec) { failsafe_hit = true; @@ -384,7 +381,7 @@ struct tls_cancel_handshake_stress_impl capy::run_async(ex, stop_src.get_token())(client_task()); capy::run_async(ex)(server_task()); - capy::run_async(ex)(failsafe_task()); + capy::run_async(ex, failsafe_stop.get_token())(failsafe_task()); ioc.run(); ioc.restart(); diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index 6b213cedb..d0471aaca 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -13,6 +13,7 @@ #include "test_utils.hpp" #include "stream_tests.hpp" +#include #include #include #include @@ -22,6 +23,7 @@ #include #include +#include #include "test_suite.hpp" @@ -831,25 +833,24 @@ testEncryptedKey(StreamFactory make_stream, bool expect_success = true) // If the TLS build cannot decrypt the key the handshake stalls // with both sides waiting; tear the transport down instead of // hanging the suite. - timer failsafe(ioc); - failsafe.expires_after(std::chrono::seconds(5)); + std::stop_source failsafe_stop; auto client_hs = [&]() -> capy::task<> { auto [ec] = co_await client.handshake(stream_type::client); client_ec = ec; client_done = true; if (server_done) - failsafe.cancel(); + failsafe_stop.request_stop(); }; auto server_hs = [&]() -> capy::task<> { auto [ec] = co_await server.handshake(stream_type::server); server_ec = ec; server_done = true; if (client_done) - failsafe.cancel(); + failsafe_stop.request_stop(); }; auto failsafe_task = [&]() -> capy::task<> { - auto [ec] = co_await failsafe.wait(); + auto [ec] = co_await corosio::delay(std::chrono::seconds(5)); if (!ec) { failsafe_hit = true; @@ -859,7 +860,8 @@ testEncryptedKey(StreamFactory make_stream, bool expect_success = true) }; capy::run_async(ioc.get_executor())(client_hs()); capy::run_async(ioc.get_executor())(server_hs()); - capy::run_async(ioc.get_executor())(failsafe_task()); + capy::run_async(ioc.get_executor(), failsafe_stop.get_token())( + failsafe_task()); ioc.run(); BOOST_TEST(client_done); diff --git a/test/unit/udp_socket.cpp b/test/unit/udp_socket.cpp index 537a0541c..06b6bea7f 100644 --- a/test/unit/udp_socket.cpp +++ b/test/unit/udp_socket.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include @@ -514,9 +514,6 @@ struct udp_socket_test BOOST_TEST_EQ(ec, std::error_code{}); auto task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - bool recv_done = false; std::error_code recv_ec; @@ -530,12 +527,10 @@ struct udp_socket_test }; capy::run_async(ioc.get_executor())(nested()); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); sock.cancel(); - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(recv_done); BOOST_TEST(recv_ec == capy::cond::canceled); @@ -556,9 +551,6 @@ struct udp_socket_test BOOST_TEST_EQ(ec, std::error_code{}); auto task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - bool recv_done = false; std::error_code recv_ec; @@ -572,12 +564,10 @@ struct udp_socket_test }; capy::run_async(ioc.get_executor())(nested()); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); sock.close(); - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(recv_done); BOOST_TEST(recv_ec == capy::cond::canceled); @@ -636,9 +626,8 @@ struct udp_socket_test }; auto failsafe_task = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(1000)); - auto [ec] = co_await t.wait(); + auto [ec] = + co_await corosio::delay(std::chrono::milliseconds(1000)); if (!ec && !recv_done) { failsafe_hit = true; @@ -719,9 +708,7 @@ struct udp_socket_test BOOST_TEST_EQ(ec3, std::error_code{}); // Wait for recv to complete - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(recv_done); BOOST_TEST_EQ(recv_ec, std::error_code{}); @@ -992,14 +979,10 @@ struct udp_socket_test }; capy::run_async(ioc.get_executor())(nested()); - timer t(ioc); - t.expires_after(std::chrono::milliseconds(50)); - (void)co_await t.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); a.cancel(); - timer t2(ioc); - t2.expires_after(std::chrono::milliseconds(50)); - (void)co_await t2.wait(); + (void)co_await corosio::delay(std::chrono::milliseconds(50)); BOOST_TEST(recv_done); BOOST_TEST(recv_ec == capy::cond::canceled); diff --git a/test/unit/wait.cpp b/test/unit/wait.cpp index 37fe38fa0..d7c39d851 100644 --- a/test/unit/wait.cpp +++ b/test/unit/wait.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Michael Vandeberg +// Copyright (c) 2026 Steve Gerbino // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -10,6 +11,7 @@ // Test that header is self-contained. #include +#include #include #include #include @@ -17,7 +19,6 @@ #include #include #include -#include #include #include @@ -25,7 +26,6 @@ #include #include -#include #include #include @@ -85,29 +85,50 @@ struct wait_test BOOST_TEST_EQ(bytes_read, payload.size()); } - // wait_type::write completes immediately on a connected socket. - // Corosio matches asio's IOCP behavior: writability is always - // treated as ready, the wait does not park on a edge transition. - void testWaitWriteImmediate() + // Acceptor wait_read fires when a client connects; accept then succeeds. + void testAcceptorWait() { io_context ioc(Backend); auto ex = ioc.get_executor(); - auto [s1, s2] = test::make_socket_pair(ioc); + + tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + auto bec = acc.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + auto lec = acc.listen(); + BOOST_TEST(!lec); + auto port = acc.local_endpoint().port(); std::error_code wait_ec; bool wait_done = false; + std::error_code accept_ec; + tcp_socket peer(ioc); + tcp_socket client(ioc); - auto writer = [&]() -> capy::task<> { - auto [ec] = co_await s1.wait(wait_type::write); - wait_ec = ec; - wait_done = true; + auto waiter = [&]() -> capy::task<> { + auto [ec1] = co_await acc.wait(wait_type::read); + wait_ec = ec1; + wait_done = true; + if (ec1) + co_return; + auto [ec2] = co_await acc.accept(peer); + accept_ec = ec2; + }; + auto connector = [&]() -> capy::task<> { + auto [ec] = co_await client.connect( + endpoint(ipv4_address::loopback(), port)); + (void)ec; }; - capy::run_async(ex)(writer()); + capy::run_async(ex)(waiter()); + capy::run_async(ex)(connector()); ioc.run(); BOOST_TEST(wait_done); BOOST_TEST(!wait_ec); + BOOST_TEST(!accept_ec); + BOOST_TEST(peer.is_open()); } // local_stream_socket wait_read fires when the peer writes. @@ -182,9 +203,7 @@ struct wait_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await delay(std::chrono::milliseconds(20)); s1.cancel(); }; @@ -196,92 +215,6 @@ struct wait_test BOOST_TEST(wait_ec == capy::cond::canceled); } - // Acceptor wait_read fires when a client connects; accept then succeeds. - void testAcceptorWait() - { - io_context ioc(Backend); - auto ex = ioc.get_executor(); - - tcp_acceptor acc(ioc); - acc.open(); - acc.set_option(socket_option::reuse_address(true)); - auto bec = acc.bind(endpoint(ipv4_address::loopback(), 0)); - BOOST_TEST(!bec); - auto lec = acc.listen(); - BOOST_TEST(!lec); - auto port = acc.local_endpoint().port(); - - std::error_code wait_ec; - bool wait_done = false; - std::error_code accept_ec; - tcp_socket peer(ioc); - tcp_socket client(ioc); - - auto waiter = [&]() -> capy::task<> { - auto [ec1] = co_await acc.wait(wait_type::read); - wait_ec = ec1; - wait_done = true; - if (ec1) - co_return; - auto [ec2] = co_await acc.accept(peer); - accept_ec = ec2; - }; - auto connector = [&]() -> capy::task<> { - auto [ec] = co_await client.connect( - endpoint(ipv4_address::loopback(), port)); - (void)ec; - }; - - capy::run_async(ex)(waiter()); - capy::run_async(ex)(connector()); - ioc.run(); - - BOOST_TEST(wait_done); - BOOST_TEST(!wait_ec); - BOOST_TEST(!accept_ec); - BOOST_TEST(peer.is_open()); - } - - // UDP socket wait_read completes when a datagram arrives. - void testWaitOnUdp() - { - io_context ioc(Backend); - auto ex = ioc.get_executor(); - - udp_socket recv(ioc); - recv.open(udp::v4()); - auto bec = recv.bind(endpoint(ipv4_address::loopback(), 0)); - BOOST_TEST(!bec); - auto port = recv.local_endpoint().port(); - - udp_socket send(ioc); - send.open(udp::v4()); - - std::error_code wait_ec; - bool wait_done = false; - - auto waiter = [&]() -> capy::task<> { - auto [ec] = co_await recv.wait(wait_type::read); - wait_ec = ec; - wait_done = true; - }; - auto sender = [&]() -> capy::task<> { - char dg[1] = { 'X' }; - auto [ec, n] = co_await send.send_to( - capy::const_buffer(dg, sizeof(dg)), - endpoint(ipv4_address::loopback(), port)); - (void)ec; - (void)n; - }; - - capy::run_async(ex)(waiter()); - capy::run_async(ex)(sender()); - ioc.run(); - - BOOST_TEST(wait_done); - BOOST_TEST(!wait_ec); - } - // Cancel a UDP wait_read while it's parked. On IOCP this exercises // the auxiliary WSAPoll reactor's cancel_wait path, where the op // has no overlapped I/O pending so CancelIoEx is a no-op and the @@ -305,9 +238,7 @@ struct wait_test wait_done = true; }; auto canceller = [&]() -> capy::task<> { - timer t(ioc); - t.expires_after(std::chrono::milliseconds(20)); - (void)co_await t.wait(); + (void)co_await delay(std::chrono::milliseconds(20)); sock.cancel(); }; @@ -322,11 +253,9 @@ struct wait_test void run() { testWaitReadAndNoConsume(); - testWaitWriteImmediate(); + testAcceptorWait(); testWaitOnLocalStream(); testCancellation(); - testAcceptorWait(); - testWaitOnUdp(); testUdpCancellation(); } }; From 97ec7e59298a30b9b0477387d98ef4d237ae808b Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 9 Jul 2026 01:35:47 +0200 Subject: [PATCH 2/2] ci: resolve the capy checkout across forks The capy branch resolution only consulted cppalliance/capy, so a branch pushed to a fork of corosio could never find its matching capy branch on the same owner's capy fork and silently fell back to capy's default branch, building against the wrong API. Try the repository owner's capy fork first for each branch candidate, then cppalliance/capy, and carry the resolved repository into the checkout step. Runs in cppalliance/corosio behave exactly as before. --- .github/workflows/ci.yml | 62 ++++++++++++++++++++--------- .github/workflows/code-coverage.yml | 62 ++++++++++++++++++++--------- .github/workflows/docs.yml | 31 ++++++++++----- 3 files changed, 110 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b62244a28..f108394b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,29 +96,42 @@ jobs: curl zip unzip tar pkg-config # Use the capy branch with the same name as this build's branch - # (for pull requests: the source branch, then the target branch); - # fall back to capy's default branch. + # (for pull requests: the source branch, then the target branch). + # Inside a fork (a push or a PR within the fork) the owner's capy + # fork is preferred, so fork branches build against their matching + # capy fork branch. Cross-repo PRs into cppalliance resolve against + # cppalliance/capy as before. Fall back to capy's default branch. - name: Resolve Capy branch id: capy-ref shell: bash run: | - url=https://github.com/cppalliance/capy.git + repo=cppalliance/capy ref= for candidate in "${{ github.head_ref }}" "${{ github.base_ref }}" "${{ github.ref_name }}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads "$url" "$candidate" > /dev/null; then - ref="$candidate" - break - fi + [ -n "$candidate" ] || continue + probed= + for fork in "${{ github.repository_owner }}/capy" cppalliance/capy; do + # Skip a fork already probed for this candidate (the owner + # fork equals cppalliance/capy when running in cppalliance). + [ "$fork" = "$probed" ] && continue + probed="$fork" + if GIT_TERMINAL_PROMPT=0 git ls-remote --exit-code --heads "https://github.com/$fork.git" "$candidate" > /dev/null; then + repo="$fork" + ref="$candidate" + break 2 + fi + done done if [ -z "$ref" ]; then - ref=$(git ls-remote --symref "$url" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') + ref=$(GIT_TERMINAL_PROMPT=0 git ls-remote --symref "https://github.com/$repo.git" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') fi + echo "repo=$repo" >> "$GITHUB_OUTPUT" echo "ref=$ref" >> "$GITHUB_OUTPUT" - name: Clone Capy uses: actions/checkout@v4 with: - repository: cppalliance/capy + repository: ${{ steps.capy-ref.outputs.repo }} ref: ${{ steps.capy-ref.outputs.ref }} path: capy-root @@ -800,29 +813,42 @@ jobs: path: corosio-root # Use the capy branch with the same name as this build's branch - # (for pull requests: the source branch, then the target branch); - # fall back to capy's default branch. + # (for pull requests: the source branch, then the target branch). + # Inside a fork (a push or a PR within the fork) the owner's capy + # fork is preferred, so fork branches build against their matching + # capy fork branch. Cross-repo PRs into cppalliance resolve against + # cppalliance/capy as before. Fall back to capy's default branch. - name: Resolve Capy branch id: capy-ref shell: bash run: | - url=https://github.com/cppalliance/capy.git + repo=cppalliance/capy ref= for candidate in "${{ github.head_ref }}" "${{ github.base_ref }}" "${{ github.ref_name }}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads "$url" "$candidate" > /dev/null; then - ref="$candidate" - break - fi + [ -n "$candidate" ] || continue + probed= + for fork in "${{ github.repository_owner }}/capy" cppalliance/capy; do + # Skip a fork already probed for this candidate (the owner + # fork equals cppalliance/capy when running in cppalliance). + [ "$fork" = "$probed" ] && continue + probed="$fork" + if GIT_TERMINAL_PROMPT=0 git ls-remote --exit-code --heads "https://github.com/$fork.git" "$candidate" > /dev/null; then + repo="$fork" + ref="$candidate" + break 2 + fi + done done if [ -z "$ref" ]; then - ref=$(git ls-remote --symref "$url" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') + ref=$(GIT_TERMINAL_PROMPT=0 git ls-remote --symref "https://github.com/$repo.git" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') fi + echo "repo=$repo" >> "$GITHUB_OUTPUT" echo "ref=$ref" >> "$GITHUB_OUTPUT" - name: Clone Capy uses: actions/checkout@v4 with: - repository: cppalliance/capy + repository: ${{ steps.capy-ref.outputs.repo }} ref: ${{ steps.capy-ref.outputs.ref }} path: capy-root diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 4c07b6f4e..53143cf2b 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -151,29 +151,42 @@ jobs: echo "CMAKE_WOLFSSL_LIBRARY=${vcpkg_installed}/lib/libwolfssl.a" >> $GITHUB_ENV # Use the capy branch with the same name as this build's branch - # (for pull requests: the source branch, then the target branch); - # fall back to capy's default branch. + # (for pull requests: the source branch, then the target branch). + # Inside a fork (a push or a PR within the fork) the owner's capy + # fork is preferred, so fork branches build against their matching + # capy fork branch. Cross-repo PRs into cppalliance resolve against + # cppalliance/capy as before. Fall back to capy's default branch. - name: Resolve Capy branch id: capy-ref shell: bash run: | - url=https://github.com/cppalliance/capy.git + repo=cppalliance/capy ref= for candidate in "${{ github.head_ref }}" "${{ github.base_ref }}" "${{ github.ref_name }}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads "$url" "$candidate" > /dev/null; then - ref="$candidate" - break - fi + [ -n "$candidate" ] || continue + probed= + for fork in "${{ github.repository_owner }}/capy" cppalliance/capy; do + # Skip a fork already probed for this candidate (the owner + # fork equals cppalliance/capy when running in cppalliance). + [ "$fork" = "$probed" ] && continue + probed="$fork" + if GIT_TERMINAL_PROMPT=0 git ls-remote --exit-code --heads "https://github.com/$fork.git" "$candidate" > /dev/null; then + repo="$fork" + ref="$candidate" + break 2 + fi + done done if [ -z "$ref" ]; then - ref=$(git ls-remote --symref "$url" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') + ref=$(GIT_TERMINAL_PROMPT=0 git ls-remote --symref "https://github.com/$repo.git" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') fi + echo "repo=$repo" >> "$GITHUB_OUTPUT" echo "ref=$ref" >> "$GITHUB_OUTPUT" - name: Clone Capy uses: actions/checkout@v6 with: - repository: cppalliance/capy + repository: ${{ steps.capy-ref.outputs.repo }} ref: ${{ steps.capy-ref.outputs.ref }} path: capy-root @@ -392,29 +405,42 @@ jobs: echo "CMAKE_OPENSSL_ROOT=C:/msys64/mingw64" >> $GITHUB_ENV # Use the capy branch with the same name as this build's branch - # (for pull requests: the source branch, then the target branch); - # fall back to capy's default branch. + # (for pull requests: the source branch, then the target branch). + # Inside a fork (a push or a PR within the fork) the owner's capy + # fork is preferred, so fork branches build against their matching + # capy fork branch. Cross-repo PRs into cppalliance resolve against + # cppalliance/capy as before. Fall back to capy's default branch. - name: Resolve Capy branch id: capy-ref shell: bash run: | - url=https://github.com/cppalliance/capy.git + repo=cppalliance/capy ref= for candidate in "${{ github.head_ref }}" "${{ github.base_ref }}" "${{ github.ref_name }}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads "$url" "$candidate" > /dev/null; then - ref="$candidate" - break - fi + [ -n "$candidate" ] || continue + probed= + for fork in "${{ github.repository_owner }}/capy" cppalliance/capy; do + # Skip a fork already probed for this candidate (the owner + # fork equals cppalliance/capy when running in cppalliance). + [ "$fork" = "$probed" ] && continue + probed="$fork" + if GIT_TERMINAL_PROMPT=0 git ls-remote --exit-code --heads "https://github.com/$fork.git" "$candidate" > /dev/null; then + repo="$fork" + ref="$candidate" + break 2 + fi + done done if [ -z "$ref" ]; then - ref=$(git ls-remote --symref "$url" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') + ref=$(GIT_TERMINAL_PROMPT=0 git ls-remote --symref "https://github.com/$repo.git" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') fi + echo "repo=$repo" >> "$GITHUB_OUTPUT" echo "ref=$ref" >> "$GITHUB_OUTPUT" - name: Clone Capy uses: actions/checkout@v6 with: - repository: cppalliance/capy + repository: ${{ steps.capy-ref.outputs.repo }} ref: ${{ steps.capy-ref.outputs.ref }} path: capy-root diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f9df35b41..837f9a784 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,29 +43,42 @@ jobs: path: corosio-root # Use the capy branch with the same name as this build's branch - # (for pull requests: the source branch, then the target branch); - # fall back to capy's default branch. + # (for pull requests: the source branch, then the target branch). + # Inside a fork (a push or a PR within the fork) the owner's capy + # fork is preferred, so fork branches build against their matching + # capy fork branch. Cross-repo PRs into cppalliance resolve against + # cppalliance/capy as before. Fall back to capy's default branch. - name: Resolve Capy branch id: capy-ref shell: bash run: | - url=https://github.com/cppalliance/capy.git + repo=cppalliance/capy ref= for candidate in "${{ github.head_ref }}" "${{ github.base_ref }}" "${{ github.ref_name }}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads "$url" "$candidate" > /dev/null; then - ref="$candidate" - break - fi + [ -n "$candidate" ] || continue + probed= + for fork in "${{ github.repository_owner }}/capy" cppalliance/capy; do + # Skip a fork already probed for this candidate (the owner + # fork equals cppalliance/capy when running in cppalliance). + [ "$fork" = "$probed" ] && continue + probed="$fork" + if GIT_TERMINAL_PROMPT=0 git ls-remote --exit-code --heads "https://github.com/$fork.git" "$candidate" > /dev/null; then + repo="$fork" + ref="$candidate" + break 2 + fi + done done if [ -z "$ref" ]; then - ref=$(git ls-remote --symref "$url" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') + ref=$(GIT_TERMINAL_PROMPT=0 git ls-remote --symref "https://github.com/$repo.git" HEAD | awk '/^ref:/ { sub("refs/heads/", "", $2); print $2 }') fi + echo "repo=$repo" >> "$GITHUB_OUTPUT" echo "ref=$ref" >> "$GITHUB_OUTPUT" - name: Clone Capy uses: actions/checkout@v4 with: - repository: cppalliance/capy + repository: ${{ steps.capy-ref.outputs.repo }} ref: ${{ steps.capy-ref.outputs.ref }} path: capy-root