LCOV - code coverage report
Current view: top level - wirestead/wrapper/serial - serial.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 95.0 % 543 516
Test Date: 2026-08-30 10:35:09 Functions: 95.6 % 91 87
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 61.0 % 323 197

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

Generated by: LCOV version 2.0-1