LCOV - code coverage report
Current view: top level - wirestead/wrapper/udp - udp_server.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 90.4 % 498 450
Test Date: 2026-08-30 10:35:09 Functions: 90.8 % 76 69
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 64.8 % 244 158

             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/wrapper/udp/udp_server.hpp"
      18                 :             : 
      19                 :             : #include <spdlog/fmt/fmt.h>
      20                 :             : 
      21                 :             : #include <boost/asio/executor_work_guard.hpp>
      22                 :             : #include <boost/asio/io_context.hpp>
      23                 :             : #include <boost/asio/steady_timer.hpp>
      24                 :             : #include <iostream>
      25                 :             : #include <mutex>
      26                 :             : #include <shared_mutex>
      27                 :             : #include <stop_token>
      28                 :             : #include <thread>
      29                 :             : #include <unordered_map>
      30                 :             : #include <vector>
      31                 :             : 
      32                 :             : #include "wirestead/base/common.hpp"
      33                 :             : #include "wirestead/concurrency/io_thread_hook.hpp"
      34                 :             : #include "wirestead/factory/channel_factory.hpp"
      35                 :             : #include "wirestead/transport/udp/udp.hpp"
      36                 :             : #include "wirestead/wrapper/callback_guard.hpp"
      37                 :             : #include "wirestead/wrapper/error_context_builder.hpp"
      38                 :             : 
      39                 :             : namespace wirestead {
      40                 :             : namespace wrapper {
      41                 :             : 
      42                 :             : namespace {
      43                 :             : // std::hash<boost::asio::ip::udp::endpoint> is not available before Boost 1.74.
      44                 :             : // Provide a portable hash by combining the raw address bytes and port.
      45                 :             : struct UdpEndpointHash {
      46                 :          23 :   std::size_t operator()(const boost::asio::ip::udp::endpoint& ep) const noexcept {
      47                 :          23 :     std::size_t seed = 0;
      48                 :         138 :     auto combine = [&](std::size_t v) { seed ^= v + 0x9e3779b9u + (seed << 6) + (seed >> 2); };
      49         [ +  - ]:          23 :     if (ep.address().is_v4()) {
      50         [ +  + ]:         115 :       for (auto byte : ep.address().to_v4().to_bytes()) {
      51                 :          92 :         combine(std::hash<unsigned char>{}(byte));
      52                 :             :       }
      53                 :             :     } else {
      54         [ #  # ]:           0 :       for (auto byte : ep.address().to_v6().to_bytes()) {
      55                 :           0 :         combine(std::hash<unsigned char>{}(byte));
      56                 :             :       }
      57                 :             :     }
      58                 :          23 :     combine(std::hash<unsigned short>{}(ep.port()));
      59                 :          23 :     return seed;
      60                 :             :   }
      61                 :             : };
      62                 :             : }  // namespace
      63                 :             : 
      64                 :             : struct UdpServer::Impl : public std::enable_shared_from_this<Impl> {
      65                 :             :   config::UdpConfig cfg;
      66                 :             :   std::shared_ptr<transport::UdpChannel> channel;
      67                 :             :   std::shared_ptr<boost::asio::io_context> external_ioc;
      68                 :             :   std::atomic<bool> use_external_context{false};
      69                 :             :   std::atomic<bool> manage_external_context{false};
      70                 :             :   std::jthread external_thread;
      71                 :             :   std::unique_ptr<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> work_guard;
      72                 :             : 
      73                 :             :   mutable std::shared_mutex mutex;
      74                 :             :   std::mutex bp_mutex_;
      75                 :             :   std::condition_variable bp_cv_;
      76                 :             :   std::vector<std::promise<bool>> pending_promises;
      77                 :             :   std::atomic<bool> started{false};
      78                 :             :   std::atomic<bool> is_listening{false};
      79                 :             : 
      80                 :             :   // Virtual Session Management
      81                 :             :   struct SessionEntry {
      82                 :             :     boost::asio::ip::udp::endpoint endpoint;
      83                 :             :     std::shared_ptr<framer::IFramer> framer;
      84                 :             :     std::chrono::steady_clock::time_point last_seen;
      85                 :             :   };
      86                 :             :   ClientId next_client_id{1};
      87                 :             :   std::unordered_map<boost::asio::ip::udp::endpoint, ClientId, UdpEndpointHash> endpoint_to_id;
      88                 :             :   std::unordered_map<ClientId, SessionEntry> sessions;
      89                 :          29 :   std::chrono::milliseconds session_timeout{0};  // 0 = disabled
      90                 :             :   std::unique_ptr<boost::asio::steady_timer> reaper_timer;
      91                 :             :   std::atomic<bool> auto_start{false};
      92                 :             :   std::atomic<bool> client_limit_enabled{false};
      93                 :             :   std::atomic<size_t> max_clients_limit{0};
      94                 :             : 
      95                 :             :   ConnectionHandler on_connect{nullptr};
      96                 :             :   ConnectionHandler on_disconnect{nullptr};
      97                 :             :   // Shared snapshots: the strand copies one out per received datagram, and a
      98                 :             :   // std::function copy allocates whenever the user handler outgrows its
      99                 :             :   // small-object buffer. See interface::SharedCallback.
     100                 :             :   interface::SharedCallback<MessageHandler> on_data;
     101                 :             :   interface::SharedCallback<BatchMessageHandler> on_data_batch_;
     102                 :             :   ErrorHandler on_error{nullptr};
     103                 :             :   std::function<void(size_t)> bp_handler{nullptr};
     104                 :             :   FramerFactory framer_factory{nullptr};
     105                 :             :   interface::SharedCallback<MessageHandler> on_message;
     106                 :             :   interface::SharedCallback<BatchMessageHandler> on_message_batch_;
     107                 :             : 
     108                 :          29 :   std::shared_ptr<bool> is_alive{std::make_shared<bool>(true)};
     109                 :             : 
     110                 :             :   // Batching logic
     111                 :             :   std::vector<MessageContext> data_batch_queue_;
     112                 :             :   std::vector<MessageContext> message_batch_queue_;
     113                 :             :   std::unique_ptr<boost::asio::steady_timer> batch_timer_;
     114                 :             :   size_t max_batch_size_ = 100;
     115                 :          29 :   std::chrono::milliseconds max_batch_latency_{1};
     116                 :             : 
     117                 :          25 :   explicit Impl(const config::UdpConfig& config) : cfg(config) {}
     118                 :           3 :   Impl(const config::UdpConfig& config, std::shared_ptr<boost::asio::io_context> ioc)
     119   [ +  -  +  - ]:           3 :       : cfg(config), external_ioc(std::move(ioc)), use_external_context(external_ioc != nullptr) {}
     120                 :           1 :   explicit Impl(std::shared_ptr<interface::Channel> ch)
     121                 :           1 :       : channel(std::dynamic_pointer_cast<transport::UdpChannel>(ch)) {
     122                 :             :     // #450: setup_internal_handlers() captures weak_from_this() - calling it
     123                 :             :     // from inside this constructor would capture an empty weak_ptr, since
     124                 :             :     // enable_shared_from_this isn't wired up until make_shared() finishes
     125                 :             :     // constructing the object. Deferred to UdpServer's own constructor,
     126                 :             :     // which runs after impl_ is a fully-formed shared_ptr<Impl>.
     127                 :           1 :   }
     128                 :             : 
     129                 :          29 :   ~Impl() {
     130                 :          29 :     *is_alive = false;
     131                 :             :     try {
     132                 :          29 :       stop();
     133                 :           0 :     } catch (...) {
     134                 :           0 :     }
     135                 :          29 :   }
     136                 :             : 
     137                 :         103 :   void fulfill_all_locked(bool value) {
     138         [ +  + ]:         124 :     for (auto& promise : pending_promises) {
     139                 :             :       try {
     140                 :          21 :         promise.set_value(value);
     141                 :           0 :       } catch (...) {
     142                 :           0 :       }
     143                 :             :     }
     144                 :         103 :     pending_promises.clear();
     145                 :         103 :   }
     146                 :             : 
     147                 :           2 :   void flush_batches() {
     148                 :           2 :     std::unique_lock<std::shared_mutex> lock(mutex);
     149         [ +  - ]:           2 :     if (!data_batch_queue_.empty()) {
     150                 :           2 :       auto handler = on_data_batch_;
     151                 :           2 :       auto batch = std::move(data_batch_queue_);
     152                 :           2 :       data_batch_queue_.clear();
     153         [ +  - ]:           2 :       if (handler) {
     154                 :           2 :         lock.unlock();
     155                 :           2 :         detail::invoke_user_callback("udp_server", "on_data_batch", handler, batch);
     156                 :           2 :         lock.lock();
     157                 :             :       }
     158                 :           2 :     }
     159         [ +  + ]:           2 :     if (!message_batch_queue_.empty()) {
     160                 :           1 :       auto handler = on_message_batch_;
     161                 :           1 :       auto batch = std::move(message_batch_queue_);
     162                 :           1 :       message_batch_queue_.clear();
     163         [ +  - ]:           1 :       if (handler) {
     164                 :           1 :         lock.unlock();
     165                 :           1 :         detail::invoke_user_callback("udp_server", "on_message_batch", handler, batch);
     166                 :           1 :         lock.lock();
     167                 :             :       }
     168                 :           1 :     }
     169         [ +  - ]:           2 :     if (batch_timer_) {
     170                 :           2 :       batch_timer_->cancel();
     171                 :             :     }
     172                 :           2 :   }
     173                 :             : 
     174                 :           5 :   void schedule_batch_timer() {
     175         [ -  + ]:           5 :     if (!batch_timer_) return;
     176                 :           5 :     batch_timer_->expires_after(max_batch_latency_);
     177                 :          10 :     batch_timer_->async_wait([this, weak_impl = weak_from_this(),
     178                 :           5 :                               alive = std::weak_ptr<bool>(is_alive)](const boost::system::error_code& ec) {
     179                 :           5 :       auto impl_keepalive = weak_impl.lock();
     180         [ -  + ]:           5 :       if (!impl_keepalive) return;
     181                 :           5 :       auto lock = alive.lock();
     182   [ +  -  -  +  :           5 :       if (!lock || !(*lock)) return;
                   -  + ]
     183                 :             : 
     184         [ +  + ]:           5 :       if (!ec) {
     185                 :           2 :         flush_batches();
     186                 :             :       }
     187                 :           5 :     });
     188                 :             :   }
     189                 :             : 
     190                 :           3 :   void schedule_reaper() {
     191   [ +  -  +  -  :           3 :     if (!started.load() || !reaper_timer || session_timeout.count() <= 0) return;
             -  +  -  + ]
     192                 :             : 
     193                 :             :     // Run reaper at interval proportional to timeout (min 100ms, max 5s)
     194                 :             :     auto interval =
     195                 :           3 :         std::max(std::chrono::milliseconds(100), std::min(std::chrono::milliseconds(5000), session_timeout / 2));
     196                 :             : 
     197                 :           3 :     reaper_timer->expires_after(interval);
     198                 :           6 :     reaper_timer->async_wait([this, weak_impl = weak_from_this(),
     199                 :           3 :                               alive = std::weak_ptr<bool>(is_alive)](const boost::system::error_code& ec) {
     200                 :           3 :       auto impl_keepalive = weak_impl.lock();
     201         [ -  + ]:           3 :       if (!impl_keepalive) return;
     202                 :           3 :       auto lock = alive.lock();
     203   [ +  -  -  +  :           3 :       if (!lock || !(*lock)) return;
                   -  + ]
     204                 :             : 
     205         [ +  + ]:           3 :       if (!ec) {
     206                 :           2 :         run_reaper();
     207                 :           2 :         schedule_reaper();
     208                 :             :       }
     209                 :           3 :     });
     210                 :             :   }
     211                 :             : 
     212                 :           2 :   void run_reaper() {
     213         [ -  + ]:           2 :     if (session_timeout.count() <= 0) return;
     214                 :             : 
     215                 :           2 :     std::vector<std::pair<ClientId, std::string>> to_remove_with_info;
     216                 :           2 :     auto now = std::chrono::steady_clock::now();
     217                 :             : 
     218                 :           2 :     ConnectionHandler disconnect_handler;
     219                 :             :     {
     220                 :           2 :       std::unique_lock<std::shared_mutex> lock(mutex);
     221         [ +  + ]:           4 :       for (auto it = sessions.begin(); it != sessions.end();) {
     222   [ +  -  +  -  :           2 :         if (now - it->second.last_seen > session_timeout) {
                   +  + ]
     223                 :             :           std::string info =
     224                 :           1 :               fmt::format("{}:{}", it->second.endpoint.address().to_string(), it->second.endpoint.port());
     225                 :           1 :           endpoint_to_id.erase(it->second.endpoint);
     226                 :           1 :           to_remove_with_info.push_back({it->first, info});
     227                 :           1 :           it = sessions.erase(it);
     228                 :           1 :         } else {
     229                 :           1 :           ++it;
     230                 :             :         }
     231                 :             :       }
     232                 :           2 :       disconnect_handler = on_disconnect;
     233                 :           2 :     }
     234                 :             : 
     235                 :             :     // Call disconnect handlers outside the lock
     236         [ +  + ]:           3 :     for (auto const& [id, info] : to_remove_with_info) {
     237                 :           1 :       detail::invoke_user_callback("udp_server", "on_disconnect", disconnect_handler, ConnectionContext(id, info));
     238                 :             :     }
     239                 :           2 :   }
     240                 :             : 
     241                 :          21 :   void setup_internal_handlers() {
     242         [ -  + ]:          21 :     if (!channel) return;
     243                 :             : 
     244                 :          21 :     batch_timer_ = std::make_unique<boost::asio::steady_timer>(channel->get_executor());
     245                 :             : 
     246                 :          21 :     std::weak_ptr<Impl> weak_impl = weak_from_this();
     247                 :             : 
     248                 :          21 :     channel->on_bytes_from([this, weak_impl](memory::ConstByteSpan data, const boost::asio::ip::udp::endpoint& ep) {
     249                 :             :       // #450: keep Impl alive for the duration of this callback - on an
     250                 :             :       // externally-owned io_context, stop() doesn't join/wait for in-flight
     251                 :             :       // handlers, so a bare `this` could otherwise dangle.
     252                 :          20 :       auto impl_keepalive = weak_impl.lock();
     253         [ -  + ]:          20 :       if (!impl_keepalive) return;
     254                 :             : 
     255                 :             :       // #449: everything below runs synchronously on this io thread - mark
     256                 :             :       // it so a blocking send_to() called from within one of these
     257                 :             :       // callbacks fails fast instead of deadlocking.
     258                 :          20 :       detail::CallbackGuard callback_guard;
     259                 :             : 
     260                 :          20 :       ClientId client_id = 0;
     261                 :          20 :       bool is_new = false;
     262                 :          20 :       ConnectionHandler connect_handler_copy{nullptr};
     263                 :             : 
     264                 :             :       {
     265                 :          20 :         std::unique_lock<std::shared_mutex> lock(mutex);
     266                 :          20 :         auto it = endpoint_to_id.find(ep);
     267         [ +  + ]:          20 :         if (it == endpoint_to_id.end()) {
     268   [ +  +  +  +  :          21 :           if (client_limit_enabled.load() && sessions.size() >= max_clients_limit.load()) {
                   +  + ]
     269                 :           1 :             return;
     270                 :             :           }
     271                 :          18 :           client_id = next_client_id++;
     272                 :          18 :           endpoint_to_id[ep] = client_id;
     273                 :          18 :           SessionEntry entry;
     274                 :          18 :           entry.endpoint = ep;
     275                 :          18 :           entry.last_seen = std::chrono::steady_clock::now();
     276                 :          18 :           is_new = true;
     277                 :             : 
     278                 :             :           // Create framer for new session
     279         [ +  + ]:          18 :           if (framer_factory) {
     280                 :           3 :             auto framer = framer_factory();
     281         [ +  - ]:           3 :             if (framer) {
     282                 :           3 :               framer->on_message([this, client_id](memory::ConstByteSpan msg) {
     283                 :             :                 // #441: snapshot under a shared_lock (pure read), build the
     284                 :             :                 // copy before taking the exclusive lock for queue mutation.
     285                 :             :                 bool batch_mode;
     286                 :           4 :                 interface::SharedCallback<MessageHandler> on_message_handler;
     287                 :             :                 {
     288                 :           4 :                   std::shared_lock<std::shared_mutex> lock(mutex);
     289                 :           4 :                   batch_mode = static_cast<bool>(on_message_batch_);
     290                 :           4 :                   on_message_handler = on_message;
     291                 :           4 :                 }
     292                 :             : 
     293         [ +  + ]:           4 :                 if (batch_mode) {
     294                 :           3 :                   MessageContext ctx(client_id, memory::SafeDataBuffer(msg));
     295                 :           3 :                   interface::SharedCallback<BatchMessageHandler> flush_handler;
     296                 :           3 :                   std::vector<MessageContext> batch;
     297                 :             :                   {
     298                 :           3 :                     std::unique_lock<std::shared_mutex> lock(mutex);
     299                 :           3 :                     message_batch_queue_.emplace_back(std::move(ctx));
     300         [ +  + ]:           3 :                     if (message_batch_queue_.size() >= max_batch_size_) {
     301                 :           1 :                       flush_handler = on_message_batch_;
     302                 :           1 :                       batch = std::move(message_batch_queue_);
     303                 :           1 :                       message_batch_queue_.clear();
     304         [ +  - ]:           2 :                     } else if (message_batch_queue_.size() == 1) {
     305                 :           2 :                       schedule_batch_timer();
     306                 :             :                     }
     307                 :           3 :                   }
     308                 :           3 :                   detail::invoke_user_callback("udp_server", "on_message_batch", flush_handler, batch);
     309                 :           3 :                   return;
     310                 :           3 :                 }
     311                 :             : 
     312                 :           1 :                 detail::invoke_user_callback("udp_server", "on_message", on_message_handler,
     313                 :           2 :                                              MessageContext(client_id, msg));
     314                 :           4 :               });
     315                 :           3 :               entry.framer = std::move(framer);
     316                 :             :             }
     317                 :           3 :           }
     318                 :          18 :           sessions[client_id] = std::move(entry);
     319                 :          18 :         } else {
     320                 :           1 :           client_id = it->second;
     321                 :           1 :           sessions[client_id].last_seen = std::chrono::steady_clock::now();
     322                 :             :         }
     323                 :          19 :         connect_handler_copy = on_connect;
     324                 :          20 :       }
     325                 :             : 
     326         [ +  + ]:          19 :       if (is_new) {
     327                 :          18 :         detail::invoke_user_callback(
     328                 :             :             "udp_server", "on_connect", connect_handler_copy,
     329                 :          36 :             ConnectionContext(client_id, fmt::format("{}:{}", ep.address().to_string(), ep.port())));
     330                 :             :       }
     331                 :             : 
     332                 :             :       {
     333                 :             :         // #441: snapshot the handler under a shared_lock (not unique_lock) -
     334                 :             :         // this is a pure read, matching try_send's locking level so it no
     335                 :             :         // longer blocks concurrent sends even briefly.
     336                 :             :         bool batch_mode;
     337                 :          19 :         interface::SharedCallback<MessageHandler> data_handler_copy;
     338                 :             :         {
     339                 :          19 :           std::shared_lock<std::shared_mutex> lock(mutex);
     340                 :          19 :           batch_mode = static_cast<bool>(on_data_batch_);
     341                 :          19 :           data_handler_copy = on_data;
     342                 :          19 :         }
     343                 :             : 
     344         [ +  + ]:          19 :         if (batch_mode) {
     345                 :             :           // #441: build the copy before taking the exclusive lock, so the
     346                 :             :           // lock is only held for the queue mutation itself, not the
     347                 :             :           // allocation.
     348                 :           4 :           MessageContext ctx(client_id, memory::SafeDataBuffer(data));
     349                 :           4 :           interface::SharedCallback<BatchMessageHandler> flush_handler;
     350                 :           4 :           std::vector<MessageContext> batch;
     351                 :             :           {
     352                 :           4 :             std::unique_lock<std::shared_mutex> lock(mutex);
     353                 :           4 :             data_batch_queue_.emplace_back(std::move(ctx));
     354         [ +  + ]:           4 :             if (data_batch_queue_.size() >= max_batch_size_) {
     355                 :           1 :               flush_handler = on_data_batch_;
     356                 :           1 :               batch = std::move(data_batch_queue_);
     357                 :           1 :               data_batch_queue_.clear();
     358         [ +  - ]:           3 :             } else if (data_batch_queue_.size() == 1) {
     359                 :           3 :               schedule_batch_timer();
     360                 :             :             }
     361                 :           4 :           }
     362                 :           4 :           detail::invoke_user_callback("udp_server", "on_data_batch", flush_handler, batch);
     363                 :           4 :         } else {
     364                 :          30 :           detail::invoke_user_callback("udp_server", "on_data", data_handler_copy, MessageContext(client_id, data));
     365                 :             :         }
     366                 :          19 :       }
     367                 :             : 
     368                 :             :       // Push to framer
     369                 :          19 :       std::shared_ptr<framer::IFramer> target_framer;
     370                 :             :       {
     371                 :          19 :         std::shared_lock<std::shared_mutex> lock(mutex);
     372                 :          19 :         auto it = sessions.find(client_id);
     373         [ +  - ]:          19 :         if (it != sessions.end()) {
     374                 :          19 :           target_framer = it->second.framer;
     375                 :             :         }
     376                 :          19 :       }
     377         [ +  + ]:          19 :       if (target_framer) {
     378                 :           3 :         target_framer->push_bytes(data);
     379                 :             :       }
     380                 :          22 :     });
     381                 :             : 
     382                 :          21 :     channel->on_backpressure([this, weak_impl](size_t queued) {
     383                 :           0 :       bp_cv_.notify_all();
     384                 :           0 :       auto impl_keepalive = weak_impl.lock();
     385         [ #  # ]:           0 :       if (!impl_keepalive) return;
     386                 :           0 :       std::function<void(size_t)> handler;
     387                 :             :       {
     388                 :           0 :         std::shared_lock<std::shared_mutex> lock(mutex);
     389                 :           0 :         handler = bp_handler;
     390                 :           0 :       }
     391                 :           0 :       detail::invoke_user_callback("udp_server", "on_backpressure", handler, queued);
     392                 :           0 :     });
     393                 :             : 
     394                 :          21 :     channel->on_state([this, weak_impl](base::LinkState state) {
     395                 :          77 :       auto impl_keepalive = weak_impl.lock();
     396         [ +  + ]:          77 :       if (!impl_keepalive) return;
     397                 :          74 :       ErrorHandler error_handler_copy{nullptr};
     398   [ +  +  +  + ]:          74 :       if (state == base::LinkState::Listening || state == base::LinkState::Connected) {
     399                 :          36 :         is_listening.store(true);
     400                 :          36 :         std::unique_lock<std::shared_mutex> lock(mutex);
     401                 :          36 :         fulfill_all_locked(true);
     402   [ +  +  +  +  :          74 :       } else if (state == base::LinkState::Error || state == base::LinkState::Closed ||
                   -  + ]
     403                 :             :                  state == base::LinkState::Idle) {
     404                 :          18 :         is_listening.store(false);
     405                 :          18 :         std::unique_lock<std::shared_mutex> lock(mutex);
     406                 :          18 :         fulfill_all_locked(false);
     407         [ +  + ]:          18 :         if (state == base::LinkState::Error) {
     408                 :           1 :           error_handler_copy = on_error;
     409                 :             :         }
     410                 :          18 :       }
     411                 :             : 
     412         [ +  - ]:          74 :       detail::invoke_user_callback("udp_server", "on_error", error_handler_copy,
     413   [ +  -  +  -  :         148 :                                    channel ? detail::build_error_context(*channel, "Server error")
                   -  - ]
     414                 :             :                                            : ErrorContext(ErrorCode::IoError, "Server error"));
     415                 :          77 :     });
     416                 :          21 :   }
     417                 :             : 
     418                 :          21 :   std::future<bool> start() {
     419                 :          21 :     std::unique_lock<std::shared_mutex> lock(mutex);
     420         [ -  + ]:          21 :     if (is_listening.load()) {
     421                 :           0 :       std::promise<bool> p;
     422                 :           0 :       p.set_value(true);
     423                 :           0 :       return p.get_future();
     424                 :           0 :     }
     425                 :             : 
     426                 :          21 :     std::promise<bool> p;
     427                 :          21 :     auto fut = p.get_future();
     428                 :          21 :     pending_promises.emplace_back(std::move(p));
     429                 :             : 
     430         [ -  + ]:          21 :     if (started.exchange(true)) {
     431                 :           0 :       return fut;
     432                 :             :     }
     433                 :             : 
     434         [ +  + ]:          21 :     if (!channel) {
     435                 :          20 :       channel = std::dynamic_pointer_cast<transport::UdpChannel>(factory::ChannelFactory::create(cfg, external_ioc));
     436                 :          20 :       setup_internal_handlers();
     437                 :             :     }
     438                 :             : 
     439                 :          21 :     lock.unlock();
     440                 :          21 :     channel->start();
     441                 :             : 
     442                 :          21 :     lock.lock();
     443   [ +  +  +  -  :          21 :     if (use_external_context.load() && manage_external_context.load() && !external_thread.joinable()) {
             +  -  +  + ]
     444   [ +  -  -  +  :           1 :       if (external_ioc->stopped()) external_ioc->restart();
                   -  - ]
     445                 :           2 :       work_guard = std::make_unique<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>>(
     446                 :           3 :           external_ioc->get_executor());
     447                 :           2 :       external_thread = std::jthread([ioc = external_ioc](std::stop_token st) {
     448                 :           1 :         wirestead::concurrency::run_io_thread_init();
     449                 :             :         try {
     450                 :           2 :           std::stop_callback cb(st, [ioc] { ioc->stop(); });
     451                 :           1 :           ioc->run();
     452                 :           1 :         } catch (...) {
     453                 :           0 :         }
     454                 :           2 :       });
     455                 :             :     }
     456                 :             : 
     457   [ +  -  +  +  :          21 :     if (channel && session_timeout.count() > 0) {
                   +  + ]
     458                 :           1 :       reaper_timer = std::make_unique<boost::asio::steady_timer>(channel->get_executor());
     459                 :           1 :       schedule_reaper();
     460                 :             :     }
     461                 :             : 
     462                 :          21 :     return fut;
     463                 :          21 :   }
     464                 :             : 
     465                 :          49 :   void stop() {
     466                 :          49 :     bool should_join = false;
     467                 :             :     {
     468                 :          49 :       std::unique_lock<std::shared_mutex> lock(mutex);
     469         [ +  + ]:          49 :       if (!started.exchange(false)) {
     470                 :          28 :         is_listening.store(false);
     471                 :          28 :         fulfill_all_locked(false);
     472                 :          28 :         return;
     473                 :             :       }
     474                 :          21 :       bp_cv_.notify_all();
     475                 :             : 
     476         [ +  + ]:          21 :       if (reaper_timer) {
     477                 :           1 :         reaper_timer->cancel();
     478                 :           1 :         reaper_timer.reset();
     479                 :             :       }
     480                 :             : 
     481         [ +  - ]:          21 :       if (batch_timer_) {
     482                 :          21 :         batch_timer_->cancel();
     483                 :          21 :         batch_timer_.reset();
     484                 :             :       }
     485                 :             : 
     486         [ +  - ]:          21 :       if (channel) {
     487                 :          21 :         lock.unlock();
     488                 :          21 :         channel->stop();
     489                 :             :         // Clear callbacks after stop() rather than before: the
     490                 :             :         // transport-level fix (#436) already synchronizes callback reads
     491                 :             :         // against these setters, but clearing after stop() means no
     492                 :             :         // in-flight handler can observe a null callback mid-shutdown in the
     493                 :             :         // first place - belt and braces once the underlying race is fixed
     494                 :             :         // at the source. Also now clears on_backpressure, which this path
     495                 :             :         // previously never did at all.
     496                 :          21 :         channel->on_bytes_from(nullptr);
     497                 :          21 :         channel->on_state(nullptr);
     498                 :          21 :         channel->on_backpressure(nullptr);
     499                 :          21 :         lock.lock();
     500                 :             :       }
     501                 :             : 
     502   [ +  +  +  -  :          21 :       if (use_external_context.load() && manage_external_context.load()) {
                   +  + ]
     503   [ +  -  +  - ]:           1 :         if (external_ioc) external_ioc->stop();
     504                 :           1 :         should_join = true;
     505                 :             :       }
     506                 :             : 
     507                 :          21 :       is_listening.store(false);
     508                 :          21 :       endpoint_to_id.clear();
     509                 :          21 :       sessions.clear();
     510                 :          21 :       next_client_id = 1;
     511                 :          21 :       fulfill_all_locked(false);
     512                 :          49 :     }
     513                 :             : 
     514   [ +  +  +  -  :          21 :     if (should_join && external_thread.joinable()) {
                   +  + ]
     515                 :             :       try {
     516         [ +  - ]:           1 :         if (std::this_thread::get_id() != external_thread.get_id()) {
     517                 :           1 :           external_thread.request_stop();
     518                 :           1 :           external_thread.join();
     519                 :             :         } else {
     520                 :           0 :           external_thread.detach();
     521                 :             :         }
     522                 :           0 :       } catch (...) {
     523                 :           0 :       }
     524                 :             :     }
     525                 :          21 :     std::unique_lock<std::shared_mutex> lock(mutex);
     526                 :          21 :     channel.reset();
     527                 :          21 :   }
     528                 :             : 
     529                 :           5 :   bool send_to(ClientId client_id, std::string_view data) {
     530         [ +  + ]:           5 :     if (cfg.backpressure_strategy == base::constants::BackpressureStrategy::Reliable)
     531                 :           4 :       return send_to_blocking(client_id, data);
     532                 :           1 :     return try_send_to(client_id, data);
     533                 :             :   }
     534                 :             : 
     535                 :          39 :   bool try_broadcast(std::string_view data) {
     536                 :          39 :     std::shared_lock<std::shared_mutex> lock(mutex);
     537         [ +  + ]:          39 :     if (!channel) return false;
     538                 :          38 :     bool sent = false;
     539                 :          38 :     auto bytes = base::safe_convert::string_to_bytes(data);
     540         [ +  + ]:         108 :     for (const auto& [id, entry] : sessions) {
     541                 :          70 :       sent |= channel->async_try_write_to(memory::ConstByteSpan(bytes.first, bytes.second), entry.endpoint);
     542                 :             :     }
     543                 :          38 :     return sent;
     544                 :          39 :   }
     545                 :             : 
     546                 :          36 :   bool broadcast(std::string_view data) { return try_broadcast(data); }
     547                 :             : 
     548                 :             :   // channel->on_backpressure() calls bp_cv_.notify_all() from the transport's io_context
     549                 :             :   // thread without holding bp_mutex_ (backpressure_active_ is a plain atomic on the transport
     550                 :             :   // side, not guarded by bp_mutex_ at all). That makes a classic lost-wakeup race possible: a
     551                 :             :   // waiter can check the predicate, find it still blocking, and be in the process of
     552                 :             :   // registering to wait when the notify fires - in the rare case that race is lost, an
     553                 :             :   // unbounded wait() would block forever. Poll with a bounded timeout instead so a missed
     554                 :             :   // notify only costs a short delay rather than a permanent hang (see #427, #431).
     555                 :             :   //
     556                 :             :   // Returns false without sending instead of waiting if called from the
     557                 :             :   // channel's own io thread while backpressure is active - e.g. a blocking
     558                 :             :   // send_to() called from inside an on_data/on_message callback. Clearing
     559                 :             :   // backpressure requires that same io thread to make progress, so
     560                 :             :   // blocking here would deadlock forever rather than eventually clear
     561                 :             :   // (#449).
     562                 :             :   // #509: see identical rationale in wrapper/tcp_server/tcp_server.cc -
     563                 :             :   // bounded retry rather than a single attempt after the wait exits.
     564                 :             :   static constexpr int kMaxBlockingSendAttempts = 5;
     565                 :             : 
     566                 :           5 :   bool send_to_blocking(ClientId client_id, std::string_view data) {
     567         [ +  + ]:          10 :     for (int attempt = 0; attempt < kMaxBlockingSendAttempts; ++attempt) {
     568                 :           9 :       std::unique_lock<std::mutex> bp_lock(bp_mutex_);
     569                 :          18 :       auto predicate = [this] {
     570                 :          18 :         std::shared_lock<std::shared_mutex> lock(mutex);
     571   [ +  +  +  -  :          36 :         return !started.load() || !channel || !channel->is_backpressure_active();
             +  -  +  - ]
     572                 :          18 :       };
     573   [ +  -  -  +  :           9 :       if (!predicate() && detail::in_data_callback()) return false;
             -  -  -  + ]
     574   [ +  -  -  + ]:           9 :       while (!bp_cv_.wait_for(bp_lock, std::chrono::milliseconds(50), predicate)) {
     575                 :             :       }
     576                 :           9 :       bp_lock.unlock();
     577   [ +  -  +  + ]:           9 :       if (try_send_to(client_id, data)) return true;
     578                 :           9 :     }
     579                 :           1 :     return false;
     580                 :             :   }
     581                 :             : 
     582                 :          12 :   bool try_send_to(ClientId client_id, std::string_view data) {
     583                 :          12 :     std::shared_lock<std::shared_mutex> lock(mutex);
     584                 :          12 :     auto it = sessions.find(client_id);
     585   [ +  +  -  +  :          12 :     if (it == sessions.end() || !channel) return false;
                   +  + ]
     586                 :             : 
     587                 :           7 :     auto bytes = base::safe_convert::string_to_bytes(data);
     588                 :           7 :     return channel->async_try_write_to(memory::ConstByteSpan(bytes.first, bytes.second), it->second.endpoint);
     589                 :          12 :   }
     590                 :             : 
     591                 :           8 :   RuntimeStats stats() const {
     592                 :           8 :     std::shared_lock<std::shared_mutex> lock(mutex);
     593   [ +  -  +  - ]:          16 :     return channel ? channel->stats() : RuntimeStats{};
     594                 :           8 :   }
     595                 :             : 
     596                 :           0 :   void reset_stats() {
     597                 :           0 :     std::shared_lock<std::shared_mutex> lock(mutex);
     598   [ #  #  #  # ]:           0 :     if (channel) channel->reset_stats();
     599                 :           0 :   }
     600                 :             : };
     601                 :             : 
     602                 :           4 : UdpServer::UdpServer(uint16_t port) {
     603                 :           4 :   config::UdpConfig cfg;
     604                 :           4 :   cfg.local_port = port;
     605                 :           4 :   impl_ = std::make_shared<Impl>(cfg);
     606                 :           4 : }
     607                 :             : 
     608                 :          21 : UdpServer::UdpServer(const config::UdpConfig& cfg) : impl_(std::make_shared<Impl>(cfg)) {}
     609                 :             : 
     610                 :           3 : UdpServer::UdpServer(const config::UdpConfig& cfg, std::shared_ptr<boost::asio::io_context> ioc)
     611                 :           3 :     : impl_(std::make_shared<Impl>(cfg, ioc)) {}
     612                 :             : 
     613                 :           1 : UdpServer::UdpServer(std::shared_ptr<interface::Channel> ch) : impl_(std::make_shared<Impl>(std::move(ch))) {
     614                 :           1 :   impl_->setup_internal_handlers();
     615                 :           1 : }
     616                 :             : 
     617                 :          33 : UdpServer::~UdpServer() = default;
     618                 :             : 
     619                 :           0 : UdpServer::UdpServer(UdpServer&&) noexcept = default;
     620                 :           0 : UdpServer& UdpServer::operator=(UdpServer&&) noexcept = default;
     621                 :             : 
     622                 :          21 : std::future<bool> UdpServer::start() { return impl_->start(); }
     623                 :          20 : void UdpServer::stop() { impl_->stop(); }
     624                 :           8 : bool UdpServer::listening() const { return impl_->is_listening.load(); }
     625                 :           8 : RuntimeStats UdpServer::stats() const { return impl_->stats(); }
     626                 :           0 : void UdpServer::reset_stats() { impl_->reset_stats(); }
     627                 :             : 
     628                 :          36 : bool UdpServer::broadcast(std::string_view data) { return impl_->broadcast(data); }
     629                 :           3 : bool UdpServer::try_broadcast(std::string_view data) { return impl_->try_broadcast(data); }
     630                 :           5 : bool UdpServer::send_to(ClientId client_id, std::string_view data) { return impl_->send_to(client_id, data); }
     631                 :           2 : bool UdpServer::try_send_to(ClientId client_id, std::string_view data) { return impl_->try_send_to(client_id, data); }
     632                 :             : 
     633                 :           1 : bool UdpServer::send_to_blocking(ClientId client_id, std::string_view data) {
     634                 :           1 :   return impl_->send_to_blocking(client_id, data);
     635                 :             : }
     636                 :             : 
     637                 :           3 : bool UdpServer::broadcast_line(std::string_view line) { return broadcast(std::string(line) + "\n"); }
     638                 :           1 : bool UdpServer::send_to_line(ClientId client_id, std::string_view line) {
     639                 :           3 :   return send_to(client_id, std::string(line) + "\n");
     640                 :             : }
     641                 :           3 : bool UdpServer::try_broadcast_line(std::string_view line) { return try_broadcast(std::string(line) + "\n"); }
     642                 :           1 : bool UdpServer::try_send_to_line(ClientId client_id, std::string_view line) {
     643                 :           3 :   return try_send_to(client_id, std::string(line) + "\n");
     644                 :             : }
     645                 :             : 
     646                 :           6 : UdpServer& UdpServer::on_connect(ConnectionHandler h) {
     647                 :           6 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     648                 :           6 :   impl_->on_connect = std::move(h);
     649                 :           6 :   return *this;
     650                 :           6 : }
     651                 :             : 
     652                 :           5 : UdpServer& UdpServer::on_disconnect(ConnectionHandler h) {
     653                 :           5 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     654                 :           5 :   impl_->on_disconnect = std::move(h);
     655                 :           5 :   return *this;
     656                 :           5 : }
     657                 :             : 
     658                 :           8 : UdpServer& UdpServer::on_data(MessageHandler h) {
     659                 :           8 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     660                 :           8 :   impl_->on_data = interface::share_callback(std::move(h));
     661                 :           8 :   return *this;
     662                 :           8 : }
     663                 :             : 
     664                 :           4 : UdpServer& UdpServer::on_data_batch(BatchMessageHandler h) {
     665                 :           4 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     666                 :           4 :   impl_->on_data_batch_ = interface::share_callback(std::move(h));
     667                 :           4 :   return *this;
     668                 :           4 : }
     669                 :             : 
     670                 :           3 : UdpServer& UdpServer::on_error(ErrorHandler h) {
     671                 :           3 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     672                 :           3 :   impl_->on_error = std::move(h);
     673                 :           3 :   return *this;
     674                 :           3 : }
     675                 :             : 
     676                 :           6 : UdpServer& UdpServer::framer(FramerFactory factory) {
     677                 :           6 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     678                 :           6 :   impl_->framer_factory = std::move(factory);
     679                 :           6 :   return *this;
     680                 :           6 : }
     681                 :             : 
     682                 :           3 : UdpServer& UdpServer::on_message(MessageHandler h) {
     683                 :           3 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     684                 :           3 :   impl_->on_message = interface::share_callback(std::move(h));
     685                 :           3 :   return *this;
     686                 :           3 : }
     687                 :             : 
     688                 :           3 : UdpServer& UdpServer::on_message_batch(BatchMessageHandler h) {
     689                 :           3 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     690                 :           3 :   impl_->on_message_batch_ = interface::share_callback(std::move(h));
     691                 :           3 :   return *this;
     692                 :           3 : }
     693                 :             : 
     694                 :          17 : size_t UdpServer::client_count() const {
     695                 :          17 :   std::shared_lock<std::shared_mutex> lock(impl_->mutex);
     696                 :          34 :   return impl_->endpoint_to_id.size();
     697                 :          17 : }
     698                 :             : 
     699                 :           3 : std::vector<ClientId> UdpServer::connected_clients() const {
     700                 :           3 :   std::shared_lock<std::shared_mutex> lock(impl_->mutex);
     701                 :           3 :   std::vector<ClientId> ids;
     702                 :           3 :   ids.reserve(impl_->sessions.size());
     703         [ +  + ]:           6 :   for (const auto& [id, entry] : impl_->sessions) {
     704                 :           3 :     ids.push_back(id);
     705                 :             :   }
     706                 :           6 :   return ids;
     707                 :           3 : }
     708                 :             : 
     709                 :           1 : UdpServer& UdpServer::auto_start(bool m) {
     710                 :           1 :   impl_->auto_start.store(m);
     711   [ +  -  +  -  :           1 :   if (impl_->auto_start.load() && !impl_->started.load()) {
                   +  - ]
     712                 :           1 :     start();
     713                 :             :   }
     714                 :           1 :   return *this;
     715                 :             : }
     716                 :             : 
     717                 :           1 : UdpServer& UdpServer::bind_address(const std::string& address) {
     718                 :           1 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     719                 :           1 :   impl_->cfg.bind_address = address;
     720                 :           1 :   return *this;
     721                 :           1 : }
     722                 :             : 
     723                 :           4 : UdpServer& UdpServer::idle_timeout(std::chrono::milliseconds timeout) {
     724                 :           4 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     725                 :           4 :   impl_->session_timeout = timeout;
     726         [ -  + ]:           4 :   if (impl_->session_timeout.count() <= 0) {
     727         [ #  # ]:           0 :     if (impl_->reaper_timer) {
     728                 :           0 :       impl_->reaper_timer->cancel();
     729                 :           0 :       impl_->reaper_timer.reset();
     730                 :             :     }
     731                 :           0 :     return *this;
     732                 :             :   }
     733   [ -  +  -  -  :           4 :   if (impl_->started.load() && impl_->channel && !impl_->reaper_timer) {
             -  -  -  + ]
     734                 :           0 :     impl_->reaper_timer = std::make_unique<boost::asio::steady_timer>(impl_->channel->get_executor());
     735                 :           0 :     impl_->schedule_reaper();
     736                 :             :   }
     737                 :           4 :   return *this;
     738                 :           4 : }
     739                 :             : 
     740                 :           2 : UdpServer& UdpServer::on_backpressure(std::function<void(size_t)> handler) {
     741                 :           2 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     742                 :           2 :   impl_->bp_handler = std::move(handler);
     743                 :           2 :   return *this;
     744                 :           2 : }
     745                 :             : 
     746                 :           4 : UdpServer& UdpServer::max_clients(size_t max) {
     747         [ +  + ]:           4 :   if (max == 0) {
     748                 :           1 :     impl_->client_limit_enabled.store(false);
     749                 :           1 :     impl_->max_clients_limit.store(0);
     750                 :             :   } else {
     751                 :           3 :     impl_->client_limit_enabled.store(true);
     752                 :           3 :     impl_->max_clients_limit.store(max);
     753                 :             :   }
     754                 :           4 :   return *this;
     755                 :             : }
     756                 :             : 
     757                 :           7 : UdpServer& UdpServer::backpressure_threshold(size_t threshold) {
     758                 :           7 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     759                 :           7 :   impl_->cfg.backpressure_threshold = threshold;
     760                 :           7 :   return *this;
     761                 :           7 : }
     762                 :             : 
     763                 :           5 : UdpServer& UdpServer::backpressure_strategy(base::constants::BackpressureStrategy strategy) {
     764                 :           5 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     765                 :           5 :   impl_->cfg.backpressure_strategy = strategy;
     766         [ -  + ]:           5 :   if (impl_->channel) {
     767                 :           0 :     impl_->channel->set_backpressure_strategy(strategy);
     768                 :             :   }
     769                 :           5 :   return *this;
     770                 :           5 : }
     771                 :             : 
     772                 :           3 : size_t UdpServer::backpressure_threshold() const {
     773                 :           3 :   std::shared_lock<std::shared_mutex> lock(impl_->mutex);
     774                 :           6 :   return impl_->cfg.backpressure_threshold;
     775                 :           3 : }
     776                 :             : 
     777                 :           3 : base::constants::BackpressureStrategy UdpServer::backpressure_strategy() const {
     778                 :           3 :   std::shared_lock<std::shared_mutex> lock(impl_->mutex);
     779                 :           6 :   return impl_->cfg.backpressure_strategy;
     780                 :           3 : }
     781                 :             : 
     782                 :           0 : UdpServer& UdpServer::send_buffer_size(size_t bytes) {
     783                 :           0 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     784                 :           0 :   impl_->cfg.send_buffer_size = bytes;
     785                 :           0 :   return *this;
     786                 :           0 : }
     787                 :             : 
     788                 :           0 : UdpServer& UdpServer::receive_buffer_size(size_t bytes) {
     789                 :           0 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     790                 :           0 :   impl_->cfg.receive_buffer_size = bytes;
     791                 :           0 :   return *this;
     792                 :           0 : }
     793                 :             : 
     794                 :           4 : UdpServer& UdpServer::manage_external_context(bool m) {
     795                 :           4 :   impl_->manage_external_context.store(m);
     796                 :           4 :   return *this;
     797                 :             : }
     798                 :             : 
     799                 :           5 : UdpServer& UdpServer::batch_size(size_t size) {
     800                 :           5 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     801                 :           5 :   impl_->max_batch_size_ = size;
     802                 :           5 :   return *this;
     803                 :           5 : }
     804                 :             : 
     805                 :           5 : UdpServer& UdpServer::batch_latency(std::chrono::milliseconds latency) {
     806                 :           5 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex);
     807                 :           5 :   impl_->max_batch_latency_ = latency;
     808                 :           5 :   return *this;
     809                 :           5 : }
     810                 :             : 
     811                 :             : }  // namespace wrapper
     812                 :             : }  // namespace wirestead
        

Generated by: LCOV version 2.0-1