LCOV - code coverage report
Current view: top level - wirestead/wrapper/udp - udp.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 92.5 % 439 406
Test Date: 2026-08-30 10:35:09 Functions: 90.7 % 75 68
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 64.3 % 283 182

             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.hpp"
      18                 :             : 
      19                 :             : #include "wirestead/concurrency/io_thread_hook.hpp"
      20                 :             : 
      21                 :             : #if defined(__GNUC__) || defined(__clang__)
      22                 :             : #pragma GCC diagnostic ignored "-Wsign-conversion"
      23                 :             : #pragma GCC diagnostic ignored "-Wconversion"
      24                 :             : #endif
      25                 :             : 
      26                 :             : #include <atomic>
      27                 :             : #include <boost/asio/executor_work_guard.hpp>
      28                 :             : #include <boost/asio/io_context.hpp>
      29                 :             : #include <boost/asio/steady_timer.hpp>
      30                 :             : #include <chrono>
      31                 :             : #include <iostream>
      32                 :             : #include <mutex>
      33                 :             : #include <shared_mutex>
      34                 :             : #include <stdexcept>
      35                 :             : #include <thread>
      36                 :             : #include <vector>
      37                 :             : 
      38                 :             : #include "wirestead/base/common.hpp"
      39                 :             : #include "wirestead/factory/channel_factory.hpp"
      40                 :             : #include "wirestead/transport/udp/udp.hpp"
      41                 :             : #include "wirestead/wrapper/callback_guard.hpp"
      42                 :             : #include "wirestead/wrapper/error_context_builder.hpp"
      43                 :             : 
      44                 :             : namespace wirestead {
      45                 :             : namespace wrapper {
      46                 :             : 
      47                 :             : struct UdpClient::Impl : public std::enable_shared_from_this<Impl> {
      48                 :             :   mutable std::shared_mutex mutex_;
      49                 :             :   std::mutex bp_mutex_;
      50                 :             :   std::condition_variable bp_cv_;
      51                 :             :   config::UdpConfig cfg;
      52                 :             :   std::shared_ptr<interface::Channel> channel;
      53                 :             :   std::shared_ptr<boost::asio::io_context> external_ioc;
      54                 :             :   std::atomic<bool> use_external_context{false};
      55                 :             :   std::atomic<bool> manage_external_context{false};
      56                 :             :   std::thread external_thread;
      57                 :             :   std::unique_ptr<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> work_guard;
      58                 :             : 
      59                 :             :   // Event handlers (Context based)
      60                 :             :   // Shared snapshots: the strand copies one out per received datagram, and a
      61                 :             :   // std::function copy allocates whenever the user handler outgrows its
      62                 :             :   // small-object buffer. See interface::SharedCallback.
      63                 :             :   interface::SharedCallback<MessageHandler> data_handler;
      64                 :             :   interface::SharedCallback<BatchMessageHandler> data_batch_handler_;
      65                 :             :   ConnectionHandler connect_handler{nullptr};
      66                 :             :   ConnectionHandler disconnect_handler{nullptr};
      67                 :             :   ErrorHandler error_handler{nullptr};
      68                 :             :   std::function<void(size_t)> bp_handler{nullptr};
      69                 :             :   interface::SharedCallback<MessageHandler> message_handler;
      70                 :             :   interface::SharedCallback<BatchMessageHandler> message_batch_handler_;
      71                 :             : 
      72                 :             :   std::shared_ptr<framer::IFramer> framer{nullptr};
      73                 :             : 
      74                 :             :   // Batching logic
      75                 :             :   std::vector<MessageContext> data_batch_queue_;
      76                 :             :   std::vector<MessageContext> message_batch_queue_;
      77                 :             :   std::unique_ptr<boost::asio::steady_timer> batch_timer_;
      78                 :             :   size_t max_batch_size_ = 100;
      79                 :          49 :   std::chrono::milliseconds max_batch_latency_{1};
      80                 :             : 
      81                 :             :   std::atomic<bool> auto_start_{false};
      82                 :             :   std::vector<std::promise<bool>> pending_promises;
      83                 :             :   std::atomic<bool> started_{false};
      84                 :          49 :   std::shared_ptr<bool> alive_marker{std::make_shared<bool>(true)};
      85                 :             : 
      86                 :             :   // False only for the dependency-injected-channel constructor below, where
      87                 :             :   // the caller owns the channel's identity and lifecycle (e.g. tests
      88                 :             :   // injecting a fake channel) - stop() must not discard and factory-rebuild
      89                 :             :   // a channel it didn't create itself.
      90                 :             :   bool factory_managed_channel_ = true;
      91                 :             : 
      92                 :          29 :   explicit Impl(const config::UdpConfig& config) : cfg(config) {}
      93                 :           5 :   Impl(const config::UdpConfig& config, std::shared_ptr<boost::asio::io_context> ioc)
      94   [ +  -  +  - ]:           5 :       : cfg(config), external_ioc(std::move(ioc)), use_external_context(external_ioc != nullptr) {}
      95                 :          15 :   explicit Impl(std::shared_ptr<interface::Channel> ch) : channel(std::move(ch)), factory_managed_channel_(false) {
      96                 :             :     // #450: setup_internal_handlers() captures weak_from_this() - calling it
      97                 :             :     // from inside this constructor would capture an empty weak_ptr, since
      98                 :             :     // enable_shared_from_this isn't wired up until make_shared() finishes
      99                 :             :     // constructing the object. Deferred to UdpClient's own constructor,
     100                 :             :     // which runs after impl_ is a fully-formed shared_ptr<Impl>.
     101                 :          15 :   }
     102                 :             : 
     103                 :          49 :   ~Impl() {
     104                 :             :     try {
     105                 :          49 :       stop();
     106                 :           0 :     } catch (...) {
     107                 :           0 :     }
     108                 :          49 :   }
     109                 :             : 
     110                 :         113 :   void fulfill_all_locked(bool value) {
     111         [ +  + ]:         140 :     for (auto& promise : pending_promises) {
     112                 :             :       try {
     113                 :          27 :         promise.set_value(value);
     114                 :           0 :       } catch (...) {
     115                 :           0 :       }
     116                 :             :     }
     117                 :         113 :     pending_promises.clear();
     118                 :         113 :   }
     119                 :             : 
     120                 :           1 :   void flush_batches() {
     121                 :           1 :     std::unique_lock<std::shared_mutex> lock(mutex_);
     122         [ +  - ]:           1 :     if (!data_batch_queue_.empty()) {
     123                 :           1 :       auto handler = data_batch_handler_;
     124                 :           1 :       auto batch = std::move(data_batch_queue_);
     125                 :           1 :       data_batch_queue_.clear();
     126         [ +  - ]:           1 :       if (handler) {
     127                 :           1 :         lock.unlock();
     128                 :           1 :         detail::invoke_user_callback("udp_client", "on_data_batch", handler, batch);
     129                 :           1 :         lock.lock();
     130                 :             :       }
     131                 :           1 :     }
     132         [ +  - ]:           1 :     if (!message_batch_queue_.empty()) {
     133                 :           1 :       auto handler = message_batch_handler_;
     134                 :           1 :       auto batch = std::move(message_batch_queue_);
     135                 :           1 :       message_batch_queue_.clear();
     136         [ +  - ]:           1 :       if (handler) {
     137                 :           1 :         lock.unlock();
     138                 :           1 :         detail::invoke_user_callback("udp_client", "on_message_batch", handler, batch);
     139                 :           1 :         lock.lock();
     140                 :             :       }
     141                 :           1 :     }
     142         [ +  - ]:           1 :     if (batch_timer_) {
     143                 :           1 :       batch_timer_->cancel();
     144                 :             :     }
     145                 :           1 :   }
     146                 :             : 
     147                 :           6 :   void schedule_batch_timer() {
     148         [ -  + ]:           6 :     if (!batch_timer_) return;
     149                 :           6 :     batch_timer_->expires_after(max_batch_latency_);
     150                 :          12 :     batch_timer_->async_wait([this, weak_impl = weak_from_this(),
     151                 :           6 :                               weak_alive = std::weak_ptr<bool>(alive_marker)](const boost::system::error_code& ec) {
     152         [ +  + ]:           2 :       if (ec) return;
     153                 :           1 :       auto impl_keepalive = weak_impl.lock();
     154         [ -  + ]:           1 :       if (!impl_keepalive) return;
     155                 :           1 :       auto alive = weak_alive.lock();
     156         [ -  + ]:           1 :       if (!alive) return;
     157                 :           1 :       flush_batches();
     158                 :           1 :     });
     159                 :             :   }
     160                 :             : 
     161                 :          32 :   std::future<bool> start() {
     162                 :          32 :     std::unique_lock<std::shared_mutex> lock(mutex_);
     163   [ +  +  +  -  :          32 :     if (channel && channel->is_connected()) {
             +  +  +  + ]
     164                 :           5 :       started_.store(true);
     165                 :           5 :       std::promise<bool> p;
     166                 :           5 :       p.set_value(true);
     167                 :           5 :       return p.get_future();
     168                 :           5 :     }
     169                 :             : 
     170                 :          27 :     std::promise<bool> p;
     171                 :          27 :     auto future = p.get_future();
     172                 :          27 :     pending_promises.emplace_back(std::move(p));
     173                 :             : 
     174         [ +  + ]:          27 :     if (started_.load()) {
     175                 :           1 :       return future;
     176                 :             :     }
     177                 :             : 
     178         [ +  + ]:          26 :     if (!channel) {
     179                 :          20 :       channel = factory::ChannelFactory::create(cfg, external_ioc);
     180                 :          20 :       setup_internal_handlers();
     181                 :             :     }
     182                 :          26 :     started_.store(true);
     183                 :             : 
     184                 :          26 :     lock.unlock();
     185                 :          26 :     channel->start();
     186   [ +  +  +  +  :          26 :     if (use_external_context && manage_external_context && !external_thread.joinable()) {
             +  -  +  + ]
     187   [ +  -  -  +  :           1 :       if (external_ioc->stopped()) external_ioc->restart();
                   -  - ]
     188                 :           2 :       work_guard = std::make_unique<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>>(
     189                 :           3 :           boost::asio::make_work_guard(*external_ioc));
     190                 :           2 :       external_thread = std::thread([this, ioc = external_ioc]() {
     191                 :           1 :         wirestead::concurrency::run_io_thread_init();
     192                 :             :         try {
     193                 :           1 :           ioc->run();
     194                 :           0 :         } catch (...) {
     195                 :           0 :         }
     196                 :           2 :       });
     197                 :             :     }
     198                 :          26 :     return future;
     199                 :          32 :   }
     200                 :             : 
     201                 :          74 :   void stop() {
     202                 :          74 :     std::unique_lock<std::shared_mutex> lock(mutex_);
     203         [ +  + ]:          74 :     if (!started_.load()) {
     204                 :          44 :       fulfill_all_locked(false);
     205                 :          44 :       return;
     206                 :             :     }
     207                 :          30 :     started_.store(false);
     208                 :          30 :     bp_cv_.notify_all();
     209                 :             : 
     210         [ +  - ]:          30 :     if (batch_timer_) {
     211                 :          30 :       batch_timer_->cancel();
     212                 :          30 :       batch_timer_.reset();
     213                 :             :     }
     214                 :             : 
     215         [ +  - ]:          30 :     if (channel) {
     216                 :          30 :       lock.unlock();
     217                 :          30 :       channel->stop();
     218                 :             :       // Clear callbacks after stop() rather than before: the transport-level
     219                 :             :       // fix (#436) already synchronizes callback reads against these
     220                 :             :       // setters, but clearing after stop() means no in-flight handler can
     221                 :             :       // observe a null callback mid-shutdown in the first place - belt and
     222                 :             :       // braces once the underlying race is fixed at the source.
     223                 :          30 :       channel->on_bytes(nullptr);
     224                 :          30 :       channel->on_state(nullptr);
     225                 :          30 :       channel->on_backpressure(nullptr);
     226                 :          30 :       lock.lock();
     227         [ +  + ]:          30 :       if (factory_managed_channel_) {
     228                 :             :         // Fully release the channel rather than reusing it: start()'s
     229                 :             :         // `if (!channel)` guard is what re-runs setup_internal_handlers() and
     230                 :             :         // rebuilds config from the (possibly changed) staged fields. Reusing
     231                 :             :         // a stopped channel left every handler nulled forever - including
     232                 :             :         // the one that fulfills the start() future - so a restart would
     233                 :             :         // hang (jwsung91/wirestead#444). Injected channels (factory_managed_
     234                 :             :         // channel_ == false) are exempt: the caller owns that channel's
     235                 :             :         // identity, so we must not discard and factory-rebuild it.
     236                 :          20 :         channel.reset();
     237                 :             :       }
     238                 :             :     }
     239                 :             : 
     240         [ +  + ]:          30 :     if (work_guard) {
     241                 :           1 :       work_guard.reset();
     242                 :             :     }
     243                 :             : 
     244   [ +  +  +  +  :          30 :     if (use_external_context && manage_external_context && external_thread.joinable()) {
             +  -  +  + ]
     245         [ +  - ]:           1 :       if (external_ioc) {
     246                 :           1 :         external_ioc->stop();
     247                 :             :       }
     248         [ +  - ]:           1 :       if (std::this_thread::get_id() != external_thread.get_id()) {
     249                 :           1 :         lock.unlock();
     250                 :           1 :         external_thread.join();
     251                 :           1 :         lock.lock();
     252                 :             :       } else {
     253                 :           0 :         external_thread.detach();
     254                 :             :       }
     255                 :             :     }
     256                 :             : 
     257                 :          30 :     fulfill_all_locked(false);
     258                 :             : 
     259         [ +  + ]:          30 :     if (framer) {
     260                 :           2 :       framer->reset();
     261                 :             :     }
     262                 :          74 :   }
     263                 :             : 
     264                 :          13 :   bool send(std::string_view data) {
     265         [ +  + ]:          13 :     if (cfg.backpressure_strategy == base::constants::BackpressureStrategy::Reliable) return send_blocking(data);
     266                 :           1 :     return try_send(data);
     267                 :             :   }
     268                 :             : 
     269                 :             :   // #509: wait_for_backpressure_clear()'s condition and the transport's own
     270                 :             :   // hard queue-byte cap are different thresholds observed at different
     271                 :             :   // times, so a single write attempt can spuriously fail right after the
     272                 :             :   // wait exits. Bounded retry rather than unbounded, so a payload that can
     273                 :             :   // never fit still fails in bounded time. If a single write is rejected
     274                 :             :   // because the payload alone exceeds the transport's cap, UdpChannel also
     275                 :             :   // transitions to LinkState::Error (unlike TCP/UDS) - wait_for_backpressure_
     276                 :             :   // clear()'s own is_connected() check catches that and ends the retry loop
     277                 :             :   // after one real attempt, so this doesn't retry into a broken channel.
     278                 :             :   static constexpr int kMaxBlockingSendAttempts = 5;
     279                 :             : 
     280                 :           2 :   bool send_move(std::vector<uint8_t>&& data) {
     281         [ +  + ]:           2 :     if (cfg.backpressure_strategy == base::constants::BackpressureStrategy::Reliable) {
     282         [ +  - ]:           1 :       for (int attempt = 0; attempt < kMaxBlockingSendAttempts; ++attempt) {
     283                 :           1 :         std::unique_lock<std::mutex> bp_lock(bp_mutex_);
     284   [ +  -  -  + ]:           1 :         if (!wait_for_backpressure_clear(bp_lock)) return false;
     285                 :           1 :         bp_lock.unlock();
     286                 :           1 :         std::shared_lock<std::shared_mutex> lock(mutex_);
     287   [ +  -  +  -  :           1 :         if (!started_.load() || !channel || !channel->is_connected()) return false;
          +  -  -  +  -  
                      + ]
     288   [ +  -  +  - ]:           1 :         if (channel->async_write_move(std::move(data))) return true;
     289                 :           2 :       }
     290                 :           0 :       return false;
     291                 :             :     }
     292                 :           1 :     return try_send_move(std::move(data));
     293                 :             :   }
     294                 :             : 
     295                 :           2 :   bool send_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     296   [ +  -  -  +  :           2 :     if (!data || data->empty()) return false;
                   -  + ]
     297         [ +  + ]:           2 :     if (cfg.backpressure_strategy == base::constants::BackpressureStrategy::Reliable) {
     298         [ +  - ]:           1 :       for (int attempt = 0; attempt < kMaxBlockingSendAttempts; ++attempt) {
     299                 :           1 :         std::unique_lock<std::mutex> bp_lock(bp_mutex_);
     300   [ +  -  -  + ]:           1 :         if (!wait_for_backpressure_clear(bp_lock)) return false;
     301                 :           1 :         bp_lock.unlock();
     302                 :           1 :         std::shared_lock<std::shared_mutex> lock(mutex_);
     303   [ +  -  +  -  :           1 :         if (!started_.load() || !channel || !channel->is_connected()) return false;
          +  -  -  +  -  
                      + ]
     304   [ +  -  +  - ]:           1 :         if (channel->async_write_shared(data)) return true;
     305                 :           2 :       }
     306                 :           0 :       return false;
     307                 :             :     }
     308                 :           1 :     return try_send_shared(std::move(data));
     309                 :             :   }
     310                 :             : 
     311                 :           5 :   bool send_line(std::string_view line) {
     312         [ +  + ]:           5 :     if (cfg.backpressure_strategy == base::constants::BackpressureStrategy::Reliable) return send_line_blocking(line);
     313                 :           1 :     return try_send_line(line);
     314                 :             :   }
     315                 :             : 
     316                 :           6 :   bool try_send_line(std::string_view line) { return try_send(std::string(line) + "\n"); }
     317                 :             : 
     318                 :          20 :   bool send_blocking(std::string_view data) {
     319                 :          20 :     auto binary_view = base::safe_convert::string_to_bytes(data);
     320                 :          20 :     memory::ConstByteSpan span(binary_view.first, binary_view.second);
     321         [ +  - ]:          21 :     for (int attempt = 0; attempt < kMaxBlockingSendAttempts; ++attempt) {
     322                 :          21 :       std::unique_lock<std::mutex> bp_lock(bp_mutex_);
     323   [ +  -  -  + ]:          21 :       if (!wait_for_backpressure_clear(bp_lock)) return false;
     324                 :          21 :       bp_lock.unlock();
     325                 :          21 :       std::shared_lock<std::shared_mutex> lock(mutex_);
     326   [ +  +  +  -  :          21 :       if (!started_.load() || !channel || !channel->is_connected()) return false;
          +  -  -  +  +  
                      + ]
     327   [ +  -  +  + ]:          15 :       if (channel->async_write_copy(span)) return true;
     328                 :          41 :     }
     329                 :           0 :     return false;
     330                 :             :   }
     331                 :             : 
     332                 :             :   // channel->on_backpressure() calls bp_cv_.notify_all() from the transport's io_context
     333                 :             :   // thread without holding bp_mutex_ (backpressure_active_ is a plain atomic on the transport
     334                 :             :   // side, not guarded by bp_mutex_ at all). That makes a classic lost-wakeup race possible: a
     335                 :             :   // waiter can check the predicate, find it still blocking, and be in the process of
     336                 :             :   // registering to wait when the notify fires - in the rare case that race is lost, an
     337                 :             :   // unbounded wait() would block forever. Poll with a bounded timeout instead so a missed
     338                 :             :   // notify only costs a short delay rather than a permanent hang (see #427).
     339                 :             :   //
     340                 :             :   // Returns false instead of waiting if called from the channel's own io
     341                 :             :   // thread while backpressure is active - e.g. a blocking send() called
     342                 :             :   // from inside an on_data/on_message callback. Clearing backpressure
     343                 :             :   // requires that same io thread to make progress, so blocking here would
     344                 :             :   // deadlock forever rather than eventually clear (#449).
     345                 :          23 :   bool wait_for_backpressure_clear(std::unique_lock<std::mutex>& bp_lock) {
     346                 :          27 :     auto predicate = [this] {
     347                 :          27 :       std::shared_lock<std::shared_mutex> lock(mutex_);
     348   [ +  +  +  -  :          54 :       return !started_.load() || !channel || !channel->is_connected() || !channel->is_backpressure_active();
          +  -  +  -  +  
                -  +  + ]
     349                 :          27 :     };
     350   [ +  -  +  + ]:          23 :     if (predicate()) return true;
     351         [ -  + ]:           1 :     if (detail::in_data_callback()) return false;
     352   [ +  -  +  + ]:           2 :     while (!bp_cv_.wait_for(bp_lock, std::chrono::milliseconds(50), predicate)) {
     353                 :             :     }
     354                 :           1 :     return true;
     355                 :             :   }
     356                 :             : 
     357                 :           5 :   bool try_send(std::string_view data) {
     358                 :           5 :     std::shared_lock<std::shared_mutex> lock(mutex_);
     359   [ +  -  +  -  :           5 :     if (channel && channel->is_connected()) {
             +  -  +  - ]
     360                 :           5 :       auto binary_view = base::safe_convert::string_to_bytes(data);
     361                 :           5 :       return channel->async_try_write_copy(memory::ConstByteSpan(binary_view.first, binary_view.second));
     362                 :             :     }
     363                 :           0 :     return false;
     364                 :           5 :   }
     365                 :             : 
     366                 :           3 :   bool try_send_move(std::vector<uint8_t>&& data) {
     367                 :           3 :     std::shared_lock<std::shared_mutex> lock(mutex_);
     368   [ +  -  +  -  :           3 :     if (channel && channel->is_connected()) {
             +  -  +  - ]
     369                 :           3 :       return channel->async_try_write_move(std::move(data));
     370                 :             :     }
     371                 :           0 :     return false;
     372                 :           3 :   }
     373                 :             : 
     374                 :           5 :   bool try_send_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     375   [ +  +  +  +  :           5 :     if (!data || data->empty()) return false;
                   +  + ]
     376                 :           3 :     std::shared_lock<std::shared_mutex> lock(mutex_);
     377   [ +  -  +  -  :           3 :     if (channel && channel->is_connected()) {
             +  -  +  - ]
     378                 :           3 :       return channel->async_try_write_shared(std::move(data));
     379                 :             :     }
     380                 :           0 :     return false;
     381                 :           3 :   }
     382                 :             : 
     383                 :          12 :   bool send_line_blocking(std::string_view line) { return send_blocking(std::string(line) + "\n"); }
     384                 :             : 
     385                 :           8 :   RuntimeStats stats() const {
     386                 :           8 :     std::shared_lock<std::shared_mutex> lock(mutex_);
     387   [ +  -  +  - ]:          16 :     return channel ? channel->stats() : RuntimeStats{};
     388                 :           8 :   }
     389                 :             : 
     390                 :           0 :   void reset_stats() {
     391                 :           0 :     std::shared_lock<std::shared_mutex> lock(mutex_);
     392   [ #  #  #  # ]:           0 :     if (channel) channel->reset_stats();
     393                 :           0 :   }
     394                 :             : 
     395                 :          35 :   void setup_internal_handlers() {
     396         [ -  + ]:          35 :     if (!channel) return;
     397                 :             : 
     398                 :          35 :     batch_timer_ = std::make_unique<boost::asio::steady_timer>(channel->get_executor());
     399                 :             : 
     400                 :          35 :     std::weak_ptr<bool> weak_alive = alive_marker;
     401                 :          35 :     std::weak_ptr<Impl> weak_impl = weak_from_this();
     402                 :             : 
     403                 :          35 :     channel->on_bytes([this, weak_impl, weak_alive](memory::ConstByteSpan data) {
     404                 :          13 :       auto impl_keepalive = weak_impl.lock();
     405         [ -  + ]:          13 :       if (!impl_keepalive) return;
     406                 :          13 :       auto alive = weak_alive.lock();
     407         [ -  + ]:          13 :       if (!alive) return;
     408                 :             : 
     409                 :             :       // #449: everything below runs synchronously on this io thread - mark
     410                 :             :       // it so a blocking send() called from within one of these callbacks
     411                 :             :       // fails fast instead of deadlocking.
     412                 :          13 :       detail::CallbackGuard callback_guard;
     413                 :             : 
     414                 :             :       // #441: snapshot the handler/framer pointers under a shared_lock (not
     415                 :             :       // unique_lock) - this is a pure read, matching try_send's locking
     416                 :             :       // level so it no longer blocks concurrent sends even briefly.
     417                 :             :       bool batch_mode;
     418                 :          13 :       interface::SharedCallback<MessageHandler> handler;
     419                 :          13 :       std::shared_ptr<framer::IFramer> framer_to_push;
     420                 :             :       {
     421                 :          13 :         std::shared_lock<std::shared_mutex> lock(mutex_);
     422                 :          13 :         batch_mode = static_cast<bool>(data_batch_handler_);
     423                 :          13 :         handler = data_handler;
     424                 :          13 :         framer_to_push = framer;
     425                 :          13 :       }
     426                 :             : 
     427         [ +  + ]:          13 :       if (batch_mode) {
     428                 :             :         // #441: build the copy before taking the exclusive lock, so the
     429                 :             :         // lock is only held for the queue mutation itself, not the
     430                 :             :         // allocation.
     431                 :           4 :         MessageContext ctx(0, memory::SafeDataBuffer(data));
     432                 :           4 :         interface::SharedCallback<BatchMessageHandler> flush_handler;
     433                 :           4 :         std::vector<MessageContext> batch;
     434                 :             :         {
     435                 :           4 :           std::unique_lock<std::shared_mutex> lock(mutex_);
     436                 :           4 :           data_batch_queue_.emplace_back(std::move(ctx));
     437         [ +  + ]:           4 :           if (data_batch_queue_.size() >= max_batch_size_) {
     438                 :           1 :             flush_handler = data_batch_handler_;
     439                 :           1 :             batch = std::move(data_batch_queue_);
     440                 :           1 :             data_batch_queue_.clear();
     441         [ +  - ]:           3 :           } else if (data_batch_queue_.size() == 1) {
     442                 :           3 :             schedule_batch_timer();
     443                 :             :           }
     444                 :           4 :         }
     445                 :           4 :         detail::invoke_user_callback("udp_client", "on_data_batch", flush_handler, batch);
     446                 :           4 :       } else {
     447                 :          18 :         detail::invoke_user_callback("udp_client", "on_data", handler, MessageContext(0, data));
     448                 :             :       }
     449                 :             : 
     450   [ +  +  +  - ]:          13 :       if (framer_to_push) framer_to_push->push_bytes(data);
     451                 :          13 :     });
     452                 :             : 
     453                 :          35 :     channel->on_backpressure([this, weak_impl, weak_alive](size_t queued) {
     454                 :           1 :       bp_cv_.notify_all();
     455                 :           1 :       auto impl_keepalive = weak_impl.lock();
     456         [ -  + ]:           1 :       if (!impl_keepalive) return;
     457                 :           1 :       auto alive = weak_alive.lock();
     458         [ -  + ]:           1 :       if (!alive) return;
     459                 :           1 :       std::function<void(size_t)> handler;
     460                 :             :       {
     461                 :           1 :         std::shared_lock<std::shared_mutex> lock(mutex_);
     462                 :           1 :         handler = bp_handler;
     463                 :           1 :       }
     464                 :           1 :       detail::invoke_user_callback("udp_client", "on_backpressure", handler, queued);
     465                 :           1 :     });
     466                 :             : 
     467                 :          35 :     channel->on_state([this, weak_impl, weak_alive](base::LinkState state) {
     468                 :          63 :       auto impl_keepalive = weak_impl.lock();
     469         [ +  + ]:          63 :       if (!impl_keepalive) return;
     470                 :          57 :       auto alive = weak_alive.lock();
     471         [ -  + ]:          57 :       if (!alive) return;
     472                 :             : 
     473      [ +  +  + ]:          57 :       switch (state) {
     474                 :          21 :         case base::LinkState::Connected:
     475                 :             :         case base::LinkState::Listening: {
     476                 :          21 :           ConnectionHandler handler;
     477                 :             :           {
     478                 :          21 :             std::unique_lock<std::shared_mutex> lock(mutex_);
     479                 :          21 :             fulfill_all_locked(true);
     480                 :          21 :             handler = connect_handler;
     481                 :          21 :           }
     482                 :          21 :           detail::invoke_user_callback("udp_client", "on_connect", handler, ConnectionContext(0));
     483                 :          21 :           break;
     484                 :          21 :         }
     485                 :          18 :         case base::LinkState::Closed:
     486                 :             :         case base::LinkState::Error:
     487                 :             :         case base::LinkState::Idle: {
     488                 :          18 :           ConnectionHandler disconnect_handler_snapshot;
     489                 :          18 :           ErrorHandler error_handler_snapshot;
     490                 :             :           {
     491                 :          18 :             std::unique_lock<std::shared_mutex> lock(mutex_);
     492                 :          18 :             fulfill_all_locked(false);
     493         [ +  + ]:          18 :             if (state == base::LinkState::Error) {
     494                 :           3 :               error_handler_snapshot = error_handler;
     495                 :             :             } else {
     496                 :          15 :               disconnect_handler_snapshot = disconnect_handler;
     497                 :             :             }
     498                 :          18 :           }
     499                 :          18 :           detail::invoke_user_callback("udp_client", "on_disconnect", disconnect_handler_snapshot,
     500                 :          36 :                                        ConnectionContext(0));
     501                 :          18 :           detail::invoke_user_callback("udp_client", "on_error", error_handler_snapshot,
     502                 :          36 :                                        detail::build_error_context(*channel, "Connection error"));
     503                 :          18 :           break;
     504                 :          18 :         }
     505                 :          18 :         default:
     506                 :          18 :           break;
     507                 :             :       }
     508                 :          63 :     });
     509                 :          35 :   }
     510                 :             : 
     511                 :           7 :   void attach_framer_callback() {
     512         [ -  + ]:           7 :     if (!framer) return;
     513                 :           7 :     framer->on_message([this](memory::ConstByteSpan msg) {
     514                 :             :       // #441: snapshot under a shared_lock (pure read), build the copy
     515                 :             :       // before taking the exclusive lock for queue mutation.
     516                 :             :       bool batch_mode;
     517                 :         103 :       interface::SharedCallback<MessageHandler> handler;
     518                 :             :       {
     519                 :         103 :         std::shared_lock<std::shared_mutex> lock(mutex_);
     520                 :         103 :         batch_mode = static_cast<bool>(message_batch_handler_);
     521                 :         103 :         handler = message_handler;
     522                 :         103 :       }
     523                 :             : 
     524         [ +  - ]:         103 :       if (batch_mode) {
     525                 :         103 :         MessageContext ctx(0, memory::SafeDataBuffer(msg));
     526                 :         103 :         interface::SharedCallback<BatchMessageHandler> flush_handler;
     527                 :         103 :         std::vector<MessageContext> batch;
     528                 :             :         {
     529                 :         103 :           std::unique_lock<std::shared_mutex> lock(mutex_);
     530                 :         103 :           message_batch_queue_.emplace_back(std::move(ctx));
     531         [ +  + ]:         103 :           if (message_batch_queue_.size() >= max_batch_size_) {
     532                 :           2 :             flush_handler = message_batch_handler_;
     533                 :           2 :             batch = std::move(message_batch_queue_);
     534                 :           2 :             message_batch_queue_.clear();
     535         [ +  + ]:         101 :           } else if (message_batch_queue_.size() == 1) {
     536                 :           3 :             schedule_batch_timer();
     537                 :             :           }
     538                 :         103 :         }
     539                 :         103 :         detail::invoke_user_callback("udp_client", "on_message_batch", flush_handler, batch);
     540                 :         103 :         return;
     541                 :         103 :       }
     542                 :             : 
     543                 :           0 :       detail::invoke_user_callback("udp_client", "on_message", handler, MessageContext(0, msg));
     544                 :         103 :     });
     545                 :             :   }
     546                 :             : 
     547                 :           7 :   void set_framer(std::unique_ptr<framer::IFramer> f) {
     548                 :           7 :     std::unique_lock<std::shared_mutex> lock(mutex_);
     549                 :           7 :     framer = std::shared_ptr<framer::IFramer>(std::move(f));
     550   [ +  -  +  -  :           7 :     if (framer && (message_handler || message_batch_handler_)) attach_framer_callback();
          -  +  -  +  -  
                      - ]
     551                 :           7 :   }
     552                 :             : 
     553                 :           3 :   void on_message(MessageHandler handler) {
     554                 :           3 :     std::unique_lock<std::shared_mutex> lock(mutex_);
     555                 :           3 :     message_handler = interface::share_callback(std::move(handler));
     556   [ +  -  +  - ]:           3 :     if (framer) attach_framer_callback();
     557                 :           3 :   }
     558                 :             : 
     559                 :           4 :   void on_message_batch(BatchMessageHandler handler) {
     560                 :           4 :     std::unique_lock<std::shared_mutex> lock(mutex_);
     561                 :           4 :     message_batch_handler_ = interface::share_callback(std::move(handler));
     562   [ +  -  +  - ]:           4 :     if (framer) attach_framer_callback();
     563                 :           4 :   }
     564                 :             : };
     565                 :             : 
     566                 :          29 : UdpClient::UdpClient(const config::UdpConfig& cfg) : impl_(std::make_shared<Impl>(cfg)) {}
     567                 :           5 : UdpClient::UdpClient(const config::UdpConfig& cfg, std::shared_ptr<boost::asio::io_context> ioc)
     568                 :           5 :     : impl_(std::make_shared<Impl>(cfg, ioc)) {}
     569                 :          15 : UdpClient::UdpClient(std::shared_ptr<interface::Channel> ch) : impl_(std::make_shared<Impl>(ch)) {
     570                 :          15 :   impl_->setup_internal_handlers();
     571                 :          15 : }
     572                 :          56 : UdpClient::~UdpClient() = default;
     573                 :             : 
     574                 :           0 : UdpClient::UdpClient(UdpClient&&) noexcept = default;
     575                 :           0 : UdpClient& UdpClient::operator=(UdpClient&&) noexcept = default;
     576                 :             : 
     577                 :          32 : std::future<bool> UdpClient::start() { return impl_->start(); }
     578                 :          25 : void UdpClient::stop() { impl_->stop(); }
     579                 :          13 : bool UdpClient::send(std::string_view data) { return impl_->send(data); }
     580                 :           2 : bool UdpClient::try_send(std::string_view data) { return impl_->try_send(data); }
     581                 :           5 : bool UdpClient::send_line(std::string_view line) { return impl_->send_line(line); }
     582                 :           1 : bool UdpClient::try_send_line(std::string_view line) { return impl_->try_send_line(line); }
     583                 :           2 : bool UdpClient::send_move(std::vector<uint8_t>&& data) { return impl_->send_move(std::move(data)); }
     584                 :           2 : bool UdpClient::try_send_move(std::vector<uint8_t>&& data) { return impl_->try_send_move(std::move(data)); }
     585                 :           2 : bool UdpClient::send_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     586                 :           2 :   return impl_->send_shared(std::move(data));
     587                 :             : }
     588                 :           4 : bool UdpClient::try_send_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     589                 :           4 :   return impl_->try_send_shared(std::move(data));
     590                 :             : }
     591                 :           4 : bool UdpClient::send_blocking(std::string_view data) { return impl_->send_blocking(data); }
     592                 :           0 : bool UdpClient::send_line_blocking(std::string_view line) { return impl_->send_line_blocking(line); }
     593                 :          11 : bool UdpClient::connected() const {
     594                 :          11 :   std::shared_lock<std::shared_mutex> lock(impl_->mutex_);
     595   [ +  +  +  -  :          22 :   return impl_->channel && impl_->channel->is_connected();
                   +  + ]
     596                 :          11 : }
     597                 :           8 : RuntimeStats UdpClient::stats() const { return impl_->stats(); }
     598                 :           0 : void UdpClient::reset_stats() { impl_->reset_stats(); }
     599                 :             : 
     600                 :          13 : UdpClient& UdpClient::on_data(MessageHandler h) {
     601                 :          13 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     602                 :          13 :   impl_->data_handler = interface::share_callback(std::move(h));
     603                 :          13 :   return *this;
     604                 :          13 : }
     605                 :           3 : UdpClient& UdpClient::on_data_batch(BatchMessageHandler h) {
     606                 :           3 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     607                 :           3 :   impl_->data_batch_handler_ = interface::share_callback(std::move(h));
     608                 :           3 :   return *this;
     609                 :           3 : }
     610                 :           6 : UdpClient& UdpClient::on_connect(ConnectionHandler h) {
     611                 :           6 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     612                 :           6 :   impl_->connect_handler = std::move(h);
     613                 :           6 :   return *this;
     614                 :           6 : }
     615                 :           4 : UdpClient& UdpClient::on_disconnect(ConnectionHandler h) {
     616                 :           4 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     617                 :           4 :   impl_->disconnect_handler = std::move(h);
     618                 :           4 :   return *this;
     619                 :           4 : }
     620                 :           7 : UdpClient& UdpClient::on_error(ErrorHandler h) {
     621                 :           7 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     622                 :           7 :   impl_->error_handler = std::move(h);
     623                 :           7 :   return *this;
     624                 :           7 : }
     625                 :             : 
     626                 :           3 : UdpClient& UdpClient::on_backpressure(std::function<void(size_t)> h) {
     627                 :           3 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     628                 :           3 :   impl_->bp_handler = std::move(h);
     629                 :           3 :   return *this;
     630                 :           3 : }
     631                 :             : 
     632                 :           7 : UdpClient& UdpClient::framer(std::unique_ptr<framer::IFramer> f) {
     633                 :           7 :   impl_->set_framer(std::move(f));
     634                 :           7 :   return *this;
     635                 :             : }
     636                 :           3 : UdpClient& UdpClient::on_message(MessageHandler h) {
     637                 :           3 :   impl_->on_message(std::move(h));
     638                 :           3 :   return *this;
     639                 :             : }
     640                 :           4 : UdpClient& UdpClient::on_message_batch(BatchMessageHandler h) {
     641                 :           4 :   impl_->on_message_batch(std::move(h));
     642                 :           4 :   return *this;
     643                 :             : }
     644                 :             : 
     645                 :           4 : UdpClient& UdpClient::auto_start(bool m) {
     646                 :           4 :   impl_->auto_start_.store(m);
     647   [ +  +  +  -  :           4 :   if (impl_->auto_start_.load() && !impl_->started_.load()) start();
             +  +  +  - ]
     648                 :           4 :   return *this;
     649                 :             : }
     650                 :             : 
     651                 :          10 : UdpClient& UdpClient::backpressure_threshold(size_t threshold) {
     652                 :          10 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     653                 :          10 :   impl_->cfg.backpressure_threshold = threshold;
     654                 :          10 :   return *this;
     655                 :          10 : }
     656                 :             : 
     657                 :           8 : UdpClient& UdpClient::backpressure_strategy(base::constants::BackpressureStrategy strategy) {
     658                 :           8 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     659                 :           8 :   impl_->cfg.backpressure_strategy = strategy;
     660         [ +  + ]:           8 :   if (impl_->channel) {
     661   [ +  -  -  + ]:           2 :     if (auto* uc = dynamic_cast<transport::UdpChannel*>(impl_->channel.get())) {
     662                 :           0 :       uc->set_backpressure_strategy(strategy);
     663                 :             :     }
     664                 :             :   }
     665                 :           8 :   return *this;
     666                 :           8 : }
     667                 :             : 
     668                 :           3 : size_t UdpClient::backpressure_threshold() const {
     669                 :           3 :   std::shared_lock<std::shared_mutex> lock(impl_->mutex_);
     670                 :           6 :   return impl_->cfg.backpressure_threshold;
     671                 :           3 : }
     672                 :             : 
     673                 :           3 : base::constants::BackpressureStrategy UdpClient::backpressure_strategy() const {
     674                 :           3 :   std::shared_lock<std::shared_mutex> lock(impl_->mutex_);
     675                 :           6 :   return impl_->cfg.backpressure_strategy;
     676                 :           3 : }
     677                 :             : 
     678                 :           0 : UdpClient& UdpClient::send_buffer_size(size_t bytes) {
     679                 :           0 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     680                 :           0 :   impl_->cfg.send_buffer_size = bytes;
     681                 :           0 :   return *this;
     682                 :           0 : }
     683                 :             : 
     684                 :           0 : UdpClient& UdpClient::receive_buffer_size(size_t bytes) {
     685                 :           0 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     686                 :           0 :   impl_->cfg.receive_buffer_size = bytes;
     687                 :           0 :   return *this;
     688                 :           0 : }
     689                 :             : 
     690                 :           6 : UdpClient& UdpClient::manage_external_context(bool m) {
     691                 :           6 :   impl_->manage_external_context.store(m);
     692                 :           6 :   return *this;
     693                 :             : }
     694                 :             : 
     695                 :           3 : UdpClient& UdpClient::batch_size(size_t size) {
     696                 :           3 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     697                 :           3 :   impl_->max_batch_size_ = size;
     698                 :           3 :   return *this;
     699                 :           3 : }
     700                 :             : 
     701                 :           3 : UdpClient& UdpClient::batch_latency(std::chrono::milliseconds latency) {
     702                 :           3 :   std::unique_lock<std::shared_mutex> lock(impl_->mutex_);
     703                 :           3 :   impl_->max_batch_latency_ = latency;
     704                 :           3 :   return *this;
     705                 :           3 : }
     706                 :             : 
     707                 :             : }  // namespace wrapper
     708                 :             : }  // namespace wirestead
        

Generated by: LCOV version 2.0-1