LCOV - code coverage report
Current view: top level - wirestead/transport/tcp_server - tcp_server.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 89.7 % 624 560
Test Date: 2026-08-30 10:35:09 Functions: 93.5 % 62 58
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 65.8 % 406 267

             Branch data     Line data    Source code
       1                 :             : /*
       2                 :             :  * Copyright 2025 Jinwoo Sung
       3                 :             :  *
       4                 :             :  * Licensed under the Apache License, Version 2.0 (the "License");
       5                 :             :  * you may not use this file except in compliance with the License.
       6                 :             :  * You may obtain a copy of the License at
       7                 :             :  *
       8                 :             :  *     http://www.apache.org/licenses/LICENSE-2.0
       9                 :             :  *
      10                 :             :  * Unless required by applicable law or agreed to in writing, software
      11                 :             :  * distributed under the License is distributed on an "AS IS" BASIS,
      12                 :             :  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13                 :             :  * See the License for the specific language governing permissions and
      14                 :             :  * limitations under the License.
      15                 :             :  */
      16                 :             : 
      17                 :             : #include "wirestead/transport/tcp_server/tcp_server.hpp"
      18                 :             : 
      19                 :             : #include <spdlog/fmt/fmt.h>
      20                 :             : 
      21                 :             : #include <algorithm>
      22                 :             : #include <atomic>
      23                 :             : #include <boost/asio.hpp>
      24                 :             : #include <future>
      25                 :             : #include <iostream>
      26                 :             : #include <mutex>
      27                 :             : #include <stop_token>
      28                 :             : #include <string_view>
      29                 :             : #include <thread>
      30                 :             : #include <unordered_map>
      31                 :             : 
      32                 :             : #include "wirestead/concurrency/io_context_manager.hpp"
      33                 :             : #include "wirestead/concurrency/io_thread_hook.hpp"
      34                 :             : #include "wirestead/concurrency/thread_safe_state.hpp"
      35                 :             : #include "wirestead/diagnostics/exceptions.hpp"
      36                 :             : #include "wirestead/diagnostics/logger.hpp"
      37                 :             : #include "wirestead/diagnostics/runtime_stats_counter.hpp"
      38                 :             : #include "wirestead/interface/itcp_acceptor.hpp"
      39                 :             : #include "wirestead/transport/base/error_info_holder.hpp"
      40                 :             : #include "wirestead/transport/tcp_server/boost_tcp_acceptor.hpp"
      41                 :             : #include "wirestead/transport/tcp_server/ssl_tcp_socket.hpp"
      42                 :             : #include "wirestead/transport/tcp_server/tcp_server_session.hpp"
      43                 :             : 
      44                 :             : namespace wirestead {
      45                 :             : namespace transport {
      46                 :             : 
      47                 :             : namespace net = boost::asio;
      48                 :             : using tcp = net::ip::tcp;
      49                 :             : 
      50                 :             : struct TcpServer::Impl {
      51                 :             :   std::atomic<bool> stopping_{false};
      52                 :             :   // #503: stop() can call perform_cleanup() from two different paths - the
      53                 :             :   // dispatched-onto-the-io_context call, and (if that doesn't complete
      54                 :             :   // within the timeout below) a direct fallback call on the stopping
      55                 :             :   // thread. Under slow/instrumented builds (TSAN) the dispatched call can
      56                 :             :   // still be mid-flight when the timeout fires, so both could run
      57                 :             :   // perform_cleanup()'s body concurrently - including acceptor_->close(),
      58                 :             :   // racing with the io_context thread's own concurrent async_accept.
      59                 :             :   // Guards perform_cleanup() to run its body at most once per stop cycle,
      60                 :             :   // reset back to false in start() alongside stopping_.
      61                 :             :   std::atomic<bool> cleanup_started_{false};
      62                 :             :   std::atomic<ClientId> next_client_id_{0};
      63                 :             : 
      64                 :             :   std::unique_ptr<net::io_context> owned_ioc_;
      65                 :             :   bool owns_ioc_;
      66                 :             :   bool uses_shared_context_{false};
      67                 :             :   net::io_context& ioc_;
      68                 :             :   std::unique_ptr<net::executor_work_guard<net::io_context::executor_type>> work_guard_;
      69                 :             :   std::jthread ioc_thread_;
      70                 :             : 
      71                 :             :   std::unique_ptr<interface::TcpAcceptorInterface> acceptor_;
      72                 :             :   config::TcpServerConfig cfg_;
      73                 :             : 
      74                 :         141 :   concurrency::AtomicLinkState state_{base::LinkState::Idle};
      75                 :             :   // Shared snapshots for the handlers the io thread copies out per received
      76                 :             :   // chunk - a std::function copy allocates whenever the target outgrows its
      77                 :             :   // small-object buffer. See interface::SharedCallback. The connect/disconnect
      78                 :             :   // handlers below stay plain: they fire once per connection, not per chunk.
      79                 :             :   interface::SharedCallback<OnBytes> on_bytes_;
      80                 :             :   interface::SharedCallback<OnState> on_state_;
      81                 :             :   interface::SharedCallback<OnBackpressure> on_bp_;
      82                 :             :   MultiClientConnectHandler on_multi_connect_;
      83                 :             :   interface::SharedCallback<MultiClientDataHandler> on_multi_data_;
      84                 :             :   MultiClientDisconnectHandler on_multi_disconnect_;
      85                 :             :   diagnostics::RuntimeStatsCounters stats_;
      86                 :             : 
      87                 :             :   mutable std::mutex sessions_mutex_;
      88                 :             :   std::unordered_map<ClientId, std::shared_ptr<TcpServerSession>> sessions_;
      89                 :             : 
      90                 :             :   size_t max_clients_;
      91                 :             :   bool client_limit_enabled_;
      92                 :             : 
      93                 :             :   std::shared_ptr<TcpServerSession> current_session_;
      94                 :             : 
      95                 :             :   ErrorInfoHolder error_info_holder_{"tcp_server"};
      96                 :             : 
      97                 :         132 :   explicit Impl(const config::TcpServerConfig& cfg, bool use_shared_context)
      98         [ +  + ]:         132 :       : owned_ioc_(use_shared_context ? nullptr : std::make_unique<net::io_context>()),
      99                 :         132 :         owns_ioc_(!use_shared_context),
     100                 :         132 :         uses_shared_context_(use_shared_context),
     101   [ +  +  +  -  :         132 :         ioc_(use_shared_context ? concurrency::IoContextManager::instance().get_context() : *owned_ioc_),
                   +  - ]
     102                 :         132 :         cfg_(cfg),
     103                 :         132 :         max_clients_(cfg.max_connections > 0 ? static_cast<size_t>(cfg.max_connections) : 0),
     104         [ +  - ]:         660 :         client_limit_enabled_(cfg.max_connections > 0) {
     105                 :             :     try {
     106                 :         132 :       acceptor_ = std::make_unique<BoostTcpAcceptor>(ioc_);
     107                 :           0 :     } catch (const std::exception& e) {
     108                 :           0 :       throw diagnostics::BuilderException("Failed to create TCP acceptor: " + std::string(e.what()), "tcp_server");
     109                 :           0 :     }
     110                 :         132 :     cfg_.validate_and_clamp();
     111                 :         132 :     max_clients_ = cfg_.max_connections > 0 ? static_cast<size_t>(cfg_.max_connections) : 0;
     112                 :         132 :     client_limit_enabled_ = cfg_.max_connections > 0;
     113                 :         132 :   }
     114                 :             : 
     115                 :           9 :   Impl(const config::TcpServerConfig& cfg, std::unique_ptr<interface::TcpAcceptorInterface> acceptor,
     116                 :             :        net::io_context& ioc)
     117                 :          18 :       : owns_ioc_(false),
     118                 :           9 :         ioc_(ioc),
     119                 :           9 :         acceptor_(std::move(acceptor)),
     120                 :           9 :         cfg_(cfg),
     121                 :           9 :         max_clients_(cfg.max_connections > 0 ? static_cast<size_t>(cfg.max_connections) : 0),
     122         [ +  - ]:          36 :         client_limit_enabled_(cfg.max_connections > 0) {
     123         [ +  + ]:           9 :     if (!acceptor_) {
     124                 :           7 :       throw diagnostics::BuilderException("Failed to create TCP acceptor", "tcp_server");
     125                 :             :     }
     126                 :           8 :     cfg_.validate_and_clamp();
     127                 :           8 :     max_clients_ = cfg_.max_connections > 0 ? static_cast<size_t>(cfg_.max_connections) : 0;
     128                 :           8 :     client_limit_enabled_ = cfg_.max_connections > 0;
     129                 :          23 :   }
     130                 :             : 
     131                 :         135 :   ~Impl() {
     132                 :             :     try {
     133                 :         135 :       stopping_.store(true);
     134         [ -  + ]:         135 :       if (ioc_thread_.joinable()) {
     135         [ #  # ]:           0 :         if (std::this_thread::get_id() == ioc_thread_.get_id()) {
     136                 :           0 :           ioc_thread_.detach();
     137                 :             :         } else {
     138                 :           0 :           ioc_thread_.request_stop();
     139                 :           0 :           ioc_thread_.join();
     140                 :             :         }
     141                 :             :       }
     142                 :         135 :       perform_cleanup();
     143                 :           0 :     } catch (...) {
     144                 :           0 :     }
     145                 :         135 :   }
     146                 :             : 
     147                 :         506 :   void notify_state() {
     148         [ +  + ]:         506 :     if (stopping_.load()) return;
     149                 :         351 :     interface::SharedCallback<OnState> cb;
     150                 :             :     try {
     151                 :             :       {
     152                 :         351 :         std::lock_guard<std::mutex> lock(sessions_mutex_);
     153                 :         351 :         cb = on_state_;
     154                 :         351 :       }
     155         [ +  + ]:         351 :       if (cb) {
     156                 :         330 :         (*cb)(state_.get());
     157                 :             :       }
     158                 :           1 :     } catch (...) {
     159                 :           1 :     }
     160                 :         351 :   }
     161                 :             : 
     162                 :             :   // One context for the whole server, shared by every accepted connection.
     163                 :             :   // Built once in start() so a bad certificate fails there rather than on the
     164                 :             :   // first client.
     165                 :             : #ifdef WIRESTEAD_TLS_ENABLED
     166                 :             :   std::shared_ptr<boost::asio::ssl::context> ssl_context_;
     167                 :             : #endif
     168                 :             : 
     169                 :             :   // Builds the ssl::context, or reports why it cannot. Called from start(), so
     170                 :             :   // a certificate problem surfaces as a start failure with a message rather
     171                 :             :   // than as connections that mysteriously drop at handshake.
     172                 :         139 :   std::string init_tls() {
     173                 :             :     // Half a TLS config is a request for TLS, not a request for plaintext. An
     174                 :             :     // empty environment variable or a typo would otherwise leave tls_enabled()
     175                 :             :     // false and bring the server up unencrypted without saying anything, which
     176                 :             :     // is the one outcome this whole path exists to prevent.
     177         [ +  + ]:         139 :     if (!cfg_.tls_certificate_file.empty() != !cfg_.tls_private_key_file.empty()) {
     178                 :           2 :       return cfg_.tls_certificate_file.empty() ? "TLS needs a certificate as well as a private key"
     179                 :           4 :                                                : "TLS needs a private key as well as a certificate";
     180                 :             :     }
     181         [ +  + ]:         137 :     if (!cfg_.tls_enabled()) return {};
     182                 :             : #ifndef WIRESTEAD_TLS_ENABLED
     183                 :             :     return "TLS was configured but this build has WIRESTEAD_ENABLE_TLS=OFF";
     184                 :             : #else
     185                 :             :     namespace ssl = boost::asio::ssl;
     186                 :             :     try {
     187                 :           4 :       auto ctx = std::make_shared<ssl::context>(ssl::context::tls_server);
     188                 :             :       // Anything below TLS 1.2 is broken in ways nobody should opt into by
     189                 :             :       // accident, so the floor is not configurable.
     190                 :           4 :       ctx->set_options(ssl::context::default_workarounds | ssl::context::no_sslv2 | ssl::context::no_sslv3 |
     191                 :             :                        ssl::context::no_tlsv1 | ssl::context::no_tlsv1_1 | ssl::context::single_dh_use);
     192                 :           4 :       ctx->use_certificate_chain_file(cfg_.tls_certificate_file);
     193                 :           3 :       ctx->use_private_key_file(cfg_.tls_private_key_file, ssl::context::pem);
     194                 :           3 :       ssl_context_ = std::move(ctx);
     195                 :           3 :       return {};
     196                 :           5 :     } catch (const std::exception& e) {
     197                 :           3 :       return std::string("Failed to load TLS certificate or key: ") + e.what();
     198                 :           1 :     }
     199                 :             : #endif
     200                 :             :   }
     201                 :             : 
     202                 :         140 :   std::shared_ptr<TcpServerSession> make_session(tcp::socket sock) {
     203                 :             : #ifdef WIRESTEAD_TLS_ENABLED
     204         [ +  + ]:         140 :     if (ssl_context_) {
     205                 :             :       return std::make_shared<TcpServerSession>(
     206                 :          64 :           ioc_, std::make_unique<SslTcpSocket>(std::move(sock), ssl_context_), cfg_.backpressure_threshold,
     207                 :          64 :           cfg_.idle_timeout_ms, cfg_.backpressure_strategy, cfg_.enable_memory_pool, cfg_.read_buffer_size);
     208                 :             :     }
     209                 :             : #endif
     210                 :         108 :     return std::make_shared<TcpServerSession>(ioc_, std::move(sock), cfg_.backpressure_threshold, cfg_.idle_timeout_ms,
     211                 :         108 :                                               cfg_.backpressure_strategy, cfg_.enable_memory_pool,
     212                 :         108 :                                               cfg_.read_buffer_size);
     213                 :             :   }
     214                 :             : 
     215                 :         140 :   void apply_accepted_socket_options(tcp::socket& sock) {
     216                 :         140 :     boost::system::error_code ec;
     217                 :             : 
     218         [ +  - ]:         140 :     if (cfg_.tcp_no_delay) {
     219                 :         140 :       sock.set_option(tcp::no_delay(true), ec);
     220         [ -  + ]:         140 :       if (ec) {
     221                 :           0 :         WIRESTEAD_LOG_WARNING("tcp_server", "socket_options",
     222                 :             :                               fmt::format("Failed to set TCP_NODELAY: {}", ec.message()));
     223                 :           0 :         ec.clear();
     224                 :             :       }
     225                 :             :     }
     226                 :             : 
     227         [ -  + ]:         140 :     if (cfg_.keep_alive) {
     228                 :           0 :       sock.set_option(net::socket_base::keep_alive(true), ec);
     229         [ #  # ]:           0 :       if (ec) {
     230                 :           0 :         WIRESTEAD_LOG_WARNING("tcp_server", "socket_options",
     231                 :             :                               fmt::format("Failed to set keep_alive: {}", ec.message()));
     232                 :           0 :         ec.clear();
     233                 :             :       }
     234                 :             :     }
     235                 :             : 
     236         [ +  + ]:         140 :     if (cfg_.send_buffer_size > 0) {
     237                 :           4 :       sock.set_option(net::socket_base::send_buffer_size(static_cast<int>(cfg_.send_buffer_size)), ec);
     238         [ -  + ]:           4 :       if (ec) {
     239                 :           0 :         WIRESTEAD_LOG_WARNING("tcp_server", "socket_options",
     240                 :             :                               fmt::format("Failed to set send buffer size: {}", ec.message()));
     241                 :           0 :         ec.clear();
     242                 :             :       }
     243                 :             :     }
     244                 :             : 
     245         [ -  + ]:         140 :     if (cfg_.receive_buffer_size > 0) {
     246                 :           0 :       sock.set_option(net::socket_base::receive_buffer_size(static_cast<int>(cfg_.receive_buffer_size)), ec);
     247         [ #  # ]:           0 :       if (ec) {
     248                 :           0 :         WIRESTEAD_LOG_WARNING("tcp_server", "socket_options",
     249                 :             :                               fmt::format("Failed to set receive buffer size: {}", ec.message()));
     250                 :           0 :         ec.clear();
     251                 :             :       }
     252                 :             :     }
     253                 :         140 :   }
     254                 :             : 
     255                 :         133 :   void attempt_port_binding(std::shared_ptr<TcpServer> self, int retry_count) {
     256         [ -  + ]:         140 :     if (stopping_.load()) return;
     257                 :         133 :     boost::system::error_code ec;
     258                 :             : 
     259                 :         133 :     auto address = net::ip::make_address(cfg_.bind_address, ec);
     260         [ +  + ]:         133 :     if (ec) {
     261                 :           2 :       std::string msg = fmt::format("Invalid bind address: {}, {}", cfg_.bind_address, ec.message());
     262                 :           2 :       WIRESTEAD_LOG_ERROR("tcp_server", "bind", msg);
     263                 :           2 :       error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONFIGURATION, "bind",
     264                 :             :                                       ec, msg, false, static_cast<uint32_t>(retry_count));
     265                 :           2 :       state_.set(base::LinkState::Error);
     266                 :           2 :       notify_state();
     267                 :           2 :       return;
     268                 :           2 :     }
     269                 :             : 
     270   [ +  -  +  + ]:         131 :     if (!acceptor_->is_open()) {
     271   [ -  +  +  - ]:         129 :       acceptor_->open(address.is_v6() ? tcp::v6() : tcp::v4(), ec);
     272         [ +  + ]:         129 :       if (ec) {
     273                 :           1 :         std::string msg = fmt::format("Failed to open acceptor: {}", ec.message());
     274                 :           1 :         WIRESTEAD_LOG_ERROR("tcp_server", "open", msg);
     275                 :           1 :         error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::SYSTEM, "open", ec,
     276                 :             :                                         msg, false, static_cast<uint32_t>(retry_count));
     277                 :           1 :         state_.set(base::LinkState::Error);
     278                 :           1 :         notify_state();
     279                 :           1 :         return;
     280                 :           1 :       }
     281                 :             :     }
     282                 :             : 
     283                 :         130 :     acceptor_->bind(tcp::endpoint(address, cfg_.port), ec);
     284         [ +  + ]:         130 :     if (ec) {
     285   [ +  +  +  - ]:           3 :       if (cfg_.enable_port_retry && retry_count < cfg_.max_port_retries) {
     286                 :           2 :         auto timer = std::make_shared<net::steady_timer>(ioc_);
     287                 :           2 :         timer->expires_after(std::chrono::milliseconds(cfg_.port_retry_interval_ms));
     288                 :           2 :         timer->async_wait([self, retry_count, timer](const boost::system::error_code& timer_ec) {
     289         [ +  - ]:           2 :           if (!timer_ec) {
     290                 :           2 :             auto* timer_impl = self->get_impl();
     291         [ +  - ]:           2 :             if (!timer_impl->stopping_.load()) {
     292                 :           2 :               timer_impl->attempt_port_binding(self, retry_count + 1);
     293                 :             :             }
     294                 :             :           }
     295                 :           2 :         });
     296                 :           2 :         return;
     297                 :           2 :       } else {
     298                 :           1 :         std::string msg = fmt::format("Failed to bind to port {}: {}", cfg_.port, ec.message());
     299                 :           1 :         WIRESTEAD_LOG_ERROR("tcp_server", "bind", msg);
     300                 :           1 :         error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "bind",
     301                 :             :                                         ec, msg, false, static_cast<uint32_t>(retry_count));
     302                 :           1 :         state_.set(base::LinkState::Error);
     303                 :           1 :         notify_state();
     304                 :           1 :         return;
     305                 :           1 :       }
     306                 :             :     }
     307                 :             : 
     308                 :         127 :     acceptor_->listen(boost::asio::socket_base::max_listen_connections, ec);
     309         [ +  + ]:         127 :     if (ec) {
     310                 :           1 :       std::string msg = fmt::format("Failed to listen on port {}: {}", cfg_.port, ec.message());
     311                 :           1 :       WIRESTEAD_LOG_ERROR("tcp_server", "listen", msg);
     312                 :           1 :       error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "listen",
     313                 :             :                                       ec, msg, false, static_cast<uint32_t>(retry_count));
     314                 :           1 :       state_.set(base::LinkState::Error);
     315                 :           1 :       notify_state();
     316                 :           1 :       return;
     317                 :           1 :     }
     318                 :             : 
     319                 :         126 :     state_.set(base::LinkState::Listening);
     320                 :         126 :     notify_state();
     321                 :         126 :     do_accept(self);
     322                 :             :   }
     323                 :             : 
     324                 :        2057 :   void do_accept(std::shared_ptr<TcpServer> self) {
     325   [ +  +  +  -  :        2057 :     if (stopping_.load() || !acceptor_ || !acceptor_->is_open()) return;
             -  +  +  + ]
     326                 :             : 
     327                 :        2042 :     acceptor_->async_accept([self](auto ec, tcp::socket sock) {
     328                 :        2040 :       auto* accept_impl = self->get_impl();
     329         [ +  + ]:        2040 :       if (accept_impl->stopping_.load()) {
     330                 :        1900 :         return;
     331                 :             :       }
     332         [ +  + ]:        1932 :       if (ec) {
     333         [ +  - ]:           1 :         if (ec != boost::asio::error::operation_aborted) {
     334                 :           1 :           accept_impl->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR,
     335                 :             :                                                        diagnostics::ErrorCategory::CONNECTION, "accept", ec,
     336                 :             :                                                        fmt::format("Accept failed: {}", ec.message()), true, 0);
     337                 :           1 :           accept_impl->state_.set(base::LinkState::Error);
     338                 :           1 :           accept_impl->notify_state();
     339                 :             :         }
     340   [ +  -  +  -  :           1 :         if (!accept_impl->state_.is_state(base::LinkState::Closed) && !accept_impl->stopping_.load()) {
                   +  - ]
     341                 :           1 :           auto timer = std::make_shared<net::steady_timer>(accept_impl->ioc_);
     342                 :           1 :           timer->expires_after(std::chrono::milliseconds(100));
     343                 :           1 :           timer->async_wait([self, timer](const boost::system::error_code&) {
     344                 :           0 :             auto* retry_impl = self->get_impl();
     345         [ #  # ]:           0 :             if (!retry_impl->stopping_.load()) {
     346                 :           0 :               retry_impl->do_accept(self);
     347                 :             :             }
     348                 :             :           });
     349                 :           1 :         }
     350                 :           1 :         return;
     351                 :             :       }
     352                 :             : 
     353                 :        1931 :       boost::system::error_code ep_ec;
     354                 :        1931 :       auto rep = sock.remote_endpoint(ep_ec);
     355                 :        1931 :       std::string client_info = "unknown";
     356         [ +  - ]:        1931 :       if (!ep_ec) {
     357                 :        1931 :         client_info = fmt::format("{}:{}", rep.address().to_string(), rep.port());
     358                 :             :       }
     359                 :             : 
     360         [ +  - ]:        1931 :       if (accept_impl->client_limit_enabled_) {
     361                 :             :         bool over_limit;
     362                 :             :         {
     363                 :        1931 :           std::lock_guard<std::mutex> lock(accept_impl->sessions_mutex_);
     364                 :        1931 :           over_limit = accept_impl->sessions_.size() >= accept_impl->max_clients_;
     365                 :        1931 :         }
     366         [ +  + ]:        1931 :         if (over_limit) {
     367                 :             :           // #437: accept and immediately close over-limit connections
     368                 :             :           // instead of pausing the accept loop entirely - a client whose
     369                 :             :           // TCP handshake already completed while paused would otherwise
     370                 :             :           // sit connected but silent until a slot frees up.
     371                 :        1791 :           boost::system::error_code close_ec;
     372                 :        1791 :           sock.close(close_ec);
     373                 :        1791 :           accept_impl->do_accept(self);
     374                 :        1791 :           return;
     375                 :             :         }
     376                 :             :       }
     377                 :             : 
     378                 :         140 :       accept_impl->apply_accepted_socket_options(sock);
     379                 :             : 
     380                 :             :       // The session only talks to TcpSocketInterface, so TLS is a matter of
     381                 :             :       // which implementation it gets wrapped in here. Everything downstream -
     382                 :             :       // reads, writes, backpressure, stats - is identical either way.
     383                 :         140 :       auto new_session = accept_impl->make_session(std::move(sock));
     384                 :             : 
     385                 :         140 :       ClientId client_id = accept_impl->next_client_id_.fetch_add(1);
     386                 :             : 
     387                 :         140 :       std::weak_ptr<TcpServer> weak_self = self;
     388                 :             : 
     389                 :         140 :       new_session->on_bytes([weak_self, client_id](memory::ConstByteSpan data) {
     390                 :        2473 :         auto shared_self = weak_self.lock();
     391         [ -  + ]:        2473 :         if (!shared_self) return;
     392                 :        2473 :         auto* bytes_impl = shared_self->get_impl();
     393                 :             : 
     394                 :        2473 :         interface::SharedCallback<OnBytes> cb;
     395                 :        2473 :         interface::SharedCallback<MultiClientDataHandler> multi_cb;
     396                 :             :         {
     397                 :        2473 :           std::lock_guard<std::mutex> lock(bytes_impl->sessions_mutex_);
     398                 :        2473 :           cb = bytes_impl->on_bytes_;
     399                 :        2473 :           multi_cb = bytes_impl->on_multi_data_;
     400                 :        2473 :         }
     401   [ +  +  +  - ]:        2473 :         if (cb) (*cb)(data);
     402         [ +  + ]:        2473 :         if (multi_cb) {
     403                 :        2466 :           (*multi_cb)(client_id, data);
     404                 :             :         }
     405                 :        2473 :       });
     406                 :             : 
     407                 :         140 :       interface::SharedCallback<OnBackpressure> bp_cb;
     408                 :             :       {
     409                 :         140 :         std::lock_guard<std::mutex> lock(accept_impl->sessions_mutex_);
     410                 :         140 :         bp_cb = accept_impl->on_bp_;
     411                 :         140 :       }
     412   [ +  +  +  -  :         140 :       if (bp_cb) new_session->on_backpressure(*bp_cb);
                   +  - ]
     413                 :             : 
     414                 :         140 :       new_session->on_close([weak_self, client_id, new_session] {
     415                 :         128 :         auto shared_self = weak_self.lock();
     416         [ -  + ]:         128 :         if (!shared_self) return;
     417                 :         128 :         auto* close_impl = shared_self->get_impl();
     418         [ +  + ]:         128 :         if (close_impl->stopping_.load()) return;
     419                 :             : 
     420                 :         119 :         MultiClientDisconnectHandler disconnect_cb;
     421                 :             :         {
     422                 :         119 :           std::lock_guard<std::mutex> lock(close_impl->sessions_mutex_);
     423                 :         119 :           disconnect_cb = close_impl->on_multi_disconnect_;
     424                 :         119 :         }
     425   [ +  +  +  - ]:         119 :         if (disconnect_cb) disconnect_cb(client_id);
     426                 :             : 
     427                 :         119 :         bool was_current = false;
     428                 :             :         {
     429                 :         119 :           std::lock_guard<std::mutex> lock(close_impl->sessions_mutex_);
     430                 :             :           // Carry the session's totals over to the server before it goes away,
     431                 :             :           // so stats() keeps reporting what this connection did. Tied to the
     432                 :             :           // erase below, which makes it exactly once even if on_close re-fires.
     433                 :         119 :           auto it = close_impl->sessions_.find(client_id);
     434   [ +  -  +  -  :         119 :           if (it != close_impl->sessions_.end() && it->second) {
                   +  - ]
     435                 :         119 :             close_impl->stats_.absorb(it->second->stats());
     436                 :             :           }
     437                 :         119 :           close_impl->sessions_.erase(client_id);
     438                 :         119 :           was_current = (close_impl->current_session_ == new_session);
     439         [ +  + ]:         119 :           if (was_current) {
     440         [ +  + ]:          91 :             if (!close_impl->sessions_.empty())
     441                 :           4 :               close_impl->current_session_ = close_impl->sessions_.begin()->second;
     442                 :             :             else
     443                 :          87 :               close_impl->current_session_.reset();
     444                 :             :           }
     445                 :         119 :         }
     446         [ +  + ]:         119 :         if (was_current) {
     447                 :          91 :           close_impl->state_.set(base::LinkState::Listening);
     448                 :          91 :           close_impl->notify_state();
     449                 :             :         }
     450                 :         128 :       });
     451                 :             : 
     452                 :             :       // alive_ must be true before the session enters sessions_, so that
     453                 :             :       // broadcast() callers who observe client_count() >= 1 are guaranteed
     454                 :             :       // to pass the alive() check inside async_try_write_shared().
     455                 :         140 :       new_session->start();
     456                 :             : 
     457                 :             :       {
     458                 :         140 :         std::lock_guard<std::mutex> lock(accept_impl->sessions_mutex_);
     459                 :         140 :         accept_impl->sessions_.emplace(client_id, new_session);
     460                 :         140 :         accept_impl->current_session_ = new_session;
     461                 :         140 :       }
     462                 :             : 
     463                 :         140 :       MultiClientConnectHandler connect_cb;
     464                 :             :       {
     465                 :         140 :         std::lock_guard<std::mutex> lock(accept_impl->sessions_mutex_);
     466                 :         140 :         connect_cb = accept_impl->on_multi_connect_;
     467                 :         140 :       }
     468   [ +  +  +  - ]:         140 :       if (connect_cb) connect_cb(client_id, client_info);
     469                 :             : 
     470                 :         140 :       accept_impl->state_.set(base::LinkState::Connected);
     471                 :         140 :       accept_impl->notify_state();
     472                 :         140 :       accept_impl->do_accept(self);
     473                 :        1931 :     });
     474                 :             :   }
     475                 :             : 
     476                 :         275 :   void perform_cleanup() {
     477         [ +  + ]:         275 :     if (cleanup_started_.exchange(true)) return;
     478                 :             :     try {
     479                 :         140 :       boost::system::error_code ec;
     480   [ +  -  +  -  :         140 :       if (acceptor_ && acceptor_->is_open()) {
             +  +  +  + ]
     481                 :         128 :         acceptor_->close(ec);
     482                 :             :       }
     483                 :             : 
     484                 :         140 :       std::vector<std::shared_ptr<TcpServerSession>> sessions_copy;
     485                 :             :       {
     486                 :         140 :         std::lock_guard<std::mutex> lock(sessions_mutex_);
     487                 :         140 :         sessions_copy.reserve(sessions_.size());
     488         [ +  + ]:         161 :         for (auto& kv : sessions_) {
     489                 :          21 :           sessions_copy.push_back(kv.second);
     490                 :             :         }
     491                 :         140 :         sessions_.clear();
     492                 :         140 :         current_session_.reset();
     493                 :         140 :       }
     494                 :             : 
     495         [ +  + ]:         161 :       for (auto& session : sessions_copy) {
     496         [ +  - ]:          21 :         if (session) {
     497                 :          21 :           session->stop();
     498                 :             :         }
     499                 :             :       }
     500                 :             : 
     501                 :         140 :       state_.set(base::LinkState::Closed);
     502                 :         140 :       notify_state();
     503                 :         140 :     } catch (...) {
     504                 :           0 :     }
     505                 :             :   }
     506                 :             : 
     507                 :         260 :   void stop(std::shared_ptr<TcpServer> self) {
     508         [ +  + ]:         260 :     if (stopping_.exchange(true)) {
     509                 :         120 :       return;
     510                 :             :     }
     511                 :             : 
     512                 :             :     {
     513                 :         140 :       std::lock_guard<std::mutex> lock(sessions_mutex_);
     514                 :         140 :       on_bytes_ = nullptr;
     515                 :         140 :       on_state_ = nullptr;
     516                 :         140 :       on_bp_ = nullptr;
     517                 :         140 :       on_multi_connect_ = nullptr;
     518                 :         140 :       on_multi_data_ = nullptr;
     519                 :         140 :       on_multi_disconnect_ = nullptr;
     520                 :         140 :     }
     521                 :             : 
     522         [ -  + ]:         140 :     if (ioc_.get_executor().running_in_this_thread()) {
     523                 :           0 :       perform_cleanup();
     524         [ #  # ]:           0 :       if (owns_ioc_) ioc_.stop();
     525                 :           0 :       return;
     526                 :             :     }
     527                 :             : 
     528                 :             :     // #503: previously gated on has_active_ioc (owns_ioc_ || a running
     529                 :             :     // shared IoContextManager) before deciding to dispatch perform_cleanup()
     530                 :             :     // onto ioc_ vs. calling it directly - but a "managed external context"
     531                 :             :     // (owns_ioc_ == false, since some other owner constructed the
     532                 :             :     // io_context, yet that owner's own thread may still be actively
     533                 :             :     // running it, e.g. the wrapper layer's own io_context+thread) fell
     534                 :             :     // through to the direct-call branch, racing acceptor_->close() against
     535                 :             :     // that thread's concurrent async_accept(). Whenever we have a valid
     536                 :             :     // self to dispatch through, always prefer dispatching onto ioc_ (with
     537                 :             :     // the same timeout-based direct-call fallback for the case where
     538                 :             :     // nothing is actually pumping it) - a stale, unserviced io_context only
     539                 :             :     // costs one extra harmless wait before falling back to the exact same
     540                 :             :     // direct call as before; self is only null when called from the
     541                 :             :     // destructor, where shared_from_this() isn't available and a direct
     542                 :             :     // call is the only option.
     543         [ +  - ]:         140 :     if (self) {
     544                 :         140 :       auto cleanup_promise = std::make_shared<std::promise<void>>();
     545                 :         140 :       auto cleanup_future = cleanup_promise->get_future();
     546                 :             : 
     547                 :         140 :       std::weak_ptr<TcpServer> weak_self = self;
     548                 :         140 :       net::dispatch(ioc_, [weak_self, cleanup_promise]() {
     549         [ +  - ]:         132 :         if (auto shared_self = weak_self.lock()) {
     550                 :         132 :           auto* cleanup_impl = shared_self->get_impl();
     551                 :         132 :           cleanup_impl->perform_cleanup();
     552                 :         132 :         }
     553                 :         132 :         cleanup_promise->set_value();
     554                 :         132 :       });
     555                 :             : 
     556   [ +  -  +  + ]:         140 :       if (cleanup_future.wait_for(std::chrono::seconds(2)) == std::future_status::timeout) {
     557                 :           8 :         perform_cleanup();
     558                 :             :       }
     559                 :         140 :     } else {
     560                 :           0 :       perform_cleanup();
     561                 :             :     }
     562                 :             : 
     563         [ +  + ]:         140 :     if (owns_ioc_) {
     564         [ +  + ]:         130 :       if (work_guard_) work_guard_->reset();
     565         [ +  + ]:         130 :       if (ioc_thread_.joinable()) {
     566         [ -  + ]:         126 :         if (std::this_thread::get_id() == ioc_thread_.get_id()) {
     567                 :           0 :           ioc_thread_.detach();
     568                 :             :         } else {
     569                 :         126 :           ioc_thread_.request_stop();
     570                 :         126 :           ioc_thread_.join();
     571                 :             :         }
     572                 :         126 :         ioc_.restart();
     573                 :             :       }
     574                 :             :     }
     575                 :             :   }
     576                 :             : };
     577                 :             : 
     578                 :         132 : std::shared_ptr<TcpServer> TcpServer::create(const config::TcpServerConfig& cfg, bool use_shared_context) {
     579                 :         132 :   return std::shared_ptr<TcpServer>(new TcpServer(cfg, use_shared_context));
     580                 :             : }
     581                 :             : 
     582                 :           9 : std::shared_ptr<TcpServer> TcpServer::create(const config::TcpServerConfig& cfg,
     583                 :             :                                              std::unique_ptr<interface::TcpAcceptorInterface> acceptor,
     584                 :             :                                              net::io_context& ioc) {
     585                 :          10 :   return std::shared_ptr<TcpServer>(new TcpServer(cfg, std::move(acceptor), ioc));
     586                 :             : }
     587                 :             : 
     588                 :         132 : TcpServer::TcpServer(const config::TcpServerConfig& cfg, bool use_shared_context)
     589                 :         132 :     : impl_(std::make_unique<Impl>(cfg, use_shared_context)) {}
     590                 :             : 
     591                 :           9 : TcpServer::TcpServer(const config::TcpServerConfig& cfg, std::unique_ptr<interface::TcpAcceptorInterface> acceptor,
     592                 :           9 :                      net::io_context& ioc)
     593                 :          10 :     : impl_(std::make_unique<Impl>(cfg, std::move(acceptor), ioc)) {}
     594                 :             : 
     595                 :         270 : TcpServer::~TcpServer() {
     596   [ +  -  -  +  :         135 :   if (impl_ && !impl_->state_.is_state(base::LinkState::Closed)) {
                   -  + ]
     597                 :             :     // Pass nullptr to stop() to indicate we are in destructor and cannot use shared_from_this
     598                 :           0 :     impl_->stop(nullptr);
     599                 :             :   }
     600                 :         270 : }
     601                 :             : 
     602                 :           0 : TcpServer::TcpServer(TcpServer&&) noexcept = default;
     603                 :           0 : TcpServer& TcpServer::operator=(TcpServer&&) noexcept = default;
     604                 :             : 
     605                 :         140 : void TcpServer::start() {
     606                 :         140 :   auto impl = get_impl();
     607                 :         140 :   auto current = impl->state_.get();
     608   [ +  +  +  -  :         140 :   if (current == base::LinkState::Listening || current == base::LinkState::Connected ||
                   -  + ]
     609                 :             :       current == base::LinkState::Connecting) {
     610                 :           4 :     return;
     611                 :             :   }
     612                 :         139 :   impl->stopping_.store(false);
     613                 :         139 :   impl->cleanup_started_.store(false);
     614                 :             :   // Restart contract (#444): stats() resets on restart. The server-level
     615                 :             :   // counters now outlive the sessions that fed them, so clearing them here is
     616                 :             :   // what keeps that promise - before absorption they were empty and a restart
     617                 :             :   // zeroed the aggregate for free.
     618                 :         139 :   impl->stats_.reset(0);
     619                 :             : 
     620                 :             :   // Load the certificate before binding. A server asked for TLS that cannot
     621                 :             :   // provide it must not come up in plaintext instead - that is the failure mode
     622                 :             :   // where everything looks healthy and nothing is encrypted.
     623   [ +  -  +  + ]:         139 :   if (const auto tls_error = impl->init_tls(); !tls_error.empty()) {
     624                 :           3 :     WIRESTEAD_LOG_ERROR("tcp_server", "start", tls_error);
     625                 :           3 :     impl->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONFIGURATION,
     626                 :             :                                           "start", {}, tls_error, false, 0);
     627                 :           3 :     impl->state_.set(base::LinkState::Error);
     628                 :           3 :     impl->notify_state();
     629                 :           3 :     return;
     630                 :         139 :   }
     631                 :             : 
     632         [ +  + ]:         136 :   if (impl->uses_shared_context_) {
     633                 :           2 :     auto& manager = concurrency::IoContextManager::instance();
     634   [ +  -  -  + ]:           2 :     if (!manager.is_running()) {
     635                 :           0 :       manager.start();
     636                 :             :     }
     637                 :             :   }
     638                 :             : 
     639         [ -  + ]:         136 :   if (!impl->acceptor_) {
     640                 :           0 :     impl->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::SYSTEM, "start",
     641                 :             :                                           {}, "No acceptor available", false, 0);
     642                 :           0 :     impl->state_.set(base::LinkState::Error);
     643                 :           0 :     impl->notify_state();
     644                 :           0 :     return;
     645                 :             :   }
     646                 :             : 
     647   [ +  +  +  -  :         136 :   if (impl->owns_ioc_ && !impl->ioc_thread_.joinable()) {
                   +  + ]
     648   [ +  -  -  + ]:         126 :     if (impl->ioc_.stopped()) {
     649                 :           0 :       impl->ioc_.restart();
     650                 :             :     }
     651                 :             :     impl->work_guard_ =
     652                 :         126 :         std::make_unique<net::executor_work_guard<net::io_context::executor_type>>(impl->ioc_.get_executor());
     653                 :         252 :     impl->ioc_thread_ = std::jthread([impl](std::stop_token st) {
     654                 :         126 :       wirestead::concurrency::run_io_thread_init();
     655                 :             :       try {
     656                 :         252 :         std::stop_callback cb(st, [impl] { impl->ioc_.stop(); });
     657                 :         126 :         impl->ioc_.run();
     658                 :         126 :       } catch (...) {
     659                 :           0 :       }
     660                 :         252 :     });
     661                 :             :   }
     662                 :         136 :   auto self = shared_from_this();
     663         [ -  + ]:         136 :   if (impl->ioc_.get_executor().running_in_this_thread()) {
     664         [ #  # ]:           0 :     if (!impl->stopping_.load()) {
     665                 :           0 :       impl->attempt_port_binding(self, 0);
     666                 :             :     }
     667                 :             :   } else {
     668                 :         136 :     net::dispatch(impl->ioc_, [self] {
     669                 :         136 :       auto impl = self->get_impl();
     670         [ +  + ]:         136 :       if (impl->stopping_.load()) return;
     671                 :         131 :       impl->attempt_port_binding(self, 0);
     672                 :             :     });
     673                 :             :   }
     674                 :         136 : }
     675                 :             : 
     676                 :         260 : void TcpServer::stop() { impl_->stop(shared_from_this()); }
     677                 :             : 
     678                 :         120 : void TcpServer::request_stop() {
     679                 :         120 :   auto impl = get_impl();
     680         [ -  + ]:         120 :   if (impl->stopping_.load()) return;
     681                 :         120 :   auto self = shared_from_this();
     682                 :         237 :   net::post(impl->ioc_, [self] { self->stop(); });
     683                 :         120 : }
     684                 :             : 
     685                 :           1 : bool TcpServer::is_connected() const {
     686                 :           1 :   auto impl = get_impl();
     687                 :           1 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     688   [ +  -  +  -  :           2 :   return impl->current_session_ && impl->current_session_->alive();
                   +  - ]
     689                 :           1 : }
     690                 :             : 
     691                 :           1 : bool TcpServer::is_backpressure_active() const { return false; }
     692                 :             : 
     693                 :           5 : bool TcpServer::is_backpressure_active(ClientId client_id) const {
     694                 :           5 :   auto impl = get_impl();
     695                 :           5 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     696                 :           5 :   auto it = impl->sessions_.find(client_id);
     697   [ +  -  +  -  :           5 :   if (it != impl->sessions_.end() && it->second) {
                   +  - ]
     698                 :           5 :     return it->second->is_backpressure_active();
     699                 :             :   }
     700                 :           0 :   return false;
     701                 :           5 : }
     702                 :             : 
     703                 :         120 : boost::asio::any_io_executor TcpServer::get_executor() { return impl_->ioc_.get_executor(); }
     704                 :             : 
     705                 :        1886 : wrapper::RuntimeStats TcpServer::stats() const {
     706                 :        1886 :   auto impl = get_impl();
     707                 :        1886 :   auto aggregate = impl->stats_.snapshot(0, 0, false);
     708                 :        1886 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     709         [ +  + ]:        1899 :   for (const auto& entry : impl->sessions_) {
     710         [ -  + ]:          13 :     if (!entry.second) continue;
     711                 :          13 :     const auto session_stats = entry.second->stats();
     712                 :          13 :     aggregate.bytes_accepted += session_stats.bytes_accepted;
     713                 :          13 :     aggregate.messages_accepted += session_stats.messages_accepted;
     714                 :          13 :     aggregate.bytes_sent += session_stats.bytes_sent;
     715                 :          13 :     aggregate.messages_sent += session_stats.messages_sent;
     716                 :          13 :     aggregate.bytes_received += session_stats.bytes_received;
     717                 :          13 :     aggregate.messages_received += session_stats.messages_received;
     718                 :          13 :     aggregate.failed_sends += session_stats.failed_sends;
     719                 :          13 :     aggregate.dropped_messages += session_stats.dropped_messages;
     720                 :          13 :     aggregate.dropped_bytes += session_stats.dropped_bytes;
     721                 :          13 :     aggregate.backpressure_events += session_stats.backpressure_events;
     722                 :          13 :     aggregate.queued_bytes += session_stats.queued_bytes;
     723                 :          13 :     aggregate.pending_bytes += session_stats.pending_bytes;
     724                 :             :     // Peak, not a total: summing per-session peaks would report a depth no
     725                 :             :     // session ever reached, because the peaks need not have been simultaneous.
     726                 :          13 :     aggregate.max_queued_bytes = std::max(aggregate.max_queued_bytes, session_stats.max_queued_bytes);
     727   [ +  +  +  + ]:          13 :     aggregate.backpressure_active = aggregate.backpressure_active || session_stats.backpressure_active;
     728                 :             :   }
     729                 :        3772 :   return aggregate;
     730                 :        1886 : }
     731                 :             : 
     732                 :           0 : void TcpServer::reset_stats() {
     733                 :           0 :   auto impl = get_impl();
     734                 :           0 :   impl->stats_.reset(0);
     735                 :           0 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     736         [ #  # ]:           0 :   for (const auto& entry : impl->sessions_) {
     737   [ #  #  #  # ]:           0 :     if (entry.second) entry.second->reset_stats();
     738                 :             :   }
     739                 :           0 : }
     740                 :             : 
     741                 :           5 : std::optional<diagnostics::ErrorInfo> TcpServer::last_error_info() const {
     742                 :           5 :   return get_impl()->error_info_holder_.last_error_info();
     743                 :             : }
     744                 :             : 
     745                 :           2 : bool TcpServer::async_write_copy(memory::ConstByteSpan data) {
     746                 :           2 :   auto impl = get_impl();
     747         [ -  + ]:           2 :   if (impl->stopping_.load()) {
     748                 :           0 :     impl->stats_.record_failed_send();
     749                 :           0 :     return false;
     750                 :             :   }
     751                 :           2 :   std::shared_ptr<TcpServerSession> session;
     752                 :             :   {
     753                 :           2 :     std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     754                 :           2 :     session = impl->current_session_;
     755                 :           2 :   }
     756                 :             : 
     757   [ +  +  +  -  :           2 :   if (session && session->alive()) {
             +  -  +  + ]
     758                 :           1 :     return session->async_write_copy(data);
     759                 :             :   }
     760                 :           1 :   impl->stats_.record_failed_send();
     761                 :           1 :   return false;
     762                 :           2 : }
     763                 :             : 
     764                 :           2 : bool TcpServer::async_write_move(std::vector<uint8_t>&& data) {
     765                 :           2 :   auto impl = get_impl();
     766         [ -  + ]:           2 :   if (impl->stopping_.load()) {
     767                 :           0 :     impl->stats_.record_failed_send();
     768                 :           0 :     return false;
     769                 :             :   }
     770                 :           2 :   std::shared_ptr<TcpServerSession> session;
     771                 :             :   {
     772                 :           2 :     std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     773                 :           2 :     session = impl->current_session_;
     774                 :           2 :   }
     775                 :             : 
     776   [ +  +  +  -  :           2 :   if (session && session->alive()) {
             +  -  +  + ]
     777                 :           1 :     return session->async_write_move(std::move(data));
     778                 :             :   }
     779                 :           1 :   impl->stats_.record_failed_send();
     780                 :           1 :   return false;
     781                 :           2 : }
     782                 :             : 
     783                 :           2 : bool TcpServer::async_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     784                 :           2 :   auto impl = get_impl();
     785   [ +  -  +  -  :           2 :   if (impl->stopping_.load() || !data || data->empty()) {
             -  +  -  + ]
     786                 :           0 :     impl->stats_.record_failed_send();
     787                 :           0 :     return false;
     788                 :             :   }
     789                 :           2 :   std::shared_ptr<TcpServerSession> session;
     790                 :             :   {
     791                 :           2 :     std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     792                 :           2 :     session = impl->current_session_;
     793                 :           2 :   }
     794                 :             : 
     795   [ +  +  +  -  :           2 :   if (session && session->alive()) {
             +  -  +  + ]
     796                 :           1 :     return session->async_write_shared(std::move(data));
     797                 :             :   }
     798                 :           1 :   impl->stats_.record_failed_send();
     799                 :           1 :   return false;
     800                 :           2 : }
     801                 :             : 
     802                 :           2 : bool TcpServer::async_try_write_copy(memory::ConstByteSpan data) {
     803                 :           2 :   auto impl = get_impl();
     804         [ -  + ]:           2 :   if (impl->stopping_.load()) {
     805                 :           0 :     impl->stats_.record_failed_send();
     806                 :           0 :     return false;
     807                 :             :   }
     808                 :           2 :   std::shared_ptr<TcpServerSession> session;
     809                 :             :   {
     810                 :           2 :     std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     811                 :           2 :     session = impl->current_session_;
     812                 :           2 :   }
     813                 :             : 
     814   [ +  +  +  -  :           2 :   if (session && session->alive()) {
             +  -  +  + ]
     815                 :           1 :     return session->async_try_write_copy(data);
     816                 :             :   }
     817                 :           1 :   impl->stats_.record_failed_send();
     818                 :           1 :   return false;
     819                 :           2 : }
     820                 :             : 
     821                 :           2 : bool TcpServer::async_try_write_move(std::vector<uint8_t>&& data) {
     822                 :           2 :   auto impl = get_impl();
     823         [ -  + ]:           2 :   if (impl->stopping_.load()) {
     824                 :           0 :     impl->stats_.record_failed_send();
     825                 :           0 :     return false;
     826                 :             :   }
     827                 :           2 :   std::shared_ptr<TcpServerSession> session;
     828                 :             :   {
     829                 :           2 :     std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     830                 :           2 :     session = impl->current_session_;
     831                 :           2 :   }
     832                 :             : 
     833   [ +  +  +  -  :           2 :   if (session && session->alive()) {
             +  -  +  + ]
     834                 :           1 :     return session->async_try_write_move(std::move(data));
     835                 :             :   }
     836                 :           1 :   impl->stats_.record_failed_send();
     837                 :           1 :   return false;
     838                 :           2 : }
     839                 :             : 
     840                 :           2 : bool TcpServer::async_try_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     841                 :           2 :   auto impl = get_impl();
     842   [ +  -  +  -  :           2 :   if (impl->stopping_.load() || !data || data->empty()) {
             -  +  -  + ]
     843                 :           0 :     impl->stats_.record_failed_send();
     844                 :           0 :     return false;
     845                 :             :   }
     846                 :           2 :   std::shared_ptr<TcpServerSession> session;
     847                 :             :   {
     848                 :           2 :     std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     849                 :           2 :     session = impl->current_session_;
     850                 :           2 :   }
     851                 :             : 
     852   [ +  +  +  -  :           2 :   if (session && session->alive()) {
             +  -  +  + ]
     853                 :           1 :     return session->async_try_write_shared(std::move(data));
     854                 :             :   }
     855                 :           1 :   impl->stats_.record_failed_send();
     856                 :           1 :   return false;
     857                 :           2 : }
     858                 :             : 
     859                 :             : // Each setter builds the shared snapshot before taking the lock, so the
     860                 :             : // allocation stays outside the critical section the io thread contends on.
     861                 :         122 : void TcpServer::on_bytes(OnBytes cb) {
     862                 :         122 :   auto shared = interface::share_callback(std::move(cb));
     863                 :         122 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     864                 :         122 :   impl_->on_bytes_ = std::move(shared);
     865                 :         122 : }
     866                 :         247 : void TcpServer::on_state(OnState cb) {
     867                 :         247 :   auto shared = interface::share_callback(std::move(cb));
     868                 :         247 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     869                 :         247 :   impl_->on_state_ = std::move(shared);
     870                 :         247 : }
     871                 :         241 : void TcpServer::on_backpressure(OnBackpressure cb) {
     872                 :         241 :   auto impl = get_impl();
     873                 :         241 :   auto shared = interface::share_callback(std::move(cb));
     874                 :         241 :   std::shared_ptr<TcpServerSession> session;
     875                 :         241 :   interface::SharedCallback<OnBackpressure> bp_cb;
     876                 :             :   {
     877                 :         241 :     std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     878                 :         241 :     impl->on_bp_ = std::move(shared);
     879                 :         241 :     bp_cb = impl->on_bp_;
     880                 :         241 :     session = impl->current_session_;
     881                 :         241 :   }
     882                 :             : 
     883   [ +  +  -  +  :         241 :   if (session) session->on_backpressure(bp_cb ? *bp_cb : OnBackpressure{});
             -  -  +  - ]
     884                 :         241 : }
     885                 :             : 
     886                 :        1167 : bool TcpServer::broadcast(std::string_view message) {
     887                 :        1167 :   auto shared_data = std::make_shared<const std::vector<uint8_t>>(message.begin(), message.end());
     888                 :        1167 :   auto impl = get_impl();
     889                 :        1167 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     890                 :        1167 :   bool sent = false;
     891                 :        1167 :   bool attempted = false;
     892         [ +  + ]:        2485 :   for (auto& entry : impl->sessions_) {
     893                 :        1318 :     auto& session = entry.second;
     894   [ +  -  +  -  :        1318 :     if (session && session->alive()) {
             +  -  +  - ]
     895                 :        1318 :       attempted = true;
     896   [ +  -  +  + ]:        1318 :       if (session->async_try_write_shared(shared_data)) sent = true;
     897                 :             :     }
     898                 :             :   }
     899         [ +  + ]:        1167 :   if (!attempted) impl->stats_.record_failed_send();
     900                 :        1167 :   return sent;
     901                 :        1167 : }
     902                 :             : 
     903                 :           2 : bool TcpServer::broadcast(memory::ConstByteSpan data) {
     904                 :           2 :   auto shared_data = std::make_shared<const std::vector<uint8_t>>(data.begin(), data.end());
     905                 :           2 :   auto impl = get_impl();
     906                 :           2 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     907                 :           2 :   bool sent = false;
     908                 :           2 :   bool attempted = false;
     909         [ +  + ]:           3 :   for (auto& entry : impl->sessions_) {
     910                 :           1 :     auto& session = entry.second;
     911   [ +  -  +  -  :           1 :     if (session && session->alive()) {
             +  -  +  - ]
     912                 :           1 :       attempted = true;
     913   [ +  -  +  - ]:           1 :       if (session->async_try_write_shared(shared_data)) sent = true;
     914                 :             :     }
     915                 :             :   }
     916         [ +  + ]:           2 :   if (!attempted) impl->stats_.record_failed_send();
     917                 :           2 :   return sent;
     918                 :           2 : }
     919                 :             : 
     920                 :          54 : bool TcpServer::send_to_client(ClientId client_id, std::string_view message) {
     921                 :         162 :   return send_to_client(client_id,
     922                 :         162 :                         memory::ConstByteSpan(reinterpret_cast<const uint8_t*>(message.data()), message.size()));
     923                 :             : }
     924                 :             : 
     925                 :          55 : bool TcpServer::send_to_client(ClientId client_id, memory::ConstByteSpan data) {
     926                 :          55 :   auto impl = get_impl();
     927                 :          55 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     928                 :          55 :   auto it = impl->sessions_.find(client_id);
     929   [ +  +  +  -  :          55 :   if (it != impl->sessions_.end() && it->second && it->second->alive()) {
          +  -  +  -  +  
                      + ]
     930                 :          54 :     return it->second->async_write_copy(data);
     931                 :             :   }
     932                 :           1 :   impl->stats_.record_failed_send();
     933                 :           1 :   return false;
     934                 :          55 : }
     935                 :             : 
     936                 :           5 : bool TcpServer::try_send_to_client(ClientId client_id, std::string_view message) {
     937                 :          15 :   return try_send_to_client(client_id,
     938                 :          15 :                             memory::ConstByteSpan(reinterpret_cast<const uint8_t*>(message.data()), message.size()));
     939                 :             : }
     940                 :             : 
     941                 :           6 : bool TcpServer::try_send_to_client(ClientId client_id, memory::ConstByteSpan data) {
     942                 :           6 :   auto impl = get_impl();
     943                 :           6 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     944                 :           6 :   auto it = impl->sessions_.find(client_id);
     945   [ +  +  +  -  :           6 :   if (it != impl->sessions_.end() && it->second && it->second->alive()) {
          +  -  +  -  +  
                      + ]
     946                 :           4 :     return it->second->async_try_write_copy(data);
     947                 :             :   }
     948                 :           2 :   impl->stats_.record_failed_send();
     949                 :           2 :   return false;
     950                 :           6 : }
     951                 :             : 
     952                 :         105 : size_t TcpServer::client_count() const {
     953                 :         105 :   auto impl = get_impl();
     954                 :         105 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     955                 :         105 :   size_t alive = 0;
     956         [ +  + ]:         251 :   for (const auto& entry : impl->sessions_)
     957   [ +  -  +  -  :         146 :     if (entry.second && entry.second->alive()) ++alive;
             +  -  +  - ]
     958                 :         105 :   return alive;
     959                 :         105 : }
     960                 :             : 
     961                 :          27 : std::optional<wrapper::RuntimeStats> TcpServer::client_stats(ClientId client_id) const {
     962                 :          27 :   auto impl = get_impl();
     963                 :          27 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     964                 :          27 :   auto it = impl->sessions_.find(client_id);
     965   [ +  +  -  +  :          27 :   if (it == impl->sessions_.end() || !it->second) return std::nullopt;
                   +  + ]
     966                 :          24 :   return it->second->stats();
     967                 :          27 : }
     968                 :             : 
     969                 :           6 : std::vector<ClientId> TcpServer::connected_clients() const {
     970                 :           6 :   auto impl = get_impl();
     971                 :           6 :   std::lock_guard<std::mutex> lock(impl->sessions_mutex_);
     972                 :           6 :   std::vector<ClientId> connected_clients;
     973                 :           6 :   connected_clients.reserve(impl->sessions_.size());
     974         [ +  + ]:          14 :   for (const auto& entry : impl->sessions_)
     975   [ +  -  +  -  :           8 :     if (entry.second && entry.second->alive()) connected_clients.push_back(entry.first);
          +  -  +  -  +  
                      - ]
     976                 :          12 :   return connected_clients;
     977                 :           6 : }
     978                 :             : 
     979                 :         121 : void TcpServer::on_multi_connect(MultiClientConnectHandler h) {
     980                 :         121 :   std::lock_guard<std::mutex> l(impl_->sessions_mutex_);
     981                 :         121 :   impl_->on_multi_connect_ = std::move(h);
     982                 :         121 : }
     983                 :         120 : void TcpServer::on_multi_data(MultiClientDataHandler h) {
     984                 :         120 :   auto shared = interface::share_callback(std::move(h));
     985                 :         120 :   std::lock_guard<std::mutex> l(impl_->sessions_mutex_);
     986                 :         120 :   impl_->on_multi_data_ = std::move(shared);
     987                 :         120 : }
     988                 :         120 : void TcpServer::on_multi_disconnect(MultiClientDisconnectHandler h) {
     989                 :         120 :   std::lock_guard<std::mutex> l(impl_->sessions_mutex_);
     990                 :         120 :   impl_->on_multi_disconnect_ = std::move(h);
     991                 :         120 : }
     992                 :             : 
     993                 :           8 : void TcpServer::set_client_limit(size_t max) {
     994                 :           8 :   auto impl = get_impl();
     995         [ -  + ]:           8 :   if (max > base::constants::MAX_MAX_CONNECTIONS) {
     996                 :           0 :     max = base::constants::MAX_MAX_CONNECTIONS;
     997                 :             :   }
     998                 :           8 :   std::lock_guard<std::mutex> l(impl->sessions_mutex_);
     999                 :           8 :   impl->max_clients_ = max;
    1000         [ -  + ]:           8 :   if (max == 0) {
    1001                 :           0 :     impl->client_limit_enabled_ = false;
    1002                 :             :   } else {
    1003                 :           8 :     impl->client_limit_enabled_ = true;
    1004                 :             :   }
    1005                 :           8 : }
    1006                 :             : 
    1007                 :          21 : base::LinkState TcpServer::state() const { return get_impl()->state_.get(); }
    1008                 :             : 
    1009                 :             : }  // namespace transport
    1010                 :             : }  // namespace wirestead
        

Generated by: LCOV version 2.0-1