include/boost/corosio/tcp_server.hpp

98.0% Lines (145/149) 97.6% List of functions (40/41) 88.6% Branches (31/35)
tcp_server.hpp
f(x) Functions (41)
Function Calls Lines Branches Blocks
boost::corosio::tcp_server::idle_push(boost::corosio::tcp_server::worker_base*) :124 88x 100.0% 100.0% boost::corosio::tcp_server::idle_pop() :130 20x 100.0% 50.0% 100.0% boost::corosio::tcp_server::idle_empty() const :138 24x 100.0% 100.0% boost::corosio::tcp_server::active_push(boost::corosio::tcp_server::worker_base*) :144 9x 100.0% 100.0% 100.0% boost::corosio::tcp_server::active_remove(boost::corosio::tcp_server::worker_base*) :155 24x 100.0% 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::promise_type<boost::corosio::tcp_server::launch_coro<boost::corosio::io_context::executor_type>&, boost::corosio::io_context::executor_type, std::stop_token, boost::corosio::tcp_server*&, boost::capy::task<void>&, boost::corosio::tcp_server::worker_base*&>(boost::corosio::tcp_server::launch_coro<boost::corosio::io_context::executor_type>&, boost::corosio::io_context::executor_type, std::stop_token, boost::corosio::tcp_server*&, boost::capy::task<void>&, boost::corosio::tcp_server::worker_base*&) :194 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::get_return_object() :202 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::initial_suspend() :207 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::final_suspend() :211 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::return_void() :215 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::unhandled_exception() :216 0 0.0% 0.0% auto boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::capy::task<void> >(boost::capy::task<void>&&) :226 9x 100.0% 100.0% auto boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::corosio::tcp_server::push_awaitable>(boost::corosio::tcp_server::push_awaitable&&) :226 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::capy::task<void> >(boost::capy::task<void>&&)::adapter::await_ready() :234 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::corosio::tcp_server::push_awaitable>(boost::corosio::tcp_server::push_awaitable&&)::adapter::await_ready() :234 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::capy::task<void> >(boost::capy::task<void>&&)::adapter::await_resume() :238 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::corosio::tcp_server::push_awaitable>(boost::corosio::tcp_server::push_awaitable&&)::adapter::await_resume() :238 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::capy::task<void> >(boost::capy::task<void>&&)::adapter::await_suspend(std::__n4861::coroutine_handle<boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type>) :243 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type::await_transform<boost::corosio::tcp_server::push_awaitable>(boost::corosio::tcp_server::push_awaitable&&)::adapter::await_suspend(std::__n4861::coroutine_handle<boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type>) :243 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::launch_wrapper(std::__n4861::coroutine_handle<boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::promise_type>) :254 9x 100.0% 100.0% boost::corosio::tcp_server::launch_wrapper<boost::corosio::io_context::executor_type>::~launch_wrapper() :259 9x 75.0% 50.0% 75.0% boost::corosio::tcp_server::launch_coro<boost::corosio::io_context::executor_type>::operator()(boost::corosio::io_context::executor_type, std::stop_token, boost::corosio::tcp_server*, boost::capy::task<void>, boost::corosio::tcp_server::worker_base*) :279 9x 100.0% 100.0% 45.8% boost::corosio::tcp_server::push_awaitable::push_awaitable(boost::corosio::tcp_server&, boost::corosio::tcp_server::worker_base&) :299 20x 100.0% 100.0% boost::corosio::tcp_server::push_awaitable::await_ready() const :305 20x 100.0% 100.0% boost::corosio::tcp_server::push_awaitable::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*) :311 20x 100.0% 100.0% boost::corosio::tcp_server::push_awaitable::await_resume() :318 20x 100.0% 100.0% 100.0% boost::corosio::tcp_server::pop_awaitable::pop_awaitable(boost::corosio::tcp_server&) :344 24x 100.0% 100.0% boost::corosio::tcp_server::pop_awaitable::await_ready() const :346 24x 100.0% 100.0% boost::corosio::tcp_server::pop_awaitable::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*) :352 4x 100.0% 100.0% boost::corosio::tcp_server::pop_awaitable::await_resume() :362 24x 100.0% 100.0% 100.0% boost::corosio::tcp_server::push(boost::corosio::tcp_server::worker_base&) :371 20x 100.0% 100.0% boost::corosio::tcp_server::push_sync(boost::corosio::tcp_server::worker_base&) :378 4x 100.0% 100.0% 100.0% boost::corosio::tcp_server::pop() :395 24x 100.0% 100.0% boost::corosio::tcp_server::launcher::launcher(boost::corosio::tcp_server&, boost::corosio::tcp_server::worker_base&) :460 13x 100.0% 100.0% boost::corosio::tcp_server::launcher::~launcher() :466 14x 100.0% 100.0% 100.0% boost::corosio::tcp_server::launcher::launcher(boost::corosio::tcp_server::launcher&&) :472 1x 100.0% 100.0% void boost::corosio::tcp_server::launcher::operator()<boost::corosio::io_context::executor_type>(boost::corosio::io_context::executor_type const&, boost::capy::task<void>) :493 10x 100.0% 100.0% 57.9% boost::corosio::tcp_server::launcher::operator()<boost::corosio::io_context::executor_type>(boost::corosio::io_context::executor_type const&, boost::capy::task<void>)::guard_t::~guard_t() :508 9x 75.0% 50.0% 66.7% boost::corosio::tcp_server::tcp_server<boost::corosio::io_context, boost::corosio::io_context::executor_type>(boost::corosio::io_context&, boost::corosio::io_context::executor_type) :540 20x 100.0% 100.0% void boost::corosio::tcp_server::set_workers<std::vector<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> >, std::allocator<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> > > > >(std::vector<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> >, std::allocator<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> > > >&&) :599 20x 100.0% 100.0% 100.0% boost::corosio::tcp_server::set_workers<std::vector<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> >, std::allocator<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> > > > >(std::vector<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> >, std::allocator<std::unique_ptr<boost::corosio::tcp_server::worker_base, std::default_delete<boost::corosio::tcp_server::worker_base> > > >&&)::{lambda(void*)#1}::operator()(void*) const :611 20x 100.0% 50.0% 100.0%
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Vinnie Falco ([email protected])
3 // Copyright (c) 2026 Michael Vandeberg
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_TCP_SERVER_HPP
12 #define BOOST_COROSIO_TCP_SERVER_HPP
13
14 #include <boost/corosio/detail/config.hpp>
15 #include <boost/corosio/detail/except.hpp>
16 #include <boost/corosio/tcp_acceptor.hpp>
17 #include <boost/corosio/tcp_socket.hpp>
18 #include <boost/corosio/io_context.hpp>
19 #include <boost/corosio/endpoint.hpp>
20 #include <boost/capy/task.hpp>
21 #include <boost/capy/concept/execution_context.hpp>
22 #include <boost/capy/concept/io_awaitable.hpp>
23 #include <boost/capy/concept/executor.hpp>
24 #include <boost/capy/ex/any_executor.hpp>
25 #include <boost/capy/ex/frame_allocator.hpp>
26 #include <boost/capy/ex/io_env.hpp>
27 #include <boost/capy/ex/run_async.hpp>
28
29 #include <coroutine>
30 #include <memory>
31 #include <ranges>
32 #include <vector>
33
34 namespace boost::corosio {
35
36 #ifdef _MSC_VER
37 #pragma warning(push)
38 #pragma warning(disable : 4251) // class needs to have dll-interface
39 #endif
40
41 /** TCP server with pooled workers.
42
43 This class manages a pool of reusable worker objects that handle
44 incoming connections. When a connection arrives, an idle worker
45 is dispatched to handle it. After the connection completes, the
46 worker returns to the pool for reuse, avoiding allocation overhead
47 per connection.
48
49 Workers are set via @ref set_workers as a forward range of
50 pointer-like objects (e.g., `unique_ptr<worker_base>`). The server
51 takes ownership of the container via type erasure.
52
53 @par Thread Safety
54 Distinct objects: Safe.
55 Shared objects: Unsafe.
56
57 @par Lifecycle
58 The server operates in three states:
59
60 - **Stopped**: Initial state, or after @ref join completes.
61 - **Running**: After @ref start, actively accepting connections.
62 - **Stopping**: After @ref stop, draining active work.
63
64 State transitions:
65 @code
66 [Stopped] --start()--> [Running] --stop()--> [Stopping] --join()--> [Stopped]
67 @endcode
68
69 @par Running the Server
70 @par !example running_the_server
71
72 @par Graceful Shutdown
73 To shut down gracefully, call @ref stop then drain the io_context:
74 @par !example graceful_shutdown
75
76 @par Restart After Stop
77 The server can be restarted after a complete shutdown cycle.
78 You must drain the io_context, call @ref join, and restart the
79 io_context itself (`ioc.restart()`) before restarting:
80 @par !example restart_after_stop
81
82 @par WARNING: What NOT to Do
83 - Do NOT call @ref join from inside a worker coroutine (deadlock).
84 - Do NOT call @ref join from a thread running `ioc.run()` (deadlock).
85 - Do NOT call @ref start without completing @ref join after @ref stop.
86 - Do NOT call `ioc.stop()` for graceful shutdown; use @ref stop instead.
87
88 @par Example
89 @par !example custom_worker
90
91 @see worker_base, set_workers, launcher
92 */
93 class BOOST_COROSIO_DECL tcp_server
94 {
95 public:
96 class worker_base; ///< Abstract base for connection handlers.
97 class launcher; ///< Move-only handle to launch worker coroutines.
98
99 private:
100 struct waiter
101 {
102 waiter* next;
103 std::coroutine_handle<> h;
104 capy::continuation cont;
105 worker_base* w;
106 };
107
108 struct impl;
109
110 static impl* make_impl(capy::execution_context& ctx);
111
112 impl* impl_;
113 capy::any_executor ex_;
114 waiter* waiters_ = nullptr;
115 worker_base* idle_head_ = nullptr; // Forward list: available workers
116 worker_base* active_head_ =
117 nullptr; // Doubly linked: workers handling connections
118 worker_base* active_tail_ = nullptr; // Tail for O(1) push_back
119 std::size_t active_accepts_ = 0; // Number of active do_accept coroutines
120 std::shared_ptr<void> storage_; // Owns the worker container (type-erased)
121 bool running_ = false;
122
123 // Idle list (forward/singly linked) - push front, pop front
124 88x void idle_push(worker_base* w) noexcept
125 {
126 88x w->next_ = idle_head_;
127 88x idle_head_ = w;
128 88x }
129
130 20x worker_base* idle_pop() noexcept
131 {
132 20x auto* w = idle_head_;
133
1/2
✓ Branch 2 → 3 taken 20 times.
✗ Branch 2 → 4 not taken.
20x if (w)
134 20x idle_head_ = w->next_;
135 20x return w;
136 }
137
138 24x bool idle_empty() const noexcept
139 {
140 24x return idle_head_ == nullptr;
141 }
142
143 // Active list (doubly linked) - push back, remove anywhere
144 9x void active_push(worker_base* w) noexcept
145 {
146 9x w->next_ = nullptr;
147 9x w->prev_ = active_tail_;
148
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 7 times.
9x if (active_tail_)
149 2x active_tail_->next_ = w;
150 else
151 7x active_head_ = w;
152 9x active_tail_ = w;
153 9x }
154
155 24x void active_remove(worker_base* w) noexcept
156 {
157 // Skip if not in active list (e.g., after failed accept)
158
4/4
✓ Branch 2 → 3 taken 17 times.
✓ Branch 2 → 5 taken 7 times.
✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 5 taken 2 times.
24x if (w != active_head_ && w->prev_ == nullptr)
159 15x return;
160
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 7 times.
9x if (w->prev_)
161 2x w->prev_->next_ = w->next_;
162 else
163 7x active_head_ = w->next_;
164
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 8 times.
9x if (w->next_)
165 1x w->next_->prev_ = w->prev_;
166 else
167 8x active_tail_ = w->prev_;
168 9x w->prev_ = nullptr; // Mark as not in active list
169 }
170
171 template<capy::Executor Ex>
172 struct launch_wrapper
173 {
174 struct promise_type
175 {
176 Ex ex; // Executor stored directly in frame (outlives child tasks)
177 capy::io_env env_;
178
179 // For regular coroutines: first arg is executor, second is stop token
180 template<class E, class S, class... Args>
181 requires capy::Executor<std::decay_t<E>>
182 promise_type(E e, S s, Args&&...)
183 : ex(std::move(e))
184 , env_{
185 capy::executor_ref(ex), std::move(s),
186 capy::get_current_frame_allocator()}
187 {
188 }
189
190 // For lambda coroutines: first arg is closure, second is executor, third is stop token
191 template<class Closure, class E, class S, class... Args>
192 requires(!capy::Executor<std::decay_t<Closure>> &&
193 capy::Executor<std::decay_t<E>>)
194 9x promise_type(Closure&&, E e, S s, Args&&...)
195 9x : ex(std::move(e))
196 9x , env_{
197 9x capy::executor_ref(ex), std::move(s),
198 9x capy::get_current_frame_allocator()}
199 {
200 9x }
201
202 9x launch_wrapper get_return_object() noexcept
203 {
204 return {
205 9x std::coroutine_handle<promise_type>::from_promise(*this)};
206 }
207 9x std::suspend_always initial_suspend() noexcept
208 {
209 9x return {};
210 }
211 9x std::suspend_never final_suspend() noexcept
212 {
213 9x return {};
214 }
215 9x void return_void() noexcept {}
216 void unhandled_exception()
217 {
218 // LCOV_EXCL_START: terminating by contract is not a
219 // coverable outcome.
220 std::terminate();
221 // LCOV_EXCL_STOP
222 }
223
224 // Inject io_env for IoAwaitable
225 template<capy::IoAwaitable Awaitable>
226 18x auto await_transform(Awaitable&& a)
227 {
228 using AwaitableT = std::decay_t<Awaitable>;
229 struct adapter
230 {
231 AwaitableT aw;
232 capy::io_env const* env;
233
234 18x bool await_ready()
235 {
236 18x return aw.await_ready();
237 }
238 18x decltype(auto) await_resume()
239 {
240 18x return aw.await_resume();
241 }
242
243 18x auto await_suspend(std::coroutine_handle<promise_type> h)
244 {
245 18x return aw.await_suspend(h, env);
246 }
247 };
248 27x return adapter{std::forward<Awaitable>(a), &env_};
249 9x }
250 };
251
252 std::coroutine_handle<promise_type> h;
253
254 9x launch_wrapper(std::coroutine_handle<promise_type> handle) noexcept
255 9x : h(handle)
256 {
257 9x }
258
259 9x ~launch_wrapper()
260 {
261
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 9 times.
9x if (h)
262 h.destroy();
263 9x }
264
265 launch_wrapper(launch_wrapper&& o) noexcept
266 : h(std::exchange(o.h, nullptr))
267 {
268 }
269
270 launch_wrapper(launch_wrapper const&) = delete;
271 launch_wrapper& operator=(launch_wrapper const&) = delete;
272 launch_wrapper& operator=(launch_wrapper&&) = delete;
273 };
274
275 // Named functor to avoid incomplete lambda type in coroutine promise
276 template<class Executor>
277 struct launch_coro
278 {
279
1/1
✓ Branch 9 → 10 taken 9 times.
9x launch_wrapper<Executor> operator()(
280 Executor,
281 std::stop_token,
282 tcp_server* self,
283 capy::task<void> t,
284 worker_base* wp)
285 {
286 // Executor and stop token stored in promise via constructor
287 co_await std::move(t);
288 co_await self->push(*wp); // worker goes back to idle list
289 18x }
290 };
291
292 class push_awaitable
293 {
294 tcp_server& self_;
295 worker_base& w_;
296 capy::continuation cont_;
297
298 public:
299 20x push_awaitable(tcp_server& self, worker_base& w) noexcept
300 20x : self_(self)
301 20x , w_(w)
302 {
303 20x }
304
305 20x bool await_ready() const noexcept
306 {
307 20x return false;
308 }
309
310 std::coroutine_handle<>
311 20x await_suspend(std::coroutine_handle<> h, capy::io_env const*) noexcept
312 {
313 // Symmetric transfer to server's executor
314 20x cont_.h = h;
315 20x return self_.ex_.dispatch(cont_);
316 }
317
318 20x void await_resume() noexcept
319 {
320 // Running on server executor - safe to modify lists
321 // Remove from active (if present), then wake waiter or add to idle
322 20x self_.active_remove(&w_);
323
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 17 times.
20x if (self_.waiters_)
324 {
325 3x auto* wait = self_.waiters_;
326 3x self_.waiters_ = wait->next;
327 3x wait->w = &w_;
328 3x wait->cont.h = wait->h;
329 3x self_.ex_.post(wait->cont);
330 }
331 else
332 {
333 17x self_.idle_push(&w_);
334 }
335 20x }
336 };
337
338 class pop_awaitable
339 {
340 tcp_server& self_;
341 waiter wait_;
342
343 public:
344 24x pop_awaitable(tcp_server& self) noexcept : self_(self), wait_{} {}
345
346 24x bool await_ready() const noexcept
347 {
348 24x return !self_.idle_empty();
349 }
350
351 bool
352 4x await_suspend(std::coroutine_handle<> h, capy::io_env const*) noexcept
353 {
354 // Running on server executor (do_accept runs there)
355 4x wait_.h = h;
356 4x wait_.w = nullptr;
357 4x wait_.next = self_.waiters_;
358 4x self_.waiters_ = &wait_;
359 4x return true;
360 }
361
362 24x worker_base& await_resume() noexcept
363 {
364 // Running on server executor
365
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 20 times.
24x if (wait_.w)
366 4x return *wait_.w; // Woken by push_awaitable
367 20x return *self_.idle_pop();
368 }
369 };
370
371 20x push_awaitable push(worker_base& w)
372 {
373 20x return push_awaitable{*this, w};
374 }
375
376 // Synchronous version for destructor/guard paths
377 // Must be called from server executor context
378 4x void push_sync(worker_base& w) noexcept
379 {
380 4x active_remove(&w);
381
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 3 times.
4x if (waiters_)
382 {
383 1x auto* wait = waiters_;
384 1x waiters_ = wait->next;
385 1x wait->w = &w;
386 1x wait->cont.h = wait->h;
387 1x ex_.post(wait->cont);
388 }
389 else
390 {
391 3x idle_push(&w);
392 }
393 4x }
394
395 24x pop_awaitable pop()
396 {
397 24x return pop_awaitable{*this};
398 }
399
400 capy::task<void> do_accept(tcp_acceptor& acc);
401
402 public:
403 /** Abstract base class for connection handlers.
404
405 Derive from this class to implement custom connection handling.
406 Each worker owns a socket and is reused across multiple
407 connections to avoid per-connection allocation.
408
409 @see tcp_server, launcher
410 */
411 class BOOST_COROSIO_DECL worker_base
412 {
413 // Ordered largest to smallest for optimal packing
414 std::stop_source stop_; // ~16 bytes
415 worker_base* next_ = nullptr; // 8 bytes - used by idle and active lists
416 worker_base* prev_ = nullptr; // 8 bytes - used only by active list
417
418 friend class tcp_server;
419
420 public:
421 /// Construct a worker.
422 worker_base();
423
424 /// Destroy the worker.
425 virtual ~worker_base();
426
427 /** Handle an accepted connection.
428
429 Called when this worker is dispatched to handle a new
430 connection. The implementation must invoke the launcher
431 exactly once to start the handling coroutine.
432
433 @param launch Handle to launch the connection coroutine.
434 */
435 virtual void run(launcher launch) = 0;
436
437 /// Return the socket used for connections.
438 virtual corosio::tcp_socket& socket() = 0;
439 };
440
441 /** Move-only handle to launch a worker coroutine.
442
443 Passed to @ref worker_base::run to start the connection-handling
444 coroutine. The launcher ensures the worker returns to the idle
445 pool when the coroutine completes or if launching fails.
446
447 The launcher must be invoked exactly once via `operator()`.
448 If destroyed without invoking, the worker is returned to the
449 idle pool automatically.
450
451 @see worker_base::run
452 */
453 class BOOST_COROSIO_DECL launcher
454 {
455 tcp_server* srv_;
456 worker_base* w_;
457
458 friend class tcp_server;
459
460 13x launcher(tcp_server& srv, worker_base& w) noexcept : srv_(&srv), w_(&w)
461 {
462 13x }
463
464 public:
465 /// Return the worker to the pool if not launched.
466 14x ~launcher()
467 {
468
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 10 times.
14x if (w_)
469 4x srv_->push_sync(*w_);
470 14x }
471
472 1x launcher(launcher&& o) noexcept
473 1x : srv_(o.srv_)
474 1x , w_(std::exchange(o.w_, nullptr))
475 {
476 1x }
477 launcher(launcher const&) = delete;
478 launcher& operator=(launcher const&) = delete;
479 launcher& operator=(launcher&&) = delete;
480
481 /** Launch the connection-handling coroutine.
482
483 Starts the given coroutine on the specified executor. When
484 the coroutine completes, the worker is automatically returned
485 to the idle pool.
486
487 @param ex The executor to run the coroutine on.
488 @param task The coroutine to execute.
489
490 @throws std::logic_error If this launcher was already invoked.
491 */
492 template<class Executor>
493 10x void operator()(Executor const& ex, capy::task<void> task)
494 {
495
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 9 times.
10x if (!w_)
496 1x detail::throw_logic_error(); // launcher already invoked
497
498 9x auto* w = std::exchange(w_, nullptr);
499
500 // Worker is being dispatched - add to active list
501 9x srv_->active_push(w);
502
503 // Return worker to pool if coroutine setup throws
504 struct guard_t
505 {
506 tcp_server* srv;
507 worker_base* w;
508 9x ~guard_t()
509 {
510
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 9 times.
9x if (w)
511 srv->push_sync(*w);
512 9x }
513 9x } guard{srv_, w};
514
515 // Reset worker's stop source for this connection
516
1/1
✓ Branch 6 → 7 taken 9 times.
9x w->stop_ = {};
517 9x auto st = w->stop_.get_token();
518
519
1/1
✓ Branch 13 → 14 taken 9 times.
9x auto wrapper =
520 18x launch_coro<Executor>{}(ex, st, srv_, std::move(task), w);
521
522 // Executor and stop token stored in promise via constructor
523
1/1
✓ Branch 18 → 19 taken 9 times.
9x ex.post(std::exchange(wrapper.h, nullptr)); // Release before post
524 9x guard.w = nullptr; // Success - dismiss guard
525 9x }
526 };
527
528 /** Construct a TCP server.
529
530 @tparam Ctx Execution context type satisfying ExecutionContext.
531 @tparam Ex Executor type satisfying Executor.
532
533 @param ctx The execution context for socket operations.
534 @param ex The executor for dispatching coroutines.
535
536 @par Example
537 @par !example tcp_server
538 */
539 template<capy::ExecutionContext Ctx, capy::Executor Ex>
540 20x tcp_server(Ctx& ctx, Ex ex) : impl_(make_impl(ctx))
541 20x , ex_(std::move(ex))
542 {
543 20x }
544
545 public:
546 /// Destroy the server, stopping all accept loops.
547 ~tcp_server();
548
549 tcp_server(tcp_server const&) = delete;
550 tcp_server& operator=(tcp_server const&) = delete;
551
552 /** Move construct from another server.
553
554 @param o The source server. After the move, @p o is
555 in a valid but unspecified state.
556 */
557 tcp_server(tcp_server&& o) noexcept;
558
559 /** Move assign from another server.
560
561 @param o The source server. After the move, @p o is
562 in a valid but unspecified state.
563
564 @return `*this`.
565 */
566 tcp_server& operator=(tcp_server&& o) noexcept;
567
568 /** Bind to a local endpoint.
569
570 Creates an acceptor listening on the specified endpoint.
571 Multiple endpoints can be bound by calling this method
572 multiple times before @ref start.
573
574 @param ep The local endpoint to bind to.
575
576 @return The error code if binding fails.
577 */
578 [[nodiscard]] std::error_code bind(endpoint ep);
579
580 /** Set the worker pool.
581
582 Replaces any existing workers with the given range. Any
583 previous workers are released and the idle/active lists
584 are cleared before populating with new workers.
585
586 @tparam Range Forward range of pointer-like objects to worker_base.
587
588 @param workers Range of workers to manage. Each element must
589 support `std::to_address()` yielding `worker_base*`.
590
591 @par Example
592 @par !example set_workers
593 */
594 template<std::ranges::forward_range Range>
595 requires std::convertible_to<
596 decltype(std::to_address(
597 std::declval<std::ranges::range_value_t<Range>&>())),
598 worker_base*>
599 20x void set_workers(Range&& workers)
600 {
601 // Clear existing state
602 20x storage_.reset();
603 20x idle_head_ = nullptr;
604 20x active_head_ = nullptr;
605 20x active_tail_ = nullptr;
606
607 // Take ownership and populate idle list
608 using StorageType = std::decay_t<Range>;
609 20x auto* p = new StorageType(std::forward<Range>(workers));
610
1/1
✓ Branch 6 → 7 taken 20 times.
40x storage_ = std::shared_ptr<void>(
611
1/2
✓ Branch 2 → 3 taken 20 times.
✗ Branch 2 → 5 not taken.
40x p, [](void* ptr) { delete static_cast<StorageType*>(ptr); });
612
2/2
✓ Branch 16 → 11 taken 68 times.
✓ Branch 16 → 17 taken 20 times.
88x for (auto&& elem : *static_cast<StorageType*>(p))
613 68x idle_push(std::to_address(elem));
614 20x }
615
616 /** Start accepting connections.
617
618 Launches accept loops for all bound endpoints. Incoming
619 connections are dispatched to idle workers from the pool.
620
621 Calling `start()` on an already-running server has no effect.
622
623 @par Preconditions
624 - At least one endpoint bound via @ref bind.
625 - Workers provided via @ref set_workers.
626 - If restarting, @ref join must have completed first, and the
627 io_context must have been restarted (`ioc.restart()`).
628
629 @par Effects
630 Creates one accept coroutine per bound endpoint. Each coroutine
631 runs on the server's executor, waiting for connections and
632 dispatching them to idle workers.
633
634 @par Restart Sequence
635 To restart after stopping, complete the full shutdown cycle:
636 @par !example start
637
638 @par Thread Safety
639 Not thread safe.
640
641 @throws std::logic_error If a previous session has not been
642 joined (accept loops still active).
643 */
644 void start();
645
646 /** Return the local endpoint for the i-th bound port.
647
648 @param index Zero-based index into the list of bound ports.
649
650 @return The local endpoint, or a default-constructed endpoint
651 if @p index is out of range or the acceptor is not open.
652 */
653 endpoint local_endpoint(std::size_t index = 0) const noexcept;
654
655 /** Stop accepting connections.
656
657 Requests the accept loops' stop token and requests cancellation
658 of active workers via their stop tokens. The acceptors are not
659 closed; a suspended accept completes once more before its loop
660 observes the stop token and ends.
661
662 This function returns immediately; it does not wait for workers
663 to finish. Pending I/O operations complete asynchronously.
664
665 Calling `stop()` on a non-running server has no effect.
666
667 @par Effects
668 - Requests stop on the accept loops' stop token. The acceptors
669 are not closed; a pending accept completes once more before
670 the accept loop ends.
671 - Requests stop on each active worker's stop token.
672 - Workers observing their stop token should exit promptly.
673
674 @par Postconditions
675 No new connections will be accepted. Active workers continue
676 until they observe their stop token or complete naturally.
677
678 @par What Happens Next
679 After calling `stop()`:
680 1. Let `ioc.run()` return (drains pending completions).
681 2. Call @ref join to wait for accept loops to finish.
682 3. Only then is it safe to restart or destroy the server.
683
684 @par Thread Safety
685 Not thread safe.
686
687 @see join, start
688 */
689 void stop();
690
691 /** Block until all accept loops complete.
692
693 Blocks the calling thread until all accept coroutines launched
694 by @ref start have finished executing. This synchronizes the
695 shutdown sequence, ensuring the server is fully stopped before
696 restarting or destroying it.
697
698 @par Preconditions
699 @ref stop has been called and `ioc.run()` has returned.
700
701 @par Postconditions
702 All accept loops have completed. The server is in the stopped
703 state and may be restarted via @ref start.
704
705 @par Example (Correct Usage)
706 @par !example correct_usage
707
708 @par WARNING: Deadlock Scenario
709 Calling `join()` from inside a worker coroutine deadlocks:
710
711 @par !example deadlock_scenarios
712
713 @par Thread Safety
714 May be called from any thread, but will deadlock if called
715 from within the io_context event loop or from a worker coroutine.
716
717 @see stop, start
718 */
719 void join();
720
721 private:
722 capy::task<> do_stop();
723 };
724
725 #ifdef _MSC_VER
726 #pragma warning(pop)
727 #endif
728
729 } // namespace boost::corosio
730
731 #endif
732