LCOV - code coverage report
Current view: top level - wirestead/transport/tcp_client - tcp_client.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 91.7 % 1129 1035
Test Date: 2026-10-29 11:36:05 Functions: 96.7 % 120 116
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 73.6 % 726 534

             Branch data     Line data    Source code
       1                 :             : /*
       2                 :             :  * Copyright 2025 Jinwoo Sung
       3                 :             :  *
       4                 :             :  * Licensed under the Apache License, Version 2.0 (the "License");
       5                 :             :  * you may not use this file except in compliance with the License.
       6                 :             :  * You may obtain a copy of the License at
       7                 :             :  *
       8                 :             :  *     http://www.apache.org/licenses/LICENSE-2.0
       9                 :             :  *
      10                 :             :  * Unless required by applicable law or agreed to in writing, software
      11                 :             :  * distributed under the License is distributed on an "AS IS" BASIS,
      12                 :             :  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13                 :             :  * See the License for the specific language governing permissions and
      14                 :             :  * limitations under the License.
      15                 :             :  */
      16                 :             : 
      17                 :             : #include "wirestead/transport/tcp_client/tcp_client.hpp"
      18                 :             : 
      19                 :             : #include "wirestead/concurrency/io_thread_hook.hpp"
      20                 :             : #include "wirestead/config/validation.hpp"
      21                 :             : #include "wirestead/diagnostics/callback.hpp"
      22                 :             : #include "wirestead/transport/base/stop_test_hook.hpp"
      23                 :             : 
      24                 :             : #if defined(__GNUC__) || defined(__clang__)
      25                 :             : #pragma GCC diagnostic ignored "-Wsign-conversion"
      26                 :             : #endif
      27                 :             : 
      28                 :             : #include <spdlog/fmt/fmt.h>
      29                 :             : 
      30                 :             : #include <algorithm>
      31                 :             : #include <array>
      32                 :             : #include <atomic>
      33                 :             : #include <boost/asio.hpp>
      34                 :             : #ifdef WIRESTEAD_TLS_ENABLED
      35                 :             : #include <boost/asio/ssl.hpp>
      36                 :             : #endif
      37                 :             : #include <condition_variable>
      38                 :             : #include <cstdint>
      39                 :             : #include <cstring>
      40                 :             : #include <deque>
      41                 :             : #include <iostream>
      42                 :             : #include <memory>
      43                 :             : #include <mutex>
      44                 :             : #include <optional>
      45                 :             : #include <stop_token>
      46                 :             : #include <string>
      47                 :             : #include <thread>
      48                 :             : #include <type_traits>
      49                 :             : #include <variant>
      50                 :             : #include <vector>
      51                 :             : 
      52                 :             : #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
      53                 :             : #include <sys/socket.h>
      54                 :             : #include <sys/types.h>
      55                 :             : #endif
      56                 :             : 
      57                 :             : #include "wirestead/base/constants.hpp"
      58                 :             : #include "wirestead/concurrency/io_context_manager.hpp"
      59                 :             : #include "wirestead/concurrency/thread_safe_state.hpp"
      60                 :             : #include "wirestead/diagnostics/error_handler.hpp"
      61                 :             : #include "wirestead/diagnostics/error_mapping.hpp"
      62                 :             : #include "wirestead/diagnostics/logger.hpp"
      63                 :             : #include "wirestead/diagnostics/runtime_stats_counter.hpp"
      64                 :             : #include "wirestead/diagnostics/send_accounting.hpp"
      65                 :             : #include "wirestead/memory/memory_pool.hpp"
      66                 :             : #include "wirestead/transport/base/bp_state_machine.hpp"
      67                 :             : #include "wirestead/transport/base/bp_utils.hpp"
      68                 :             : #include "wirestead/transport/base/error_info_holder.hpp"
      69                 :             : #include "wirestead/transport/tcp_client/detail/reconnect_decider.hpp"
      70                 :             : #include "wirestead/transport/tcp_client/detail/write_wait.hpp"
      71                 :             : 
      72                 :             : namespace wirestead {
      73                 :             : namespace transport {
      74                 :             : 
      75                 :             : namespace net = boost::asio;
      76                 :             : using tcp = net::ip::tcp;
      77                 :             : 
      78                 :             : using base::LinkState;
      79                 :             : using concurrency::AtomicLinkState;
      80                 :             : using config::TcpClientConfig;
      81                 :             : using interface::Channel;
      82                 :             : 
      83                 :             : struct TcpClient::Impl {
      84                 :             :   using Ledger = diagnostics::SendAccountingLedger;
      85                 :             :   struct TrackedBuffer {
      86                 :             :     BufferVariant buffer;
      87                 :             :     Ledger::Request request;
      88                 :             :   };
      89                 :             :   struct BufferProjection {
      90                 :             :     template <typename T>
      91                 :        8128 :     decltype(auto) operator()(T& item) const {
      92                 :        8128 :       return (item.buffer);
      93                 :             :     }
      94                 :             :   };
      95                 :             :   Ledger send_accounting_;
      96                 :             :   // Members moved from TcpClient
      97                 :             :   std::shared_ptr<net::io_context> owned_ioc_;
      98                 :             :   net::io_context* ioc_ = nullptr;
      99                 :             :   net::strand<net::io_context::executor_type> strand_;
     100                 :             :   // Internal write lambdas have the default allocator and no associated executor.
     101                 :             :   // Cache post's properties once; never execute inline under submission_mtx_.
     102                 :             :   const net::strand<net::io_context::executor_type> write_executor_;
     103                 :             :   std::unique_ptr<net::executor_work_guard<net::io_context::executor_type>> work_guard_;
     104                 :             :   std::jthread ioc_thread_;
     105                 :             :   std::atomic<uint64_t> lifecycle_seq_{0};
     106                 :             :   std::atomic<uint64_t> stop_seq_{0};
     107                 :             :   std::atomic<uint64_t> current_seq_{0};
     108                 :             :   // Changes when a usable connection ends, independently of start/stop runs.
     109                 :             :   std::atomic<uint64_t> connection_seq_{0};
     110                 :             :   std::shared_ptr<detail::TcpWriteWait> write_wait_;
     111                 :             :   tcp::resolver resolver_;
     112                 :             :   tcp::socket socket_;
     113                 :             : 
     114                 :             : #ifdef WIRESTEAD_TLS_ENABLED
     115                 :             :   // The stream borrows socket_ rather than owning it, so connect, socket
     116                 :             :   // options, cancel and close all keep operating on socket_ exactly as they do
     117                 :             :   // without TLS. Only reads, writes and shutdown route through here, and only
     118                 :             :   // while a connection is up - it is rebuilt per connection because a TLS
     119                 :             :   // session cannot outlive the socket it negotiated on.
     120                 :             :   std::shared_ptr<boost::asio::ssl::context> ssl_context_;
     121                 :             :   std::optional<boost::asio::ssl::stream<tcp::socket&>> tls_;
     122                 :             :   // Whether IO should currently be routed through tls_, tracked separately
     123                 :             :   // from tls_.has_value() because the stream now outlives the connection - see
     124                 :             :   // close_socket(). Strand-confined, like the stream itself.
     125                 :             :   bool tls_engaged_ = false;
     126                 :             : #endif
     127                 :             : 
     128                 :             :   // True when this connection is encrypted. Reads to it happen on the strand.
     129                 :        2313 :   bool tls_active() const {
     130                 :             : #ifdef WIRESTEAD_TLS_ENABLED
     131                 :        2313 :     return tls_engaged_;
     132                 :             : #else
     133                 :             :     return false;
     134                 :             : #endif
     135                 :             :   }
     136                 :             :   // Guards the mutable subset of cfg_ (retry_interval_ms, max_retries,
     137                 :             :   // connection_timeout_ms, idle_timeout_ms, idle_timeout_action) and
     138                 :             :   // reconnect_policy_ below - the fields that have runtime setters
     139                 :             :   // (set_retry_interval() etc.) reachable from any user thread while the
     140                 :             :   // strand concurrently reads them for reconnect/idle-timeout decisions.
     141                 :             :   // Fields with no runtime setter (tcp_no_delay, keep_alive, buffer sizes)
     142                 :             :   // are set once at construction and read only at connect time, so they
     143                 :             :   // don't need this lock (#436).
     144                 :             :   mutable std::mutex cfg_mtx_;
     145                 :             :   TcpClientConfig cfg_;
     146                 :             :   // #443: per-channel pool instead of the process-wide GlobalMemoryPool
     147                 :             :   // singleton - avoids cross-channel contention on the singleton's bucket
     148                 :             :   // mutexes. Capacity is much smaller than the old shared default (400/2000)
     149                 :             :   // since it's no longer amortized across every channel in the process.
     150                 :             :   // Prefill stays 0. This literal was written while MemoryPool discarded
     151                 :             :   // initial_pool_size, so 50 allocated nothing; #575 made the parameter real
     152                 :             :   // and turned it into ~1 MiB eagerly allocated per channel at construction.
     153                 :             :   // The pool fills as buffers are released.
     154                 :             :   memory::MemoryPool pool_{0, 200};
     155                 :             :   net::steady_timer retry_timer_;
     156                 :             :   net::steady_timer connect_timer_;
     157                 :             :   net::steady_timer idle_timer_;
     158                 :             :   bool owns_ioc_ = true;
     159                 :             :   // Orders caller-side write admission and strand submission before stop.
     160                 :             :   // Never run strand handlers/user callbacks while holding this mutex.
     161                 :             :   std::mutex submission_mtx_;
     162                 :             :   std::atomic<bool> stop_requested_{false};
     163                 :             :   std::atomic<bool> stopping_{false};
     164                 :             :   std::atomic<bool> terminal_state_notified_{false};
     165                 :             :   std::atomic<bool> reconnect_pending_{false};
     166                 :             : 
     167                 :             :   // Per-connection storage, sized from cfg_.read_buffer_size. A cancelled
     168                 :             :   // read retains its buffer even if a replacement connection is ready.
     169                 :             :   std::shared_ptr<std::vector<uint8_t>> rx_;
     170                 :             :   std::deque<TrackedBuffer> tx_;
     171                 :             :   std::deque<TrackedBuffer> pending_;
     172                 :             :   std::atomic<size_t> pending_bytes_{0};
     173                 :             :   // Each operation owns its buffers until its completion handler returns,
     174                 :             :   // even if a replacement connection has already started writing.
     175                 :             :   struct WriteBatch {
     176                 :             :     std::vector<TrackedBuffer> buffers;
     177                 :             :     std::vector<net::const_buffer> views;
     178                 :             :     size_t bytes = 0;
     179                 :             :   };
     180                 :             :   std::shared_ptr<WriteBatch> active_write_;
     181                 :             :   bool writing_ = false;
     182                 :             :   std::atomic<size_t> queue_bytes_{0};
     183                 :             :   // Bytes accepted by a plain async_write_* call but not yet routed onto the
     184                 :             :   // strand - reserved via try_reserve_limit_bytes() to close the
     185                 :             :   // accept-then-drop race (jwsung91/wirestead#517). inflight_bytes_ mutations
     186                 :             :   // and the queue_bytes_/pending_bytes_ increments that promote a
     187                 :             :   // reservation both go through write_reserve_mtx_ - see bp_utils.hpp.
     188                 :             :   std::atomic<size_t> inflight_bytes_{0};
     189                 :             :   std::mutex write_reserve_mtx_;
     190                 :             :   // D-1: shutdown *requested* and shutdown *completed* are separate states.
     191                 :             :   // The cleanup handler that runs on the strand is what completes it, and
     192                 :             :   // waiting callers wait for that signal rather than for a mutex - a mutex
     193                 :             :   // only says another caller was here, not that the teardown ran.
     194                 :             :   std::mutex stop_mtx_;
     195                 :             :   std::condition_variable stop_cv_;
     196                 :             :   bool cleanup_done_ = false;
     197                 :             :   std::atomic<bool> cleanup_started_{false};
     198                 :             :   // Strand-confined. Cancellation does not mean its completion handler has
     199                 :             :   // run; keep the run closed until those handlers have left the old buffers.
     200                 :             :   size_t pending_io_ = 0;
     201                 :             :   bool cleanup_finished_ = false;
     202                 :             :   struct IoCompletion {
     203                 :             :     Impl* impl;
     204                 :        4275 :     ~IoCompletion() {
     205   [ +  +  +  + ]:        4275 :       if (impl->cleanup_finished_ && impl->pending_io_ == 1) {
     206         [ +  + ]:         476 :         if (auto hook = detail::g_tcp_io_completion_hook.load()) hook();
     207                 :             :       }
     208   [ +  +  +  +  :        4275 :       if (--impl->pending_io_ == 0 && impl->cleanup_finished_) impl->mark_cleanup_done();
                   +  + ]
     209                 :        4275 :     }
     210                 :             :   };
     211                 :             : 
     212                 :             :   // Serializes the join itself: joining a thread from two callers is not
     213                 :             :   // allowed, and the second caller must not return before the first has.
     214                 :             :   std::mutex join_mtx_;
     215                 :             : 
     216                 :         722 :   void mark_cleanup_done() {
     217                 :         722 :     detail::stop_test_hook(this, true);
     218                 :             :     {
     219                 :         722 :       std::lock_guard<std::mutex> lock(stop_mtx_);
     220                 :         722 :       cleanup_done_ = true;
     221                 :         722 :     }
     222                 :         722 :     stop_cv_.notify_all();
     223                 :         722 :   }
     224                 :             : 
     225                 :             :   // Waits for the teardown that was posted to the strand. The contract's
     226                 :             :   // precondition for an externally run io_context is that the caller keeps it
     227                 :             :   // running, so this waits and nothing more: running the caller's executor
     228                 :             :   // here would execute other channels' handlers on the stopping thread.
     229                 :        1459 :   void wait_for_cleanup() {
     230                 :        1459 :     detail::stop_test_hook(this, false);
     231                 :        1459 :     std::unique_lock<std::mutex> lock(stop_mtx_);
     232                 :        3576 :     stop_cv_.wait(lock, [this] { return cleanup_done_; });
     233                 :        1459 :   }
     234                 :             : 
     235                 :             :   // Called with submission_mtx_ held. Explicit stop and readiness publication
     236                 :             :   // use that mutex; cleanup completion has its own condition-variable mutex.
     237                 :        6471 :   std::optional<wrapper::SendRejection> admission_rejection(
     238                 :             :       std::optional<uint64_t> expected_connection = std::nullopt) {
     239         [ +  + ]:        6471 :     if (stop_requested_.load()) {
     240                 :         151 :       std::lock_guard<std::mutex> lock(stop_mtx_);
     241         [ +  + ]:         151 :       return cleanup_done_ ? wrapper::SendRejection::NotStarted : wrapper::SendRejection::Stopping;
     242                 :         151 :     }
     243         [ +  + ]:       12640 :     if (current_seq_.load() == 0) return wrapper::SendRejection::NotStarted;
     244   [ +  +  +  -  :       20549 :     if ((expected_connection && *expected_connection != connection_seq_.load()) || state_.is_state(LinkState::Closed) ||
                   +  - ]
     245   [ +  +  +  -  :       18747 :         state_.is_state(LinkState::Error) || !ioc_ || !connected_.load()) {
             +  +  +  + ]
     246                 :         104 :       return wrapper::SendRejection::NotReady;
     247                 :             :     }
     248                 :        6147 :     return std::nullopt;
     249                 :             :   }
     250                 :             : 
     251                 :             :   // Atomic rather than mutex-guarded: read both from the strand and from
     252                 :             :   // arbitrary caller threads (async_try_write_* fast-fail prechecks) (#436).
     253                 :             :   std::atomic<base::constants::BackpressureStrategy> bp_strategy_{base::constants::BackpressureStrategy::Reliable};
     254                 :             :   size_t bp_high_;
     255                 :             :   size_t bp_low_;
     256                 :             :   size_t bp_limit_;
     257                 :             :   std::atomic<bool> backpressure_active_{false};
     258                 :             :   diagnostics::RuntimeStatsCounters stats_;
     259                 :             :   unsigned first_retry_interval_ms_ = 100;
     260                 :             : 
     261                 :             :   // Shared snapshots rather than plain std::functions: the io thread copies
     262                 :             :   // one out per received chunk, and a std::function copy allocates whenever
     263                 :             :   // the target outgrows its small-object buffer. See interface::SharedCallback.
     264                 :             :   interface::SharedCallback<OnBytes> on_bytes_;
     265                 :             :   interface::SharedCallback<OnState> on_state_;
     266                 :             :   interface::SharedCallback<OnBackpressure> on_bp_;
     267                 :             :   mutable std::mutex callback_mtx_;
     268                 :             :   std::atomic<bool> connected_{false};
     269                 :         464 :   AtomicLinkState state_{LinkState::Idle};
     270                 :             :   int retry_attempts_ = 0;
     271                 :             :   uint32_t reconnect_attempt_count_{0};
     272                 :             :   std::optional<ReconnectPolicy> reconnect_policy_;
     273                 :             : 
     274                 :             :   ErrorInfoHolder error_info_holder_{"tcp_client"};
     275                 :             : 
     276                 :         464 :   Impl(const TcpClientConfig& cfg, net::io_context* ioc_ptr)
     277   [ +  +  +  - ]:         464 :       : owned_ioc_(ioc_ptr ? nullptr : std::make_shared<net::io_context>()),
     278         [ +  + ]:         464 :         ioc_(ioc_ptr ? ioc_ptr : owned_ioc_.get()),
     279                 :         464 :         strand_(net::make_strand(*ioc_)),
     280                 :         464 :         write_executor_(net::prefer(net::require(strand_, net::execution::blocking.never),
     281                 :             :                                     net::execution::relationship.fork,
     282                 :         464 :                                     net::execution::allocator(std::allocator<void>{}))),
     283                 :         464 :         resolver_(strand_),
     284                 :         464 :         socket_(strand_),
     285                 :         464 :         cfg_(cfg),
     286                 :         464 :         retry_timer_(strand_),
     287                 :         464 :         connect_timer_(strand_),
     288                 :         464 :         idle_timer_(strand_),
     289                 :         464 :         owns_ioc_(!ioc_ptr),
     290                 :         464 :         bp_strategy_(cfg.backpressure_strategy),
     291                 :        3248 :         bp_high_(cfg.backpressure_threshold) {
     292                 :         464 :     init();
     293                 :         489 :   }
     294                 :             : 
     295                 :        2210 :   void mark_disconnected() {
     296                 :        2210 :     std::lock_guard<std::mutex> lock(submission_mtx_);
     297                 :        2210 :     mark_disconnected_locked();
     298                 :        2210 :   }
     299                 :             : 
     300                 :             :   // Admission, write completion and explicit stop use the same terminal boundary.
     301                 :        2237 :   void mark_disconnected_locked() {
     302         [ +  + ]:        2237 :     if (connected_.exchange(false)) {
     303                 :         505 :       send_accounting_.end(Ledger::Cause::ConnectionLoss);
     304         [ +  - ]:         505 :       if (write_wait_) write_wait_->end(wrapper::SendRejection::NotReady);
     305                 :         505 :       connection_seq_.fetch_add(1);
     306                 :             :     }
     307                 :        2237 :   }
     308                 :             : 
     309                 :             :   void discard_connection_writes();
     310                 :             : 
     311                 :         464 :   void init() {
     312                 :         464 :     connected_ = false;
     313                 :         464 :     writing_ = false;
     314                 :         464 :     queue_bytes_ = 0;
     315                 :         464 :     pending_bytes_ = 0;
     316                 :         464 :     config::detail::validate(cfg_);
     317                 :         463 :     rx_ = std::make_shared<std::vector<uint8_t>>(cfg_.read_buffer_size);
     318                 :         463 :     recalculate_backpressure_bounds();
     319                 :         463 :     first_retry_interval_ms_ = std::min(first_retry_interval_ms_, cfg_.retry_interval_ms);
     320                 :         463 :   }
     321                 :             : 
     322                 :             :   void do_resolve_connect(std::shared_ptr<TcpClient> self, uint64_t seq);
     323                 :             :   void schedule_retry(std::shared_ptr<TcpClient> self, uint64_t seq);
     324                 :             :   void start_read(std::shared_ptr<TcpClient> self, uint64_t seq);
     325                 :             :   void do_write(std::shared_ptr<TcpClient> self, uint64_t seq);
     326                 :             :   void handle_close(std::shared_ptr<TcpClient> self, uint64_t seq, const boost::system::error_code& ec = {});
     327                 :             :   void handle_idle_timeout(std::shared_ptr<TcpClient> self, uint64_t seq);
     328                 :             :   void transition_to(LinkState next, const boost::system::error_code& ec = {});
     329                 :             :   void perform_stop_cleanup();
     330                 :             :   void reset_start_state();
     331                 :             :   void join_ioc_thread(bool allow_detach);
     332                 :             :   void close_socket();
     333                 :             :   void recalculate_backpressure_bounds();
     334                 :             :   void report_backpressure(std::shared_ptr<TcpClient> self, size_t queued_bytes);
     335                 :             :   void observe_queue();
     336                 :             :   // Shared decide_enqueue()/route dispatch used by all 3 async_write_* variants (#434).
     337                 :             :   // Every caller reserves added bytes in inflight_bytes_ before posting.
     338                 :             :   void route_enqueued_buffer(std::shared_ptr<TcpClient> self, TrackedBuffer&& buf, size_t added);
     339                 :             :   queue_util::BackpressureFields bp_fields();
     340                 :             :   void notify_state();
     341                 :             :   void reset_io_objects();
     342                 :             :   void apply_socket_options();
     343                 :             :   void handshake_then(std::shared_ptr<TcpClient> self, uint64_t seq, std::function<void()> next);
     344                 :             :   void finish_connect(std::shared_ptr<TcpClient> self, uint64_t seq);
     345                 :             :   void reset_idle_timer(std::shared_ptr<TcpClient> self, uint64_t seq);
     346                 :             :   void cancel_idle_timer();
     347                 :             :   void record_error(diagnostics::ErrorLevel lvl, diagnostics::ErrorCategory cat, std::string_view operation,
     348                 :             :                     const boost::system::error_code& ec, std::string_view msg, bool retryable, uint32_t retry_count);
     349                 :             : };
     350                 :             : 
     351                 :         148 : std::shared_ptr<TcpClient> TcpClient::create(const TcpClientConfig& cfg) {
     352                 :         148 :   return std::shared_ptr<TcpClient>(new TcpClient(cfg));
     353                 :             : }
     354                 :             : 
     355                 :         316 : std::shared_ptr<TcpClient> TcpClient::create(const TcpClientConfig& cfg, boost::asio::io_context& ioc) {
     356                 :         316 :   return std::shared_ptr<TcpClient>(new TcpClient(cfg, ioc));
     357                 :             : }
     358                 :             : 
     359                 :         149 : TcpClient::TcpClient(const TcpClientConfig& cfg) : impl_(std::make_unique<Impl>(cfg, nullptr)) {}
     360                 :         316 : TcpClient::TcpClient(const TcpClientConfig& cfg, boost::asio::io_context& ioc)
     361                 :         316 :     : impl_(std::make_unique<Impl>(cfg, &ioc)) {}
     362                 :             : 
     363                 :         927 : TcpClient::~TcpClient() {
     364                 :             :   // #446: null after being moved-from - the move ctor/assignment are
     365                 :             :   // defaulted, and destroying a moved-from instance must not dereference
     366                 :             :   // a null impl_ (matches TcpServer/Serial/UdpChannel/UdsServer's
     367                 :             :   // destructors, which already guard this way).
     368         [ +  + ]:         464 :   if (!impl_) return;
     369                 :         463 :   stop();
     370                 :         463 :   impl_->join_ioc_thread(true);
     371                 :             : 
     372                 :         463 :   impl_->on_bytes_ = nullptr;
     373                 :         463 :   impl_->on_state_ = nullptr;
     374                 :         463 :   impl_->on_bp_ = nullptr;
     375                 :         929 : }
     376                 :             : 
     377                 :           1 : TcpClient::TcpClient(TcpClient&&) noexcept = default;
     378                 :           0 : TcpClient& TcpClient::operator=(TcpClient&&) noexcept = default;
     379                 :             : 
     380                 :           5 : std::optional<diagnostics::ErrorInfo> TcpClient::last_error_info() const {
     381                 :           5 :   return impl_->error_info_holder_.last_error_info();
     382                 :             : }
     383                 :             : 
     384                 :         700 : void TcpClient::start() {
     385                 :         700 :   auto current_state = impl_->state_.get();
     386   [ +  -  -  + ]:         700 :   if (current_state == LinkState::Connecting || current_state == LinkState::Connected) {
     387                 :           0 :     WIRESTEAD_LOG_DEBUG("tcp_client", "start", "Start called while already active, ignoring");
     388                 :           0 :     return;
     389                 :             :   }
     390                 :             : 
     391         [ -  + ]:         700 :   if (!impl_->ioc_) {
     392                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "start", "io_context is null");
     393                 :             :   }
     394                 :             : 
     395                 :         700 :   impl_->recalculate_backpressure_bounds();
     396                 :             : 
     397   [ +  -  +  -  :         700 :   if (impl_->ioc_ && impl_->ioc_->stopped()) {
             +  +  +  + ]
     398                 :          21 :     WIRESTEAD_LOG_DEBUG("tcp_client", "start", "io_context stopped; restarting before start");
     399                 :          21 :     impl_->ioc_->restart();
     400                 :             :   }
     401                 :             : 
     402         [ -  + ]:         700 :   if (impl_->ioc_thread_.joinable()) {
     403                 :           0 :     impl_->join_ioc_thread(false);
     404                 :             :   }
     405                 :             : 
     406                 :         700 :   const auto seq = impl_->lifecycle_seq_.fetch_add(1) + 1;
     407                 :         700 :   impl_->current_seq_.store(seq);
     408                 :         700 :   impl_->reset_start_state();
     409                 :             : 
     410   [ +  +  +  -  :         700 :   if (impl_->owns_ioc_ && impl_->ioc_) {
                   +  + ]
     411                 :         163 :     impl_->work_guard_ =
     412                 :         326 :         std::make_unique<net::executor_work_guard<net::io_context::executor_type>>(impl_->ioc_->get_executor());
     413                 :         326 :     impl_->ioc_thread_ = std::jthread([ioc = impl_->owned_ioc_](std::stop_token st) {
     414                 :         163 :       wirestead::concurrency::run_io_thread_init();
     415                 :             :       try {
     416                 :         163 :         std::stop_callback cb(st, [ioc] { ioc->stop(); });
     417                 :         163 :         ioc->run();
     418                 :         163 :       } catch (const std::exception& e) {
     419                 :           0 :         WIRESTEAD_LOG_ERROR("tcp_client", "io_context", fmt::format("IO context error: {}", e.what()));
     420                 :           0 :         diagnostics::error_reporting::report_system_error("tcp_client", "io_context",
     421                 :           0 :                                                           fmt::format("Exception in IO context: {}", e.what()));
     422                 :           0 :       }
     423                 :         326 :     });
     424                 :             :   }
     425                 :             : 
     426                 :         700 :   auto weak_self = weak_from_this();
     427         [ +  - ]:         700 :   if (impl_->ioc_) {
     428                 :         700 :     net::dispatch(impl_->strand_, [weak_self, seq] {
     429         [ +  - ]:         700 :       if (auto self = weak_self.lock()) {
     430         [ +  + ]:        1400 :         if (seq <= self->impl_->stop_seq_.load()) {
     431                 :         205 :           return;
     432                 :             :         }
     433                 :         495 :         self->impl_->mark_disconnected();
     434                 :         495 :         self->impl_->reset_io_objects();
     435                 :         495 :         self->impl_->transition_to(LinkState::Connecting);
     436                 :         495 :         self->impl_->do_resolve_connect(self, seq);
     437                 :         700 :       }
     438                 :             :     });
     439                 :             :   } else {
     440                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "start", "io_context is null");
     441                 :             :   }
     442                 :         700 : }
     443                 :             : 
     444                 :        1524 : void TcpClient::stop() {
     445                 :        1524 :   const bool on_executor = impl_->ioc_->get_executor().running_in_this_thread();
     446                 :             :   bool first;
     447                 :             :   {
     448                 :        1524 :     std::lock_guard<std::mutex> submission_lock(impl_->submission_mtx_);
     449                 :        1524 :     first = !impl_->stop_requested_.exchange(true);
     450         [ +  + ]:        1524 :     if (first) {
     451                 :         722 :       impl_->send_accounting_.end(Impl::Ledger::Cause::ExplicitStop);
     452         [ +  + ]:         722 :       if (impl_->write_wait_) impl_->write_wait_->end(wrapper::SendRejection::CancelledWhileWaiting);
     453                 :         722 :       impl_->stopping_.store(true);
     454                 :        1444 :       impl_->stop_seq_.store(impl_->current_seq_.load());
     455                 :             :     }
     456                 :        1524 :   }
     457         [ +  + ]:        1524 :   if (first) {
     458                 :             :     // Only a never-started transport has no executor work to serialize with.
     459         [ +  + ]:        1444 :     if (impl_->current_seq_.load() == 0) {
     460                 :          23 :       impl_->perform_stop_cleanup();
     461                 :             :     } else {
     462                 :             :       // A waiting destructor keeps Impl alive until this handler completes.
     463                 :             :       // Ordinary requests retain the transport across request-only returns.
     464                 :         699 :       auto self = weak_from_this().lock();
     465                 :         699 :       auto* impl = impl_.get();
     466                 :        1398 :       net::post(impl_->strand_, [self, impl] { impl->perform_stop_cleanup(); });
     467                 :         699 :     }
     468                 :             :   }
     469         [ +  + ]:        1524 :   if (on_executor) return;
     470                 :             : 
     471                 :             :   // External executors must keep progressing, as required by D-1. Never
     472                 :             :   // poll them here or infer completion from stopped()/elapsed time.
     473                 :        1459 :   impl_->wait_for_cleanup();
     474                 :        1459 :   std::lock_guard<std::mutex> join_lock(impl_->join_mtx_);
     475                 :        1459 :   impl_->join_ioc_thread(false);
     476                 :        1459 : }
     477                 :             : 
     478                 :        3672 : bool TcpClient::is_connected() const { return get_impl()->connected_.load(); }
     479                 :        1989 : bool TcpClient::is_backpressure_active() const { return get_impl()->backpressure_active_.load(); }
     480                 :        4122 : std::optional<size_t> TcpClient::write_queue_limit() const { return impl_->bp_limit_; }
     481                 :         852 : wrapper::RuntimeStats TcpClient::stats() const {
     482                 :        3408 :   auto result = impl_->stats_.snapshot(impl_->queue_bytes_.load(std::memory_order_relaxed),
     483                 :         852 :                                        impl_->pending_bytes_.load(std::memory_order_relaxed),
     484                 :         852 :                                        impl_->backpressure_active_.load(std::memory_order_relaxed));
     485                 :         852 :   result.send_accounting = impl_->send_accounting_.snapshot();
     486                 :         852 :   return result;
     487                 :             : }
     488                 :          15 : void TcpClient::reset_stats() {
     489                 :          15 :   std::lock_guard<std::mutex> lock(impl_->submission_mtx_);
     490                 :          15 :   impl_->send_accounting_.reset();
     491                 :          15 :   impl_->stats_.reset(impl_->queue_bytes_.load(std::memory_order_relaxed) +
     492                 :          30 :                       impl_->pending_bytes_.load(std::memory_order_relaxed));
     493                 :          15 : }
     494                 :             : 
     495                 :           4 : void TcpClient::fail_receive() {
     496                 :           4 :   const auto connection = write_connection();
     497         [ -  + ]:           4 :   if (!connection) return;
     498                 :           4 :   const auto run = impl_->current_seq_.load();
     499                 :           4 :   net::dispatch(impl_->strand_, [self = shared_from_this(), connection, run] {
     500   [ +  -  +  -  :           8 :     if (run != self->impl_->current_seq_.load() || self->write_connection() != connection) return;
             -  +  -  + ]
     501                 :           4 :     self->impl_->handle_close(self, run, make_error_code(boost::system::errc::no_buffer_space));
     502                 :             :   });
     503                 :             : }
     504                 :             : 
     505                 :       10792 : boost::asio::any_io_executor TcpClient::get_executor() { return impl_->strand_; }
     506                 :             : 
     507                 :          43 : std::shared_ptr<detail::TcpWriteWait> TcpClient::capture_write_wait() const {
     508                 :          43 :   std::lock_guard<std::mutex> lock(impl_->submission_mtx_);
     509   [ +  -  -  +  :          43 :   if (impl_->stop_requested_.load() || !impl_->connected_.load()) return {};
                   -  + ]
     510                 :          43 :   return impl_->write_wait_;
     511                 :          43 : }
     512                 :             : 
     513                 :          36 : std::optional<wrapper::SendResult> TcpClient::poll_write_wait(const std::shared_ptr<detail::TcpWriteWait>& wait) const {
     514                 :          36 :   std::lock_guard<std::mutex> lock(impl_->submission_mtx_);
     515         [ -  + ]:          36 :   if (!wait) return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     516         [ +  + ]:          36 :   if (wait->ended_by) return wrapper::SendResult::reject(*wait->ended_by);
     517   [ +  -  -  +  :          24 :   if (wait->sequence != impl_->connection_seq_.load() || !impl_->connected_.load())
                   -  + ]
     518                 :           0 :     return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     519         [ +  - ]:          12 :   if (!impl_->backpressure_active_.load()) return wrapper::SendResult::accept();
     520                 :           0 :   return std::nullopt;
     521                 :          36 : }
     522                 :             : 
     523                 :         451 : void TcpClient::cancel_write_waits() {
     524                 :         451 :   std::lock_guard<std::mutex> lock(impl_->submission_mtx_);
     525         [ +  + ]:         451 :   if (impl_->write_wait_) impl_->write_wait_->end(wrapper::SendRejection::CancelledWhileWaiting);
     526                 :         451 : }
     527                 :             : 
     528                 :        1888 : std::optional<uint64_t> TcpClient::write_connection() const {
     529                 :        1888 :   std::lock_guard<std::mutex> lock(impl_->submission_mtx_);
     530   [ +  -  -  +  :        1888 :   if (impl_->stop_requested_.load() || !impl_->connected_.load()) return std::nullopt;
                   -  + ]
     531                 :        3776 :   return impl_->connection_seq_.load();
     532                 :        1888 : }
     533                 :             : 
     534                 :        1873 : wrapper::SendResult TcpClient::write_state() {
     535                 :        1873 :   std::lock_guard<std::mutex> lock(impl_->submission_mtx_);
     536   [ +  -  +  + ]:        1873 :   if (auto reason = impl_->admission_rejection()) return wrapper::SendResult::reject(*reason);
     537                 :        1827 :   return wrapper::SendResult::accept();
     538                 :        1873 : }
     539                 :             : 
     540                 :         152 : wrapper::SendResult TcpClient::async_write_copy_result(memory::ConstByteSpan data) {
     541                 :         152 :   const auto result = write_copy(data, std::nullopt);
     542   [ +  +  +  - ]:         152 :   if (auto hook = detail::g_tcp_write_result_hook.load()) hook(result);
     543                 :         152 :   return result;
     544                 :             : }
     545                 :             : 
     546                 :        1422 : wrapper::SendResult TcpClient::write_copy(memory::ConstByteSpan data, std::optional<uint64_t> expected_connection) {
     547         [ +  + ]:        1422 :   if (expected_connection) {
     548   [ +  +  +  - ]:        1270 :     if (auto hook = detail::g_tcp_pinned_write_hook.load()) hook();
     549                 :             :   }
     550                 :        1422 :   std::lock_guard<std::mutex> submission_lock(impl_->submission_mtx_);
     551                 :        1422 :   const auto seq = impl_->current_seq_.load();
     552                 :        1422 :   const auto connection = impl_->connection_seq_.load();
     553   [ +  -  +  + ]:        1422 :   if (auto reason = impl_->admission_rejection(expected_connection)) {
     554                 :          49 :     impl_->stats_.record_failed_send();
     555                 :          49 :     return wrapper::SendResult::reject(*reason);
     556                 :             :   }
     557   [ +  +  +  - ]:        1373 :   if (auto hook = detail::g_tcp_write_admission_hook.load()) hook();
     558                 :             : 
     559                 :        1373 :   size_t size = data.size();
     560         [ +  + ]:        1373 :   if (size == 0) {
     561                 :           7 :     WIRESTEAD_LOG_WARNING("tcp_client", "async_write_copy", "Ignoring zero-length write");
     562                 :           7 :     impl_->stats_.record_failed_send();
     563                 :           7 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     564                 :             :   }
     565                 :             : 
     566         [ +  + ]:        1366 :   if (size > base::constants::MAX_BUFFER_SIZE) {
     567                 :           5 :     WIRESTEAD_LOG_ERROR("tcp_client", "async_write_copy",
     568                 :             :                         fmt::format("Write size exceeds maximum allowed ({} bytes)", size));
     569                 :           5 :     impl_->stats_.record_failed_send();
     570                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     571                 :             :   }
     572                 :             : 
     573   [ +  +  +  +  :        1361 :   if (size <= 65536 && impl_->cfg_.enable_memory_pool) {
                   +  + ]
     574                 :             :     try {
     575                 :        1319 :       memory::PooledBuffer pooled_buffer(size, impl_->pool_);
     576   [ +  -  +  - ]:        1319 :       if (pooled_buffer.valid()) {
     577                 :        1319 :         base::safe_memory::safe_memcpy(pooled_buffer.data(), data.data(), size);
     578                 :        1319 :         const auto added = pooled_buffer.size();
     579                 :             : 
     580   [ +  -  +  + ]:        1319 :         if (!queue_util::try_reserve_limit_bytes(impl_->write_reserve_mtx_, impl_->queue_bytes_, impl_->pending_bytes_,
     581                 :        1319 :                                                  impl_->inflight_bytes_, added, impl_->bp_limit_)) {
     582                 :           3 :           impl_->stats_.record_failed_send();
     583                 :           3 :           return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     584                 :             :         }
     585                 :        1316 :         impl_->stats_.record_accepted(added);
     586                 :        1316 :         Impl::Ledger::Admission admission(impl_->send_accounting_, added);
     587                 :        1316 :         const auto request = admission.request();
     588                 :        1316 :         impl_->write_executor_.execute([self = shared_from_this(), buf = std::move(pooled_buffer), added, seq,
     589                 :             :                                         connection, request]() mutable {
     590         [ -  + ]:        2632 :           if (seq != self->impl_->current_seq_.load()) return;
     591         [ +  + ]:        2632 :           if (connection != self->impl_->connection_seq_.load()) {
     592                 :           1 :             self->impl_->stats_.record_dropped(1, added);
     593                 :           1 :             queue_util::release_reserved_limit_bytes(self->impl_->write_reserve_mtx_, self->impl_->inflight_bytes_,
     594                 :             :                                                      added);
     595                 :           1 :             return;
     596                 :             :           }
     597                 :        1315 :           self->impl_->route_enqueued_buffer(self, Impl::TrackedBuffer{BufferVariant{std::move(buf)}, request}, added);
     598                 :             :         });
     599                 :        1316 :         admission.commit();
     600                 :        1316 :         return wrapper::SendResult::accept();
     601                 :        1316 :       }
     602                 :        1319 :     } catch (const std::exception& e) {
     603                 :           0 :       WIRESTEAD_LOG_ERROR("tcp_client", "async_write_copy",
     604                 :             :                           fmt::format("Failed to acquire pooled buffer: {}", e.what()));
     605                 :           0 :     }
     606                 :             :   }
     607                 :             : 
     608                 :          42 :   std::vector<uint8_t> fallback(data.begin(), data.end());
     609                 :          42 :   const auto added = fallback.size();
     610                 :             : 
     611   [ +  -  +  + ]:          42 :   if (!queue_util::try_reserve_limit_bytes(impl_->write_reserve_mtx_, impl_->queue_bytes_, impl_->pending_bytes_,
     612                 :          42 :                                            impl_->inflight_bytes_, added, impl_->bp_limit_)) {
     613                 :           2 :     impl_->stats_.record_failed_send();
     614                 :           2 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     615                 :             :   }
     616                 :          40 :   impl_->stats_.record_accepted(added);
     617                 :          40 :   Impl::Ledger::Admission admission(impl_->send_accounting_, added);
     618                 :          40 :   const auto request = admission.request();
     619                 :             : 
     620                 :          40 :   impl_->write_executor_.execute([self = shared_from_this(), buf = std::move(fallback), added, seq, connection,
     621                 :             :                                   request]() mutable {
     622         [ -  + ]:          80 :     if (seq != self->impl_->current_seq_.load()) return;
     623         [ +  + ]:          80 :     if (connection != self->impl_->connection_seq_.load()) {
     624                 :           1 :       self->impl_->stats_.record_dropped(1, added);
     625                 :           1 :       queue_util::release_reserved_limit_bytes(self->impl_->write_reserve_mtx_, self->impl_->inflight_bytes_, added);
     626                 :           1 :       return;
     627                 :             :     }
     628                 :          39 :     self->impl_->route_enqueued_buffer(self, Impl::TrackedBuffer{BufferVariant{std::move(buf)}, request}, added);
     629                 :             :   });
     630                 :          40 :   admission.commit();
     631                 :          40 :   return wrapper::SendResult::accept();
     632                 :        1422 : }
     633                 :             : 
     634                 :         197 : wrapper::SendResult TcpClient::async_write_move_result(std::vector<uint8_t>&& data) {
     635                 :         197 :   const auto result = write_move(std::move(data), std::nullopt);
     636   [ +  +  +  - ]:         197 :   if (auto hook = detail::g_tcp_write_result_hook.load()) hook(result);
     637                 :         197 :   return result;
     638                 :             : }
     639                 :             : 
     640                 :         725 : wrapper::SendResult TcpClient::write_move(std::vector<uint8_t>&& data, std::optional<uint64_t> expected_connection) {
     641         [ +  + ]:         725 :   if (expected_connection) {
     642   [ +  +  +  - ]:         528 :     if (auto hook = detail::g_tcp_pinned_write_hook.load()) hook();
     643                 :             :   }
     644                 :         725 :   std::lock_guard<std::mutex> submission_lock(impl_->submission_mtx_);
     645                 :         725 :   const auto seq = impl_->current_seq_.load();
     646                 :         725 :   const auto connection = impl_->connection_seq_.load();
     647   [ +  -  +  + ]:         725 :   if (auto reason = impl_->admission_rejection(expected_connection)) {
     648                 :          47 :     impl_->stats_.record_failed_send();
     649                 :          47 :     return wrapper::SendResult::reject(*reason);
     650                 :             :   }
     651   [ +  +  +  - ]:         678 :   if (auto hook = detail::g_tcp_write_admission_hook.load()) hook();
     652                 :         678 :   const auto size = data.size();
     653         [ +  + ]:         678 :   if (size == 0) {
     654                 :           6 :     WIRESTEAD_LOG_WARNING("tcp_client", "async_write_move", "Ignoring zero-length write");
     655                 :           6 :     impl_->stats_.record_failed_send();
     656                 :           6 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     657                 :             :   }
     658         [ +  + ]:         672 :   if (size > base::constants::MAX_BUFFER_SIZE) {
     659                 :           5 :     WIRESTEAD_LOG_ERROR("tcp_client", "async_write_move",
     660                 :             :                         fmt::format("Write size exceeds maximum allowed ({} bytes)", size));
     661                 :           5 :     impl_->stats_.record_failed_send();
     662                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     663                 :             :   }
     664                 :             : 
     665                 :         667 :   const auto added = size;
     666                 :             : 
     667   [ +  -  +  + ]:         667 :   if (!queue_util::try_reserve_limit_bytes(impl_->write_reserve_mtx_, impl_->queue_bytes_, impl_->pending_bytes_,
     668                 :         667 :                                            impl_->inflight_bytes_, added, impl_->bp_limit_)) {
     669                 :          17 :     impl_->stats_.record_failed_send();
     670                 :          17 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     671                 :             :   }
     672                 :         650 :   impl_->stats_.record_accepted(added);
     673                 :         650 :   Impl::Ledger::Admission admission(impl_->send_accounting_, added);
     674                 :         650 :   const auto request = admission.request();
     675                 :         650 :   impl_->write_executor_.execute([self = shared_from_this(), buf = std::move(data), added, seq, connection,
     676                 :             :                                   request]() mutable {
     677         [ -  + ]:        1300 :     if (seq != self->impl_->current_seq_.load()) return;
     678         [ +  + ]:        1300 :     if (connection != self->impl_->connection_seq_.load()) {
     679                 :           1 :       self->impl_->stats_.record_dropped(1, added);
     680                 :           1 :       queue_util::release_reserved_limit_bytes(self->impl_->write_reserve_mtx_, self->impl_->inflight_bytes_, added);
     681                 :           1 :       return;
     682                 :             :     }
     683                 :         649 :     self->impl_->route_enqueued_buffer(self, Impl::TrackedBuffer{BufferVariant{std::move(buf)}, request}, added);
     684                 :             :   });
     685                 :         650 :   admission.commit();
     686                 :         650 :   return wrapper::SendResult::accept();
     687                 :         725 : }
     688                 :             : 
     689                 :         102 : wrapper::SendResult TcpClient::async_write_shared_result(std::shared_ptr<const std::vector<uint8_t>> data) {
     690                 :         102 :   const auto result = write_shared(std::move(data), std::nullopt);
     691   [ +  +  +  - ]:         102 :   if (auto hook = detail::g_tcp_write_result_hook.load()) hook(result);
     692                 :         102 :   return result;
     693                 :             : }
     694                 :             : 
     695                 :         106 : wrapper::SendResult TcpClient::write_shared(std::shared_ptr<const std::vector<uint8_t>> data,
     696                 :             :                                             std::optional<uint64_t> expected_connection) {
     697         [ +  + ]:         106 :   if (expected_connection) {
     698   [ +  +  +  - ]:           4 :     if (auto hook = detail::g_tcp_pinned_write_hook.load()) hook();
     699                 :             :   }
     700                 :         106 :   std::lock_guard<std::mutex> submission_lock(impl_->submission_mtx_);
     701                 :         106 :   const auto seq = impl_->current_seq_.load();
     702                 :         106 :   const auto connection = impl_->connection_seq_.load();
     703   [ +  -  +  + ]:         106 :   if (auto reason = impl_->admission_rejection(expected_connection)) {
     704                 :          41 :     impl_->stats_.record_failed_send();
     705                 :          41 :     return wrapper::SendResult::reject(*reason);
     706                 :             :   }
     707   [ +  +  +  - ]:          65 :   if (auto hook = detail::g_tcp_write_admission_hook.load()) hook();
     708   [ +  +  +  +  :          65 :   if (!data || data->empty()) {
                   +  + ]
     709                 :           7 :     WIRESTEAD_LOG_WARNING("tcp_client", "async_write_shared", "Ignoring empty shared buffer");
     710                 :           7 :     impl_->stats_.record_failed_send();
     711                 :           7 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     712                 :             :   }
     713                 :          58 :   const auto size = data->size();
     714         [ +  + ]:          58 :   if (size > base::constants::MAX_BUFFER_SIZE) {
     715                 :           5 :     WIRESTEAD_LOG_ERROR("tcp_client", "async_write_shared",
     716                 :             :                         fmt::format("Write size exceeds maximum allowed ({} bytes)", size));
     717                 :           5 :     impl_->stats_.record_failed_send();
     718                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     719                 :             :   }
     720                 :             : 
     721                 :          53 :   const auto added = size;
     722                 :             : 
     723   [ +  -  +  + ]:          53 :   if (!queue_util::try_reserve_limit_bytes(impl_->write_reserve_mtx_, impl_->queue_bytes_, impl_->pending_bytes_,
     724                 :          53 :                                            impl_->inflight_bytes_, added, impl_->bp_limit_)) {
     725                 :           5 :     impl_->stats_.record_failed_send();
     726                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     727                 :             :   }
     728                 :          48 :   impl_->stats_.record_accepted(added);
     729                 :          48 :   Impl::Ledger::Admission admission(impl_->send_accounting_, added);
     730                 :          48 :   const auto request = admission.request();
     731                 :          48 :   impl_->write_executor_.execute([self = shared_from_this(), buf = std::move(data), added, seq, connection,
     732                 :             :                                   request]() mutable {
     733         [ -  + ]:          96 :     if (seq != self->impl_->current_seq_.load()) return;
     734         [ +  + ]:          96 :     if (connection != self->impl_->connection_seq_.load()) {
     735                 :           1 :       self->impl_->stats_.record_dropped(1, added);
     736                 :           1 :       queue_util::release_reserved_limit_bytes(self->impl_->write_reserve_mtx_, self->impl_->inflight_bytes_, added);
     737                 :           1 :       return;
     738                 :             :     }
     739                 :          47 :     self->impl_->route_enqueued_buffer(self, Impl::TrackedBuffer{BufferVariant{std::move(buf)}, request}, added);
     740                 :             :   });
     741                 :          48 :   admission.commit();
     742                 :          48 :   return wrapper::SendResult::accept();
     743                 :         106 : }
     744                 :             : 
     745                 :          97 : wrapper::SendResult TcpClient::async_try_write_copy_result(memory::ConstByteSpan data) {
     746                 :          97 :   const auto result = try_write_copy(data);
     747   [ +  +  +  - ]:          97 :   if (auto hook = detail::g_tcp_write_result_hook.load()) hook(result);
     748                 :          97 :   return result;
     749                 :             : }
     750                 :             : 
     751                 :        2134 : wrapper::SendResult TcpClient::try_write_copy(memory::ConstByteSpan data) {
     752         [ +  + ]:        2134 :   if (data.empty()) {
     753                 :           5 :     impl_->stats_.record_failed_send();
     754                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     755                 :             :   }
     756         [ +  + ]:        2129 :   if (data.size() > base::constants::MAX_BUFFER_SIZE) {
     757                 :           4 :     impl_->stats_.record_failed_send();
     758                 :           4 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     759                 :             :   }
     760                 :        4250 :   return try_write_move(std::vector<uint8_t>(data.begin(), data.end()));
     761                 :             : }
     762                 :             : 
     763                 :         108 : wrapper::SendResult TcpClient::async_try_write_move_result(std::vector<uint8_t>&& data) {
     764                 :         108 :   const auto result = try_write_move(std::move(data));
     765   [ +  +  +  - ]:         108 :   if (auto hook = detail::g_tcp_write_result_hook.load()) hook(result);
     766                 :         108 :   return result;
     767                 :             : }
     768                 :             : 
     769                 :        2245 : wrapper::SendResult TcpClient::try_write_move(std::vector<uint8_t>&& data) {
     770                 :        2245 :   std::lock_guard<std::mutex> submission_lock(impl_->submission_mtx_);
     771                 :        2245 :   const auto seq = impl_->current_seq_.load();
     772                 :        2245 :   const auto connection = impl_->connection_seq_.load();
     773   [ +  -  +  + ]:        2245 :   if (auto reason = impl_->admission_rejection()) {
     774                 :          96 :     impl_->stats_.record_failed_send();
     775                 :          96 :     return wrapper::SendResult::reject(*reason);
     776                 :             :   }
     777   [ +  +  +  - ]:        2149 :   if (auto hook = detail::g_tcp_write_admission_hook.load()) hook();
     778                 :        2149 :   const auto added = data.size();
     779   [ +  +  +  + ]:        2149 :   if (added == 0 || added > base::constants::MAX_BUFFER_SIZE) {
     780                 :           9 :     impl_->stats_.record_failed_send();
     781         [ +  + ]:           9 :     return wrapper::SendResult::reject(added == 0 ? wrapper::SendRejection::InvalidArgument
     782                 :           9 :                                                   : wrapper::SendRejection::TooLarge);
     783                 :             :   }
     784                 :          27 :   const auto reject_for_pressure = [this, added]() {
     785         [ +  + ]:          27 :     if (impl_->bp_strategy_ == base::constants::BackpressureStrategy::BestEffort) {
     786                 :          18 :       impl_->stats_.record_dropped(1, added);
     787                 :             :     } else {
     788                 :           9 :       impl_->stats_.record_failed_send();
     789                 :             :     }
     790                 :        2167 :   };
     791   [ +  +  +  +  :        4253 :   if (impl_->backpressure_active_.load() || impl_->queue_bytes_ + added > impl_->bp_high_ ||
                   +  + ]
     792         [ -  + ]:        2113 :       impl_->queue_bytes_ + impl_->pending_bytes_ + added > impl_->bp_limit_) {
     793                 :          27 :     reject_for_pressure();
     794                 :          27 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     795                 :             :   }
     796   [ +  -  -  + ]:        2113 :   if (!queue_util::try_reserve_write_bytes(impl_->write_reserve_mtx_, impl_->inflight_bytes_, impl_->queue_bytes_,
     797                 :        2113 :                                            impl_->pending_bytes_, impl_->backpressure_active_, added, impl_->bp_high_,
     798                 :        2113 :                                            impl_->bp_limit_)) {
     799                 :           0 :     reject_for_pressure();
     800                 :           0 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     801                 :             :   }
     802                 :        2113 :   impl_->stats_.record_accepted(added);
     803                 :        2113 :   Impl::Ledger::Admission admission(impl_->send_accounting_, added);
     804                 :        2113 :   const auto request = admission.request();
     805                 :             : 
     806                 :        2113 :   impl_->write_executor_.execute(
     807                 :        2113 :       [self = shared_from_this(), buf = std::move(data), added, seq, connection, request]() mutable {
     808         [ -  + ]:        4226 :         if (seq != self->impl_->current_seq_.load()) return;
     809         [ +  + ]:        4226 :         if (connection != self->impl_->connection_seq_.load()) {
     810                 :           2 :           self->impl_->stats_.record_dropped(1, added);
     811                 :             :           // The old connection drain already removed this queue reservation.
     812                 :           2 :           return;
     813                 :             :         }
     814                 :        2111 :         auto impl = self->impl_.get();
     815   [ +  +  +  -  :        4185 :         if (impl->stop_requested_.load() || impl->state_.is_state(LinkState::Closed) ||
                   -  + ]
     816                 :        4185 :             impl->state_.is_state(LinkState::Error)) {
     817                 :          37 :           queue_util::release_reserved_write_bytes(impl->queue_bytes_, added);
     818                 :          37 :           impl->stats_.record_failed_send();
     819                 :          37 :           return;
     820                 :             :         }
     821                 :             : 
     822                 :        2074 :         impl->tx_.push_back(Impl::TrackedBuffer{BufferVariant{std::move(buf)}, request});
     823                 :        2074 :         impl->observe_queue();
     824                 :        2074 :         impl->report_backpressure(self, impl->queue_bytes_);
     825   [ +  +  +  - ]:        2118 :         if (!impl->writing_) impl->do_write(self, impl->current_seq_.load());
     826                 :             :       });
     827                 :        2113 :   admission.commit();
     828                 :        2113 :   return wrapper::SendResult::accept();
     829                 :        2245 : }
     830                 :             : 
     831                 :          97 : wrapper::SendResult TcpClient::async_try_write_shared_result(std::shared_ptr<const std::vector<uint8_t>> data) {
     832                 :          97 :   const auto result = try_write_shared(std::move(data));
     833   [ +  +  +  - ]:          97 :   if (auto hook = detail::g_tcp_write_result_hook.load()) hook(result);
     834                 :          97 :   return result;
     835                 :             : }
     836                 :             : 
     837                 :         109 : wrapper::SendResult TcpClient::try_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     838                 :         109 :   std::lock_guard<std::mutex> submission_lock(impl_->submission_mtx_);
     839                 :         109 :   const auto seq = impl_->current_seq_.load();
     840                 :         109 :   const auto connection = impl_->connection_seq_.load();
     841   [ +  -  +  +  :         109 :   if (!data || data->empty()) {
                   +  + ]
     842                 :           5 :     impl_->stats_.record_failed_send();
     843                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     844                 :             :   }
     845         [ +  + ]:         104 :   if (data->size() > base::constants::MAX_BUFFER_SIZE) {
     846                 :           4 :     impl_->stats_.record_failed_send();
     847                 :           4 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     848                 :             :   }
     849                 :         100 :   const auto added = data->size();
     850                 :           8 :   const auto reject_for_pressure = [this, added]() {
     851         [ +  + ]:           8 :     if (impl_->bp_strategy_ == base::constants::BackpressureStrategy::BestEffort) {
     852                 :           4 :       impl_->stats_.record_dropped(1, added);
     853                 :             :     } else {
     854                 :           4 :       impl_->stats_.record_failed_send();
     855                 :             :     }
     856                 :         108 :   };
     857   [ +  -  +  + ]:         100 :   if (auto reason = impl_->admission_rejection()) {
     858                 :          45 :     impl_->stats_.record_failed_send();
     859                 :          45 :     return wrapper::SendResult::reject(*reason);
     860                 :             :   }
     861   [ +  +  +  - ]:          55 :   if (auto hook = detail::g_tcp_write_admission_hook.load()) hook();
     862   [ +  -  +  +  :         102 :   if (impl_->backpressure_active_.load() || impl_->queue_bytes_ + added > impl_->bp_high_ ||
                   +  + ]
     863         [ -  + ]:          47 :       impl_->queue_bytes_ + impl_->pending_bytes_ + added > impl_->bp_limit_) {
     864                 :           8 :     reject_for_pressure();
     865                 :           8 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     866                 :             :   }
     867   [ +  -  -  + ]:          47 :   if (!queue_util::try_reserve_write_bytes(impl_->write_reserve_mtx_, impl_->inflight_bytes_, impl_->queue_bytes_,
     868                 :          47 :                                            impl_->pending_bytes_, impl_->backpressure_active_, added, impl_->bp_high_,
     869                 :          47 :                                            impl_->bp_limit_)) {
     870                 :           0 :     reject_for_pressure();
     871                 :           0 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     872                 :             :   }
     873                 :          47 :   impl_->stats_.record_accepted(added);
     874                 :          47 :   Impl::Ledger::Admission admission(impl_->send_accounting_, added);
     875                 :          47 :   const auto request = admission.request();
     876                 :             : 
     877                 :          47 :   impl_->write_executor_.execute(
     878                 :          47 :       [self = shared_from_this(), buf = std::move(data), added, seq, connection, request]() mutable {
     879         [ -  + ]:          94 :         if (seq != self->impl_->current_seq_.load()) return;
     880         [ +  + ]:          94 :         if (connection != self->impl_->connection_seq_.load()) {
     881                 :           1 :           self->impl_->stats_.record_dropped(1, added);
     882                 :             :           // The old connection drain already removed this queue reservation.
     883                 :           1 :           return;
     884                 :             :         }
     885                 :          46 :         auto impl = self->impl_.get();
     886   [ +  +  +  -  :          76 :         if (impl->stop_requested_.load() || impl->state_.is_state(LinkState::Closed) ||
                   -  + ]
     887                 :          76 :             impl->state_.is_state(LinkState::Error)) {
     888                 :          16 :           queue_util::release_reserved_write_bytes(impl->queue_bytes_, added);
     889                 :          16 :           impl->stats_.record_failed_send();
     890                 :          16 :           return;
     891                 :             :         }
     892                 :             : 
     893                 :          30 :         impl->tx_.push_back(Impl::TrackedBuffer{BufferVariant{std::move(buf)}, request});
     894                 :          30 :         impl->observe_queue();
     895                 :          30 :         impl->report_backpressure(self, impl->queue_bytes_);
     896   [ +  +  +  - ]:          45 :         if (!impl->writing_) impl->do_write(self, impl->current_seq_.load());
     897                 :             :       });
     898                 :          47 :   admission.commit();
     899                 :          47 :   return wrapper::SendResult::accept();
     900                 :         109 : }
     901                 :             : 
     902                 :             : // Each setter builds the shared snapshot before taking the lock, so the
     903                 :             : // allocation stays outside the critical section the io thread contends on.
     904                 :         783 : void TcpClient::on_bytes(OnBytes cb) {
     905                 :         783 :   auto shared = interface::share_callback(std::move(cb));
     906                 :         783 :   std::lock_guard<std::mutex> lock(impl_->callback_mtx_);
     907                 :         783 :   impl_->on_bytes_ = std::move(shared);
     908                 :         783 : }
     909                 :         827 : void TcpClient::on_state(OnState cb) {
     910                 :         827 :   auto shared = interface::share_callback(std::move(cb));
     911                 :         827 :   std::lock_guard<std::mutex> lock(impl_->callback_mtx_);
     912                 :         827 :   impl_->on_state_ = std::move(shared);
     913                 :         827 : }
     914                 :         770 : void TcpClient::on_backpressure(OnBackpressure cb) {
     915                 :         770 :   auto shared = interface::share_callback(std::move(cb));
     916                 :         770 :   std::lock_guard<std::mutex> lock(impl_->callback_mtx_);
     917                 :         770 :   impl_->on_bp_ = std::move(shared);
     918                 :         770 : }
     919                 :          25 : void TcpClient::set_backpressure_strategy(base::constants::BackpressureStrategy strategy) {
     920                 :          25 :   config::detail::strategy(strategy);
     921                 :          25 :   impl_->bp_strategy_.store(strategy, std::memory_order_relaxed);
     922                 :          25 : }
     923                 :             : 
     924                 :           3 : void TcpClient::set_retry_interval(unsigned interval_ms) {
     925                 :           3 :   std::lock_guard<std::mutex> lock(impl_->cfg_mtx_);
     926                 :           3 :   auto candidate = impl_->cfg_;
     927                 :           3 :   candidate.retry_interval_ms = interval_ms;
     928                 :           3 :   config::detail::validate(candidate);
     929                 :           2 :   impl_->cfg_.retry_interval_ms = interval_ms;
     930                 :           4 : }
     931                 :           2 : void TcpClient::set_max_retries(int max_retries) {
     932                 :           2 :   std::lock_guard<std::mutex> lock(impl_->cfg_mtx_);
     933                 :           2 :   auto candidate = impl_->cfg_;
     934                 :           2 :   candidate.max_retries = max_retries;
     935                 :           2 :   config::detail::validate(candidate);
     936                 :           2 :   impl_->cfg_.max_retries = max_retries;
     937                 :           2 : }
     938                 :           2 : void TcpClient::set_connection_timeout(unsigned timeout_ms) {
     939                 :           2 :   std::lock_guard<std::mutex> lock(impl_->cfg_mtx_);
     940                 :           2 :   auto candidate = impl_->cfg_;
     941                 :           2 :   candidate.connection_timeout_ms = timeout_ms;
     942                 :           2 :   config::detail::validate(candidate);
     943                 :           2 :   impl_->cfg_.connection_timeout_ms = timeout_ms;
     944                 :           2 : }
     945                 :           1 : void TcpClient::set_idle_timeout(unsigned timeout_ms) {
     946                 :           1 :   std::lock_guard<std::mutex> lock(impl_->cfg_mtx_);
     947                 :           1 :   auto candidate = impl_->cfg_;
     948                 :           1 :   candidate.idle_timeout_ms = timeout_ms;
     949                 :           1 :   config::detail::validate(candidate);
     950                 :           1 :   impl_->cfg_.idle_timeout_ms = timeout_ms;
     951                 :           1 : }
     952                 :           1 : void TcpClient::set_idle_timeout_action(IdleTimeoutAction action) {
     953   [ -  +  -  -  :           1 :   config::detail::require(action == IdleTimeoutAction::Close || action == IdleTimeoutAction::Reconnect,
                   +  - ]
     954                 :             :                           "invalid idle timeout action");
     955                 :           1 :   std::lock_guard<std::mutex> lock(impl_->cfg_mtx_);
     956                 :           1 :   impl_->cfg_.idle_timeout_action = action;
     957                 :           1 : }
     958                 :           8 : void TcpClient::set_reconnect_policy(ReconnectPolicy policy) {
     959                 :           8 :   std::lock_guard<std::mutex> lock(impl_->cfg_mtx_);
     960         [ +  + ]:           8 :   if (policy) {
     961                 :           7 :     impl_->reconnect_policy_ = std::move(policy);
     962                 :             :   } else {
     963                 :           1 :     impl_->reconnect_policy_ = std::nullopt;
     964                 :             :   }
     965                 :           8 : }
     966                 :             : 
     967                 :             : // Impl methods implementation
     968                 :             : 
     969                 :         534 : void TcpClient::Impl::apply_socket_options() {
     970                 :         534 :   boost::system::error_code ec;
     971                 :             : 
     972         [ +  - ]:         534 :   if (cfg_.tcp_no_delay) {
     973                 :         534 :     socket_.set_option(tcp::no_delay(true), ec);
     974         [ -  + ]:         534 :     if (ec) {
     975                 :           0 :       WIRESTEAD_LOG_WARNING("tcp_client", "socket_options", fmt::format("Failed to set TCP_NODELAY: {}", ec.message()));
     976                 :           0 :       ec.clear();
     977                 :             :     }
     978                 :             :   }
     979                 :             : 
     980         [ -  + ]:         534 :   if (cfg_.keep_alive) {
     981                 :           0 :     socket_.set_option(net::socket_base::keep_alive(true), ec);
     982         [ #  # ]:           0 :     if (ec) {
     983                 :           0 :       WIRESTEAD_LOG_WARNING("tcp_client", "socket_options", fmt::format("Failed to set keep_alive: {}", ec.message()));
     984                 :           0 :       ec.clear();
     985                 :             :     }
     986                 :             :   }
     987                 :             : 
     988         [ +  + ]:         534 :   if (cfg_.send_buffer_size > 0) {
     989                 :         192 :     socket_.set_option(net::socket_base::send_buffer_size(static_cast<int>(cfg_.send_buffer_size)), ec);
     990         [ -  + ]:         192 :     if (ec) {
     991                 :           0 :       WIRESTEAD_LOG_WARNING("tcp_client", "socket_options",
     992                 :             :                             fmt::format("Failed to set send buffer size: {}", ec.message()));
     993                 :           0 :       ec.clear();
     994                 :             :     }
     995                 :             :   }
     996                 :             : 
     997         [ -  + ]:         534 :   if (cfg_.receive_buffer_size > 0) {
     998                 :           0 :     socket_.set_option(net::socket_base::receive_buffer_size(static_cast<int>(cfg_.receive_buffer_size)), ec);
     999         [ #  # ]:           0 :     if (ec) {
    1000                 :           0 :       WIRESTEAD_LOG_WARNING("tcp_client", "socket_options",
    1001                 :             :                             fmt::format("Failed to set receive buffer size: {}", ec.message()));
    1002                 :           0 :       ec.clear();
    1003                 :             :     }
    1004                 :             :   }
    1005                 :         534 : }
    1006                 :             : 
    1007                 :         598 : void TcpClient::Impl::do_resolve_connect(std::shared_ptr<TcpClient> self, uint64_t seq) {
    1008                 :         598 :   ++pending_io_;
    1009                 :         598 :   resolver_.async_resolve(
    1010                 :        1196 :       cfg_.host, fmt::format("{}", cfg_.port), [self, seq](auto ec, tcp::resolver::results_type results) {
    1011                 :         598 :         IoCompletion completion{self->impl_.get()};
    1012   [ +  +  -  +  :        1195 :         if (ec == net::error::operation_aborted || seq != self->impl_->current_seq_.load()) {
                   +  + ]
    1013                 :           1 :           return;
    1014                 :             :         }
    1015   [ +  +  -  +  :         597 :         if (self->impl_->stop_requested_.load() || self->impl_->stopping_.load()) {
                   +  + ]
    1016                 :           4 :           return;
    1017                 :             :         }
    1018         [ +  + ]:         593 :         if (ec) {
    1019                 :             :           bool has_policy;
    1020                 :             :           {
    1021                 :           1 :             std::lock_guard<std::mutex> lock(self->impl_->cfg_mtx_);
    1022                 :           1 :             has_policy = self->impl_->reconnect_policy_.has_value();
    1023                 :           1 :           }
    1024                 :           1 :           uint32_t current_attempts =
    1025         [ -  + ]:           1 :               has_policy ? self->impl_->reconnect_attempt_count_ : static_cast<uint32_t>(self->impl_->retry_attempts_);
    1026                 :           2 :           self->impl_->record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "resolve",
    1027                 :             :                                     ec, fmt::format("Resolution failed: {}", ec.message()),
    1028                 :           1 :                                     diagnostics::is_retryable_tcp_connect_error(ec), current_attempts);
    1029                 :           1 :           self->impl_->schedule_retry(self, seq);
    1030                 :           1 :           return;
    1031                 :             :         }
    1032                 :             :         unsigned connection_timeout_ms;
    1033                 :             :         {
    1034                 :         592 :           std::lock_guard<std::mutex> lock(self->impl_->cfg_mtx_);
    1035                 :         592 :           connection_timeout_ms = self->impl_->cfg_.connection_timeout_ms;
    1036                 :         592 :         }
    1037                 :         592 :         self->impl_->connect_timer_.expires_after(std::chrono::milliseconds(connection_timeout_ms));
    1038                 :         592 :         ++self->impl_->pending_io_;
    1039                 :         592 :         self->impl_->connect_timer_.async_wait([self, seq](const boost::system::error_code& timer_ec) {
    1040                 :         592 :           IoCompletion completion{self->impl_.get()};
    1041   [ +  +  -  +  :         594 :           if (timer_ec == net::error::operation_aborted || seq != self->impl_->current_seq_.load()) {
                   +  + ]
    1042                 :         590 :             return;
    1043                 :             :           }
    1044   [ +  -  +  -  :           2 :           if (!timer_ec && !self->impl_->stop_requested_.load() && !self->impl_->stopping_.load()) {
             +  -  +  - ]
    1045                 :             :             bool has_policy;
    1046                 :             :             unsigned timeout_ms;
    1047                 :             :             {
    1048                 :           2 :               std::lock_guard<std::mutex> lock(self->impl_->cfg_mtx_);
    1049                 :           2 :               has_policy = self->impl_->reconnect_policy_.has_value();
    1050                 :           2 :               timeout_ms = self->impl_->cfg_.connection_timeout_ms;
    1051                 :           2 :             }
    1052                 :           2 :             WIRESTEAD_LOG_ERROR("tcp_client", "connect_timeout",
    1053                 :             :                                 fmt::format("Connection timed out after {}ms", timeout_ms));
    1054         [ -  + ]:           2 :             uint32_t current_attempts = has_policy ? self->impl_->reconnect_attempt_count_
    1055                 :           2 :                                                    : static_cast<uint32_t>(self->impl_->retry_attempts_);
    1056                 :           4 :             self->impl_->record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "connect",
    1057                 :             :                                       boost::asio::error::timed_out, "Connection timed out",
    1058                 :           2 :                                       diagnostics::is_retryable_tcp_connect_error(boost::asio::error::timed_out),
    1059                 :             :                                       current_attempts);
    1060                 :           2 :             self->impl_->handle_close(self, seq, boost::asio::error::timed_out);
    1061                 :             :           }
    1062                 :         592 :         });
    1063                 :             : 
    1064                 :         592 :         ++self->impl_->pending_io_;
    1065                 :         592 :         net::async_connect(self->impl_->socket_, results, [self, seq](auto ec2, const auto&) {
    1066                 :         592 :           IoCompletion completion{self->impl_.get()};
    1067   [ +  +  -  +  :        1180 :           if (ec2 == net::error::operation_aborted || seq != self->impl_->current_seq_.load()) {
                   +  + ]
    1068                 :           4 :             return;
    1069                 :             :           }
    1070   [ +  -  -  +  :         588 :           if (self->impl_->stop_requested_.load() || self->impl_->stopping_.load()) {
                   -  + ]
    1071                 :           0 :             self->impl_->close_socket();
    1072                 :           0 :             self->impl_->connect_timer_.cancel();
    1073                 :           0 :             return;
    1074                 :             :           }
    1075         [ +  + ]:         588 :           if (ec2) {
    1076                 :          54 :             self->impl_->connect_timer_.cancel();
    1077                 :             :             bool has_policy;
    1078                 :             :             {
    1079                 :          54 :               std::lock_guard<std::mutex> lock(self->impl_->cfg_mtx_);
    1080                 :          54 :               has_policy = self->impl_->reconnect_policy_.has_value();
    1081                 :          54 :             }
    1082         [ +  + ]:          54 :             uint32_t current_attempts = has_policy ? self->impl_->reconnect_attempt_count_
    1083                 :          27 :                                                    : static_cast<uint32_t>(self->impl_->retry_attempts_);
    1084                 :         108 :             self->impl_->record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "connect",
    1085                 :             :                                       ec2, fmt::format("Connection failed: {}", ec2.message()),
    1086                 :          54 :                                       diagnostics::is_retryable_tcp_connect_error(ec2), current_attempts);
    1087                 :          54 :             self->impl_->schedule_retry(self, seq);
    1088                 :          54 :             return;
    1089                 :             :           }
    1090                 :         534 :           self->impl_->connect_timer_.cancel();
    1091                 :         534 :           self->impl_->retry_attempts_ = 0;
    1092                 :         534 :           self->impl_->reconnect_attempt_count_ = 0;
    1093                 :             : 
    1094                 :             : #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
    1095                 :             :           int yes = 1;
    1096                 :             :           (void)::setsockopt(static_cast<int>(self->impl_->socket_.native_handle()), SOL_SOCKET, SO_NOSIGPIPE, &yes,
    1097                 :             :                              static_cast<socklen_t>(sizeof(yes)));
    1098                 :             : #endif
    1099                 :             : 
    1100                 :         534 :           self->impl_->apply_socket_options();
    1101                 :             : 
    1102                 :             :           // TCP is up; TLS still has to prove who answered. Reading before the
    1103                 :             :           // handshake completes would hand the session ciphertext, and a failed
    1104                 :             :           // handshake must not be reported as a working connection - so the
    1105                 :             :           // rest of the connect path waits behind it. Plaintext runs the
    1106                 :             :           // continuation immediately, which is what it did before TLS existed.
    1107                 :        1039 :           self->impl_->handshake_then(self, seq, [self, seq] { self->impl_->finish_connect(self, seq); });
    1108                 :         592 :         });
    1109                 :         598 :       });
    1110                 :         598 : }
    1111                 :             : 
    1112                 :             : // Sets up the TLS stream if configured, runs the handshake, then hands control
    1113                 :             : // back. Without TLS - or in a build without it - `next` runs straight away and
    1114                 :             : // the connect path is byte for byte what it was.
    1115                 :         534 : void TcpClient::Impl::handshake_then(std::shared_ptr<TcpClient> self, uint64_t seq, std::function<void()> next) {
    1116                 :             : #ifdef WIRESTEAD_TLS_ENABLED
    1117                 :         534 :   bool want_tls = false;
    1118                 :         534 :   std::string ca_file;
    1119                 :             :   {
    1120                 :         534 :     std::lock_guard<std::mutex> lock(cfg_mtx_);
    1121                 :         534 :     want_tls = cfg_.tls_enabled;
    1122                 :         534 :     ca_file = cfg_.tls_ca_file;
    1123                 :         534 :   }
    1124                 :             : 
    1125         [ +  + ]:         534 :   if (want_tls) {
    1126                 :             :     namespace ssl = boost::asio::ssl;
    1127                 :             :     try {
    1128         [ +  + ]:          30 :       if (!ssl_context_) {
    1129                 :           2 :         auto ctx = std::make_shared<ssl::context>(ssl::context::tls_client);
    1130                 :           2 :         ctx->set_options(ssl::context::default_workarounds | ssl::context::no_sslv2 | ssl::context::no_sslv3 |
    1131                 :             :                          ssl::context::no_tlsv1 | ssl::context::no_tlsv1_1);
    1132         [ +  + ]:           2 :         if (ca_file.empty()) {
    1133                 :           1 :           ctx->set_default_verify_paths();
    1134                 :             :         } else {
    1135                 :           1 :           ctx->load_verify_file(ca_file);
    1136                 :             :         }
    1137                 :             :         // Not optional. An unverified TLS connection encrypts traffic to
    1138                 :             :         // whoever answered, which is what an attacker in the middle wants.
    1139                 :           2 :         ctx->set_verify_mode(ssl::verify_peer);
    1140                 :           2 :         ssl_context_ = std::move(ctx);
    1141                 :           2 :       }
    1142                 :          30 :       tls_.emplace(socket_, *ssl_context_);
    1143                 :          30 :       tls_engaged_ = true;
    1144                 :          30 :       tls_->set_verify_callback(ssl::host_name_verification(cfg_.host));
    1145                 :             :       // SNI, and the name the certificate is checked against.
    1146                 :          30 :       ::SSL_set_tlsext_host_name(tls_->native_handle(), cfg_.host.c_str());
    1147                 :           0 :     } catch (const std::exception& e) {
    1148                 :           0 :       const std::string msg = std::string("TLS setup failed: ") + e.what();
    1149                 :           0 :       WIRESTEAD_LOG_ERROR("tcp_client", "handshake", msg);
    1150                 :           0 :       record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "handshake",
    1151                 :             :                    boost::asio::error::invalid_argument, msg, false, 0);
    1152                 :           0 :       tls_.reset();
    1153                 :           0 :       tls_engaged_ = false;
    1154                 :           0 :       handle_close(self, seq, boost::asio::error::invalid_argument);
    1155                 :           0 :       return;
    1156                 :           0 :     }
    1157                 :             : 
    1158                 :          30 :     ++pending_io_;
    1159                 :          60 :     tls_->async_handshake(ssl::stream_base::client,
    1160                 :          60 :                           net::bind_executor(strand_, [this, self, seq, next](const boost::system::error_code& ec) {
    1161                 :          30 :                             IoCompletion completion{this};
    1162   [ +  -  +  -  :          60 :                             if (seq != current_seq_.load() || stop_requested_.load() || stopping_.load()) return;
             -  +  -  + ]
    1163         [ +  + ]:          30 :                             if (ec) {
    1164                 :          29 :                               WIRESTEAD_LOG_ERROR("tcp_client", "handshake", "TLS handshake failed: " + ec.message());
    1165                 :          29 :                               record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION,
    1166                 :          58 :                                            "handshake", ec, "TLS handshake failed: " + ec.message(), false, 0);
    1167                 :             :                               // Not tls_.reset(): this runs from inside the
    1168                 :             :                               // stream's own completion handler, and
    1169                 :             :                               // close_socket() below explains why the stream
    1170                 :             :                               // has to outlive its operations.
    1171                 :          29 :                               tls_engaged_ = false;
    1172                 :          29 :                               handle_close(self, seq, ec);
    1173                 :          29 :                               return;
    1174                 :             :                             }
    1175                 :           1 :                             next();
    1176                 :          30 :                           }));
    1177                 :          30 :     return;
    1178                 :             :   }
    1179                 :             : #else
    1180                 :             :   (void)self;
    1181                 :             :   (void)seq;
    1182                 :             : #endif
    1183                 :         504 :   next();
    1184                 :         534 : }
    1185                 :             : 
    1186                 :         505 : void TcpClient::Impl::finish_connect(std::shared_ptr<TcpClient> self, uint64_t seq) {
    1187                 :             :   // Set here rather than at TCP connect: with TLS, a socket whose handshake
    1188                 :             :   // has not finished is not a usable connection, and connected() is what
    1189                 :             :   // callers poll before sending. Reporting true for a peer that failed
    1190                 :             :   // verification would be worse than useless.
    1191                 :             :   {
    1192                 :         505 :     std::lock_guard<std::mutex> lock(submission_mtx_);
    1193   [ +  -  +  -  :        1010 :     if (seq != current_seq_.load() || stop_requested_.load() || stopping_.load()) return;
             -  +  -  + ]
    1194                 :        1010 :     write_wait_ = std::make_shared<detail::TcpWriteWait>(connection_seq_.load());
    1195                 :         505 :     connected_.store(true);
    1196                 :         505 :   }
    1197                 :         505 :   rx_ = std::make_shared<std::vector<uint8_t>>(cfg_.read_buffer_size);
    1198                 :         505 :   const auto connection = connection_seq_.load();
    1199                 :         505 :   transition_to(LinkState::Connected);
    1200                 :         505 :   boost::system::error_code ep_ec;
    1201                 :         505 :   auto rep = socket_.remote_endpoint(ep_ec);
    1202         [ +  - ]:         505 :   if (!ep_ec) {
    1203                 :         505 :     WIRESTEAD_LOG_INFO("tcp_client", "connect",
    1204                 :             :                        fmt::format("Connected to {}:{}", rep.address().to_string(), rep.port()));
    1205                 :             :   }
    1206                 :         505 :   start_read(self, seq);
    1207                 :         505 :   reset_idle_timer(self, seq);
    1208                 :         505 :   net::post(strand_, [self, seq, connection]() {
    1209   [ +  -  +  +  :        1515 :     if (seq != self->impl_->current_seq_.load() || connection != self->impl_->connection_seq_.load()) return;
                   +  + ]
    1210   [ +  +  +  - ]:         496 :     if (!self->impl_->writing_) self->impl_->do_write(self, seq);
    1211                 :             :   });
    1212                 :             : }
    1213                 :             : 
    1214                 :         169 : void TcpClient::Impl::schedule_retry(std::shared_ptr<TcpClient> self, uint64_t seq) {
    1215                 :         169 :   mark_disconnected();
    1216   [ +  -  -  +  :         169 :   if (stop_requested_.load() || stopping_.load()) {
                   -  + ]
    1217                 :          31 :     return;
    1218                 :             :   }
    1219                 :             : 
    1220                 :             :   // Prevent double scheduling of reconnect
    1221         [ -  + ]:         169 :   if (reconnect_pending_.exchange(true)) {
    1222                 :           0 :     return;
    1223                 :             :   }
    1224                 :             : 
    1225                 :         169 :   std::optional<diagnostics::ErrorInfo> last_err = error_info_holder_.last_error_info();
    1226                 :             : 
    1227         [ -  + ]:         169 :   if (!last_err) {
    1228                 :           0 :     last_err = diagnostics::ErrorInfo(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION,
    1229                 :             :                                       "tcp_client", "schedule_retry", "Unknown error",
    1230                 :           0 :                                       make_error_code(boost::asio::error::not_connected), true);
    1231                 :             :   }
    1232                 :             : 
    1233                 :             :   // Snapshot once rather than locking repeatedly for each read below -
    1234                 :             :   // cfg_/reconnect_policy_ can change concurrently via set_retry_interval()
    1235                 :             :   // etc. from any user thread while this runs on the strand (#436).
    1236                 :         169 :   TcpClientConfig cfg_snapshot;
    1237                 :         169 :   std::optional<ReconnectPolicy> reconnect_policy_snapshot;
    1238                 :             :   {
    1239                 :         169 :     std::lock_guard<std::mutex> lock(cfg_mtx_);
    1240                 :         169 :     cfg_snapshot = cfg_;
    1241                 :         169 :     reconnect_policy_snapshot = reconnect_policy_;
    1242                 :         169 :   }
    1243                 :             : 
    1244                 :             :   // Determine current attempt count based on active mode
    1245                 :             :   uint32_t current_attempts =
    1246         [ +  + ]:         169 :       reconnect_policy_snapshot ? reconnect_attempt_count_ : static_cast<uint32_t>(retry_attempts_);
    1247                 :             : 
    1248                 :         169 :   auto decision = detail::decide_reconnect(cfg_snapshot, *last_err, current_attempts, reconnect_policy_snapshot);
    1249                 :             : 
    1250         [ +  + ]:         169 :   if (!decision.should_retry) {
    1251                 :          31 :     WIRESTEAD_LOG_INFO("tcp_client", "retry", "Reconnect stopped by policy/config");
    1252                 :          31 :     transition_to(LinkState::Error);
    1253                 :          31 :     reconnect_pending_.store(false);
    1254                 :          31 :     return;
    1255                 :             :   }
    1256                 :             : 
    1257                 :             :   // The decider returns a base delay for both policy and fallback paths.
    1258                 :         138 :   std::chrono::milliseconds delay = decision.delay.value_or(std::chrono::milliseconds(cfg_snapshot.retry_interval_ms));
    1259         [ +  + ]:         138 :   if (reconnect_policy_snapshot) {
    1260                 :          25 :     reconnect_attempt_count_++;
    1261                 :             :   } else {
    1262                 :             :     // Preserve existing "fast first retry" behavior for non-policy mode.
    1263                 :         113 :     ++retry_attempts_;
    1264         [ +  + ]:         113 :     if (retry_attempts_ == 1) {
    1265                 :         103 :       delay = std::chrono::milliseconds(first_retry_interval_ms_);
    1266                 :             :     }
    1267                 :             :   }
    1268                 :             : 
    1269                 :         138 :   transition_to(LinkState::Connecting);
    1270                 :             : 
    1271                 :         138 :   WIRESTEAD_LOG_INFO("tcp_client", "retry",
    1272                 :             :                      fmt::format("Scheduling retry in {:.3f}s", static_cast<double>(delay.count()) / 1000.0));
    1273                 :             : 
    1274                 :         138 :   retry_timer_.expires_after(delay);
    1275                 :         138 :   ++pending_io_;
    1276                 :         138 :   retry_timer_.async_wait([self, seq](const boost::system::error_code& ec) {
    1277                 :         138 :     IoCompletion completion{self->impl_.get()};
    1278                 :             :     // Clear pending flag regardless of result (fired or aborted)
    1279                 :         138 :     self->impl_->reconnect_pending_.store(false);
    1280                 :             : 
    1281   [ +  +  -  +  :         241 :     if (ec == net::error::operation_aborted || seq != self->impl_->current_seq_.load()) {
                   +  + ]
    1282                 :          35 :       return;
    1283                 :             :     }
    1284   [ +  -  +  -  :         103 :     if (!ec && !self->impl_->stop_requested_.load() && !self->impl_->stopping_.load())
             +  -  +  - ]
    1285                 :         103 :       self->impl_->do_resolve_connect(self, seq);
    1286                 :         138 :   });
    1287                 :         231 : }
    1288                 :             : 
    1289                 :         722 : void TcpClient::Impl::start_read(std::shared_ptr<TcpClient> self, uint64_t seq) {
    1290                 :         722 :   const auto connection = connection_seq_.load();
    1291                 :         722 :   auto buffer = rx_;
    1292                 :         722 :   ++pending_io_;
    1293                 :         722 :   auto on_read = [self, seq, connection, buffer](auto ec, std::size_t n) {
    1294                 :         722 :     IoCompletion completion{self->impl_.get()};
    1295   [ +  +  +  -  :        1324 :     if (ec == net::error::operation_aborted || seq != self->impl_->current_seq_.load() ||
                   +  + ]
    1296         [ +  + ]:         602 :         connection != self->impl_->connection_seq_.load()) {
    1297                 :         451 :       return;
    1298                 :             :     }
    1299         [ +  + ]:         271 :     if (self->impl_->stop_requested_.load()) {
    1300                 :           9 :       return;
    1301                 :             :     }
    1302         [ +  + ]:         262 :     if (ec) {
    1303                 :          45 :       self->impl_->handle_close(self, seq, ec);
    1304                 :          45 :       return;
    1305                 :             :     }
    1306         [ +  - ]:         217 :     if (n > 0) {
    1307                 :         217 :       self->impl_->reset_idle_timer(self, seq);
    1308                 :             :     }
    1309                 :         217 :     interface::SharedCallback<OnBytes> on_bytes;
    1310                 :             :     {
    1311                 :         217 :       std::lock_guard<std::mutex> lock(self->impl_->callback_mtx_);
    1312                 :         217 :       on_bytes = self->impl_->on_bytes_;
    1313                 :         217 :     }
    1314                 :             : 
    1315                 :         217 :     self->impl_->stats_.record_received(n);
    1316                 :             : 
    1317                 :         217 :     diagnostics::invoke_callback("tcp_client", "on_bytes", on_bytes, memory::ConstByteSpan(buffer->data(), n));
    1318                 :         217 :     self->impl_->start_read(self, seq);
    1319                 :        1444 :   };
    1320                 :             : #ifdef WIRESTEAD_TLS_ENABLED
    1321         [ +  + ]:         722 :   if (tls_active()) {
    1322                 :           2 :     tls_->async_read_some(net::buffer(buffer->data(), buffer->size()), std::move(on_read));
    1323                 :           2 :     return;
    1324                 :             :   }
    1325                 :             : #endif
    1326                 :         720 :   socket_.async_read_some(net::buffer(buffer->data(), buffer->size()), std::move(on_read));
    1327                 :         724 : }
    1328                 :             : 
    1329                 :        3333 : void TcpClient::Impl::do_write(std::shared_ptr<TcpClient> self, uint64_t seq) {
    1330         [ +  + ]:        3333 :   if (stop_requested_.load()) {
    1331                 :          24 :     tx_.clear();
    1332                 :          24 :     queue_bytes_ = 0;
    1333                 :          24 :     pending_.clear();
    1334                 :          24 :     pending_bytes_ = 0;
    1335                 :          24 :     writing_ = false;
    1336                 :          24 :     report_backpressure(self, queue_bytes_);
    1337                 :        1742 :     return;
    1338                 :             :   }
    1339                 :             : 
    1340         [ -  + ]:        3309 :   if (!connected_.load()) {
    1341                 :           0 :     writing_ = false;
    1342                 :           0 :     return;
    1343                 :             :   }
    1344                 :             : 
    1345   [ +  +  +  -  :        3309 :   if (tx_.empty() || state_.is_state(LinkState::Closed) || state_.is_state(LinkState::Error)) {
             -  +  +  + ]
    1346                 :        1709 :     writing_ = false;
    1347                 :        1709 :     return;
    1348                 :             :   }
    1349                 :        1600 :   writing_ = true;
    1350                 :             : 
    1351                 :             :   // The ledger, not the admission mutex, fences this handoff against stop and
    1352                 :             :   // loss: taking the admission mutex here per batch contends with every producer.
    1353                 :        1600 :   auto batch = std::make_shared<WriteBatch>();
    1354                 :        1600 :   const auto queued_bytes = queue_util::take_gather_batch(tx_, batch->buffers, batch->views, BufferProjection{});
    1355                 :        1600 :   batch->bytes = queued_bytes;
    1356   [ +  +  +  + ]:        3198 :   if (stop_requested_.load() ||
    1357   [ +  -  -  + ]:        9722 :       !send_accounting_.begin_batch(batch->buffers, [](const auto& item) { return item.request; })) {
    1358                 :             :     // Stop/loss already terminated these; leave them to its queue cleanup.
    1359                 :           2 :     queue_util::return_gather_batch(tx_, batch->buffers);
    1360                 :           2 :     writing_ = false;
    1361                 :           2 :     return;
    1362                 :             :   }
    1363                 :        1598 :   active_write_ = batch;
    1364                 :        1598 :   const auto connection = connection_seq_.load();
    1365                 :             : 
    1366                 :        1598 :   ++pending_io_;
    1367                 :        1591 :   auto on_write = [self, queued_bytes, seq, connection, batch](auto ec, std::size_t bytes_written) {
    1368                 :        1591 :     IoCompletion completion{self->impl_.get()};
    1369                 :             :     bool current_connection;
    1370                 :             :     {
    1371                 :             :       // Only a failure must close admission; success needs just the ledger.
    1372                 :        1591 :       std::unique_lock<std::mutex> admission_lock(self->impl_->submission_mtx_, std::defer_lock);
    1373   [ +  +  +  - ]:        1591 :       if (ec) admission_lock.lock();
    1374                 :        1591 :       current_connection = connection == self->impl_->connection_seq_.load();
    1375                 :        1591 :       self->impl_->send_accounting_.complete_batch(
    1376                 :        5646 :           batch->buffers, bytes_written, [](const auto& item) { return item.request; },
    1377                 :        4055 :           [](const auto& item) {
    1378                 :        8110 :             return std::visit([](const auto& b) { return queue_util::variant_buffer_size(b); }, item.buffer);
    1379                 :             :           });
    1380                 :             :       // Preserve the completed prefix and close admission before a competing
    1381                 :             :       // stop can reclassify the rest of this connection as an explicit stop.
    1382   [ +  +  +  +  :        1618 :       if (current_connection && ec && ec != net::error::operation_aborted && seq == self->impl_->current_seq_.load()) {
          +  -  +  -  +  
                      + ]
    1383                 :          27 :         self->impl_->mark_disconnected_locked();
    1384                 :             :       }
    1385                 :        1591 :     }
    1386         [ +  + ]:        1591 :     if (!current_connection) {
    1387                 :             :       // Release pooled buffers while the captured client still owns its pool.
    1388                 :          63 :       batch->buffers.clear();
    1389                 :          63 :       return;
    1390                 :             :     }
    1391                 :        1528 :     self->impl_->active_write_.reset();
    1392   [ +  -  -  +  :        3056 :     if (ec == net::error::operation_aborted || seq != self->impl_->current_seq_.load()) {
                   -  + ]
    1393                 :           0 :       batch->buffers.clear();
    1394                 :           0 :       queue_util::release_reserved_write_bytes(self->impl_->queue_bytes_, queued_bytes);
    1395                 :           0 :       self->impl_->report_backpressure(self, self->impl_->queue_bytes_);
    1396                 :           0 :       self->impl_->writing_ = false;
    1397                 :           0 :       return;
    1398                 :             :     }
    1399                 :             : 
    1400         [ +  + ]:        1528 :     if (ec) {
    1401                 :          27 :       queue_util::return_gather_batch(self->impl_->tx_, batch->buffers);
    1402                 :             : 
    1403                 :          27 :       WIRESTEAD_LOG_ERROR("tcp_client", "do_write", fmt::format("Write failed: {}", ec.message()));
    1404                 :          27 :       self->impl_->record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::COMMUNICATION, "write", ec,
    1405                 :             :                                 fmt::format("Write failed: {}", ec.message()), false, 0);
    1406                 :          27 :       self->impl_->writing_ = false;
    1407                 :          27 :       self->impl_->handle_close(self, seq, ec);
    1408                 :          27 :       return;
    1409                 :             :     }
    1410                 :             : 
    1411                 :        1501 :     batch->buffers.clear();
    1412                 :        1501 :     self->impl_->stats_.record_sent(bytes_written);
    1413         [ +  - ]:        1501 :     if (bytes_written > 0) {
    1414                 :        1501 :       self->impl_->reset_idle_timer(self, seq);
    1415                 :             :     }
    1416                 :        1501 :     queue_util::release_reserved_write_bytes(self->impl_->queue_bytes_, queued_bytes);
    1417                 :        1501 :     self->impl_->report_backpressure(self, self->impl_->queue_bytes_);
    1418                 :             : 
    1419   [ +  +  +  -  :        3001 :     if (self->impl_->stop_requested_.load() || self->impl_->state_.is_state(LinkState::Closed) ||
                   -  + ]
    1420                 :        3001 :         self->impl_->state_.is_state(LinkState::Error)) {
    1421                 :           1 :       self->impl_->writing_ = false;
    1422                 :           1 :       return;
    1423                 :             :     }
    1424                 :             : 
    1425                 :        1500 :     self->impl_->do_write(self, seq);
    1426                 :        3189 :   };
    1427                 :             : 
    1428                 :             :   try {
    1429   [ +  +  -  + ]:        1598 :     if (auto hook = detail::g_tcp_write_initiation_hook.load()) hook();
    1430                 :             : #ifdef WIRESTEAD_TLS_ENABLED
    1431         [ +  + ]:        1591 :     if (tls_active()) {
    1432                 :           1 :       net::async_write(*tls_, batch->views, on_write);
    1433                 :             :     } else
    1434                 :             : #endif
    1435                 :             :     {
    1436                 :        1590 :       net::async_write(socket_, batch->views, on_write);
    1437                 :             :     }
    1438                 :           7 :   } catch (...) {
    1439                 :             :     // No completion handler owns this pending-I/O slot after failed initiation.
    1440                 :             :     // The accepted request crossed the local-write boundary; connection cleanup
    1441                 :             :     // terminates it as an abort and discards requests still awaiting handoff.
    1442                 :           7 :     --pending_io_;
    1443                 :           7 :     mark_disconnected();
    1444                 :           7 :     handle_close(self, seq, make_error_code(net::error::no_buffer_space));
    1445                 :           7 :     return;
    1446                 :           7 :   }
    1447   [ +  +  +  - ]:        1591 :   if (auto hook = detail::g_tcp_write_started_hook.load()) hook();
    1448                 :        1607 : }
    1449                 :             : 
    1450                 :         114 : void TcpClient::Impl::handle_close(std::shared_ptr<TcpClient> self, uint64_t seq, const boost::system::error_code& ec) {
    1451   [ +  -  -  +  :         228 :   if (ec == net::error::operation_aborted || seq != current_seq_.load()) {
                   -  + ]
    1452                 :           0 :     return;
    1453                 :             :   }
    1454                 :         114 :   mark_disconnected();
    1455                 :         114 :   discard_connection_writes();
    1456                 :         114 :   WIRESTEAD_LOG_INFO("tcp_client", "handle_close", fmt::format("Closing connection. Error: {}", ec.message()));
    1457         [ +  - ]:         114 :   if (ec) {
    1458                 :             :     bool has_policy;
    1459                 :             :     {
    1460                 :         114 :       std::lock_guard<std::mutex> lock(cfg_mtx_);
    1461                 :         114 :       has_policy = reconnect_policy_.has_value();
    1462                 :         114 :     }
    1463                 :         114 :     const bool retryable = diagnostics::is_retryable_tcp_connect_error(ec);
    1464         [ +  + ]:         114 :     const uint32_t current_attempts = has_policy ? reconnect_attempt_count_ : static_cast<uint32_t>(retry_attempts_);
    1465                 :             : 
    1466                 :         114 :     record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "handle_close", ec,
    1467                 :         228 :                  fmt::format("Connection closed with error: {}", ec.message()), retryable, current_attempts);
    1468                 :             :   }
    1469                 :         114 :   writing_ = false;
    1470                 :         114 :   cancel_idle_timer();
    1471                 :         114 :   connect_timer_.cancel();
    1472                 :         114 :   close_socket();
    1473   [ +  +  +  -  :         114 :   if (stop_requested_.load() || stopping_.load() || state_.is_state(LinkState::Closed)) {
             -  +  +  + ]
    1474                 :           1 :     transition_to(LinkState::Closed, ec);
    1475                 :           1 :     return;
    1476                 :             :   }
    1477                 :         113 :   transition_to(LinkState::Connecting, ec);
    1478                 :         113 :   schedule_retry(self, seq);
    1479                 :             : }
    1480                 :             : 
    1481                 :           3 : void TcpClient::Impl::handle_idle_timeout(std::shared_ptr<TcpClient> self, uint64_t seq) {
    1482   [ +  -  +  -  :           6 :   if (seq != current_seq_.load() || stop_requested_.load() || stopping_.load() || !connected_.load()) {
          +  -  -  +  -  
                      + ]
    1483                 :           2 :     return;
    1484                 :             :   }
    1485                 :             : 
    1486                 :           3 :   mark_disconnected();
    1487                 :           3 :   discard_connection_writes();
    1488                 :             :   IdleTimeoutAction idle_timeout_action;
    1489                 :             :   unsigned idle_timeout_ms;
    1490                 :             :   bool has_policy;
    1491                 :             :   {
    1492                 :           3 :     std::lock_guard<std::mutex> lock(cfg_mtx_);
    1493                 :           3 :     idle_timeout_action = cfg_.idle_timeout_action;
    1494                 :           3 :     idle_timeout_ms = cfg_.idle_timeout_ms;
    1495                 :           3 :     has_policy = reconnect_policy_.has_value();
    1496                 :           3 :   }
    1497                 :             : 
    1498                 :           3 :   const auto ec = make_error_code(boost::asio::error::timed_out);
    1499                 :           3 :   const bool should_reconnect = idle_timeout_action == IdleTimeoutAction::Reconnect;
    1500         [ -  + ]:           3 :   const uint32_t current_attempts = has_policy ? reconnect_attempt_count_ : static_cast<uint32_t>(retry_attempts_);
    1501                 :             : 
    1502   [ +  -  +  -  :           3 :   WIRESTEAD_LOG_WARNING("tcp_client", "idle_timeout",
          +  -  +  -  +  
             +  +  -  +  
                      - ]
    1503                 :             :                         fmt::format("Idle timeout expired after {}ms; {}", idle_timeout_ms,
    1504                 :             :                                     should_reconnect ? "scheduling reconnect" : "closing connection"));
    1505                 :           3 :   record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION, "idle_timeout", ec,
    1506                 :             :                "Idle timeout expired", should_reconnect, current_attempts);
    1507                 :             : 
    1508                 :           3 :   writing_ = false;
    1509                 :           3 :   cancel_idle_timer();
    1510                 :           3 :   connect_timer_.cancel();
    1511                 :           3 :   close_socket();
    1512                 :             : 
    1513         [ +  + ]:           3 :   if (!should_reconnect) {
    1514                 :           2 :     transition_to(LinkState::Closed, ec);
    1515                 :           2 :     return;
    1516                 :             :   }
    1517                 :             : 
    1518                 :           1 :   transition_to(LinkState::Connecting, ec);
    1519                 :           1 :   schedule_retry(self, seq);
    1520                 :             : }
    1521                 :             : 
    1522                 :         117 : void TcpClient::Impl::discard_connection_writes() {
    1523                 :         117 :   size_t messages = tx_.size() + pending_.size();
    1524                 :         117 :   size_t bytes = 0;
    1525         [ +  + ]:         159 :   for (const auto& buffer : tx_)
    1526                 :          84 :     bytes += std::visit([](const auto& b) { return queue_util::variant_buffer_size(b); }, buffer.buffer);
    1527         [ +  + ]:         118 :   for (const auto& buffer : pending_)
    1528                 :           2 :     bytes += std::visit([](const auto& b) { return queue_util::variant_buffer_size(b); }, buffer.buffer);
    1529         [ +  + ]:         117 :   if (active_write_) {
    1530                 :          20 :     messages += active_write_->buffers.size();
    1531                 :          20 :     bytes += active_write_->bytes;
    1532                 :             :     // Its completion handler retains the buffers; it must not mutate new state.
    1533                 :          20 :     active_write_.reset();
    1534                 :             :   }
    1535                 :         117 :   tx_.clear();
    1536                 :         117 :   pending_.clear();
    1537                 :         117 :   queue_bytes_ = 0;
    1538                 :         117 :   pending_bytes_ = 0;
    1539                 :         117 :   writing_ = false;
    1540                 :         117 :   backpressure_active_ = false;
    1541         [ +  + ]:         117 :   if (messages) stats_.record_dropped(messages, bytes);
    1542                 :         117 : }
    1543                 :             : 
    1544                 :        1334 : void TcpClient::Impl::close_socket() {
    1545                 :        1334 :   boost::system::error_code ec;
    1546                 :             : #ifdef WIRESTEAD_TLS_ENABLED
    1547                 :             :   // One SSL_shutdown writes close_notify and returns; a second would wait for
    1548                 :             :   // the peer's, which is the block measured at 8 s on the server side.
    1549   [ +  +  +  - ]:        1334 :   if (tls_) ::SSL_shutdown(tls_->native_handle());
    1550                 :             :   // Deliberately no tls_.reset() here. socket_.cancel()/close() do not retract
    1551                 :             :   // a boost::asio::ssl::detail::io_op continuation that is already queued on
    1552                 :             :   // the strand: the strand runs it afterwards, and it calls back into the
    1553                 :             :   // engine's BIO. Destroying the stream at this point therefore left that
    1554                 :             :   // continuation dereferencing freed memory - a SIGSEGV inside BIO_ctrl,
    1555                 :             :   // reproducible under parallel load and seen intermittently in CI.
    1556                 :             :   //
    1557                 :             :   // The stream is instead destroyed by the next tls_.emplace(), which runs on
    1558                 :             :   // the strand after any pending continuation, or by ~Impl once the io thread
    1559                 :             :   // has been joined. It cannot be moved out to a holder either - a pending
    1560                 :             :   // operation holds the stream's original address.
    1561                 :        1334 :   tls_engaged_ = false;
    1562                 :             : #endif
    1563                 :        1334 :   socket_.shutdown(tcp::socket::shutdown_both, ec);
    1564                 :        1334 :   socket_.close(ec);
    1565                 :        1334 : }
    1566                 :             : 
    1567                 :        1163 : void TcpClient::Impl::recalculate_backpressure_bounds() {
    1568                 :        1163 :   bp_high_ = cfg_.backpressure_threshold;
    1569         [ +  - ]:        1163 :   bp_low_ = bp_high_ > 1 ? bp_high_ / 2 : bp_high_;
    1570         [ -  + ]:        1163 :   if (bp_low_ == 0) {
    1571                 :           0 :     bp_low_ = 1;
    1572                 :             :   }
    1573                 :        1163 :   bp_limit_ = std::min(std::max(bp_high_ * 4, base::constants::DEFAULT_BACKPRESSURE_THRESHOLD),
    1574                 :             :                        base::constants::MAX_BUFFER_SIZE);
    1575         [ -  + ]:        1163 :   if (bp_limit_ < bp_high_) {
    1576                 :           0 :     bp_limit_ = bp_high_;
    1577                 :             :   }
    1578                 :        1163 :   backpressure_active_ = false;
    1579                 :        1163 : }
    1580                 :             : 
    1581                 :        7566 : queue_util::BackpressureFields TcpClient::Impl::bp_fields() {
    1582                 :        7566 :   return queue_util::BackpressureFields{queue_bytes_,
    1583                 :        7566 :                                         pending_bytes_,
    1584                 :        7566 :                                         backpressure_active_,
    1585                 :        7566 :                                         bp_high_,
    1586                 :        7566 :                                         bp_low_,
    1587                 :        7566 :                                         bp_limit_,
    1588                 :        7566 :                                         bp_strategy_.load(std::memory_order_relaxed),
    1589                 :        7566 :                                         &write_reserve_mtx_};
    1590                 :             : }
    1591                 :             : 
    1592                 :        2050 : void TcpClient::Impl::route_enqueued_buffer(std::shared_ptr<TcpClient> self, TrackedBuffer&& buf, size_t added) {
    1593   [ +  +  +  -  :        2050 :   if (stop_requested_.load() || state_.is_state(LinkState::Closed) || state_.is_state(LinkState::Error)) {
             -  +  +  + ]
    1594                 :          53 :     queue_util::release_reserved_limit_bytes(write_reserve_mtx_, inflight_bytes_, added);
    1595                 :          53 :     stats_.record_failed_send();
    1596                 :          77 :     return;
    1597                 :             :   }
    1598                 :             : 
    1599                 :        1997 :   auto f = bp_fields();
    1600                 :        1997 :   queue_util::DropAccounting dropped;
    1601                 :        1997 :   auto decision = queue_util::decide_enqueue(f, added, tx_, dropped, BufferProjection{}, [this](const auto& removed) {
    1602                 :             :     send_accounting_.discard(removed.request, Ledger::Cause::QueuePressure);
    1603                 :             :   });
    1604   [ -  +  -  - ]:        1997 :   if (dropped.any()) stats_.record_dropped(dropped.messages, dropped.bytes);
    1605                 :             : 
    1606         [ -  + ]:        1997 :   if (decision == queue_util::EnqueueDecision::Rejected) {
    1607                 :           0 :     send_accounting_.discard(buf.request, Ledger::Cause::QueuePressure);
    1608                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "write", fmt::format("Queue limit exceeded ({} bytes)", queue_bytes_ + added));
    1609                 :           0 :     record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::COMMUNICATION, "write",
    1610                 :             :                  boost::asio::error::no_buffer_space, "Queue limit exceeded", false, 0);
    1611                 :             :     // #448: this path used to leave RuntimeStats showing the message as
    1612                 :             :     // accepted (from the caller-thread pre-check) with no corresponding
    1613                 :             :     // sent/dropped/queued accounting - record it as dropped so it's at
    1614                 :             :     // least observable.
    1615                 :           0 :     stats_.record_dropped(1, added);
    1616                 :           0 :     queue_util::release_reserved_limit_bytes(write_reserve_mtx_, inflight_bytes_, added);
    1617                 :           0 :     report_backpressure(self, queue_bytes_ + added);
    1618                 :           0 :     return;
    1619                 :             :   }
    1620         [ +  + ]:        1997 :   if (decision == queue_util::EnqueueDecision::Pending) {
    1621                 :          24 :     queue_util::commit_reserved_limit_bytes(write_reserve_mtx_, pending_bytes_, inflight_bytes_, added);
    1622                 :          24 :     pending_.emplace_back(std::move(buf));
    1623                 :          24 :     observe_queue();
    1624                 :          24 :     return;
    1625                 :             :   }
    1626                 :        1973 :   queue_util::commit_reserved_limit_bytes(write_reserve_mtx_, queue_bytes_, inflight_bytes_, added);
    1627                 :        1973 :   tx_.emplace_back(std::move(buf));
    1628                 :        1973 :   observe_queue();
    1629                 :        1973 :   report_backpressure(self, queue_bytes_);
    1630   [ +  +  +  - ]:        3311 :   if (!writing_) do_write(self, current_seq_.load());
    1631                 :             : }
    1632                 :             : 
    1633                 :        9703 : void TcpClient::Impl::observe_queue() {
    1634                 :       29109 :   stats_.observe_queue(queue_bytes_.load(std::memory_order_relaxed) + pending_bytes_.load(std::memory_order_relaxed));
    1635                 :        9703 : }
    1636                 :             : 
    1637                 :        5602 : void TcpClient::Impl::report_backpressure(std::shared_ptr<TcpClient> self, size_t queued_bytes) {
    1638   [ +  +  -  +  :        5602 :   if (stop_requested_.load() || stopping_.load()) return;
                   +  + ]
    1639                 :        5569 :   observe_queue();
    1640                 :             : 
    1641                 :        5569 :   interface::SharedCallback<OnBackpressure> on_bp;
    1642                 :             :   {
    1643                 :        5569 :     std::lock_guard<std::mutex> lock(callback_mtx_);
    1644                 :        5569 :     on_bp = on_bp_;
    1645                 :        5569 :   }
    1646                 :        5569 :   static const OnBackpressure kNoCallback;
    1647                 :             : 
    1648                 :        5569 :   auto f = bp_fields();
    1649   [ +  +  +  - ]:       10772 :   queue_util::report_backpressure(
    1650                 :        5203 :       f, queued_bytes, on_bp ? *on_bp : kNoCallback, stats_,
    1651                 :           0 :       [&]() -> size_t {
    1652                 :          33 :         const size_t moved = pending_bytes_.exchange(0);
    1653         [ +  + ]:          56 :         while (!pending_.empty()) {
    1654                 :          23 :           tx_.emplace_back(std::move(pending_.front()));
    1655                 :          23 :           pending_.pop_front();
    1656                 :             :         }
    1657                 :          33 :         return moved;
    1658                 :             :       },
    1659                 :        5569 :       [&]() {
    1660                 :          33 :         observe_queue();
    1661   [ -  +  -  - ]:          33 :         if (!writing_) do_write(self, current_seq_.load());
    1662                 :          33 :       });
    1663                 :        5569 : }
    1664                 :             : 
    1665                 :        2008 : void TcpClient::Impl::transition_to(LinkState next, const boost::system::error_code& ec) {
    1666         [ -  + ]:        2008 :   if (ec == net::error::operation_aborted) {
    1667                 :           0 :     return;
    1668                 :             :   }
    1669                 :             : 
    1670                 :        2008 :   const auto current = state_.get();
    1671   [ +  +  +  + ]:        2008 :   const bool retrying_same_state = (next == LinkState::Connecting && current == LinkState::Connecting);
    1672   [ +  +  +  + ]:        2008 :   if ((current == LinkState::Closed || current == LinkState::Error) &&
    1673   [ -  +  -  - ]:          34 :       (next == LinkState::Closed || next == LinkState::Error)) {
    1674                 :          34 :     return;
    1675                 :             :   }
    1676                 :             : 
    1677   [ +  +  +  + ]:        1974 :   if (next == LinkState::Closed || next == LinkState::Error) {
    1678         [ -  + ]:         722 :     if (terminal_state_notified_.exchange(true)) {
    1679                 :           0 :       return;
    1680                 :             :     }
    1681   [ +  +  -  + ]:        1252 :   } else if (current == next && !retrying_same_state) {
    1682                 :           0 :     return;
    1683                 :             :   }
    1684                 :             : 
    1685                 :        1974 :   state_.set(next);
    1686                 :        1974 :   notify_state();
    1687                 :             : }
    1688                 :             : 
    1689                 :         722 : void TcpClient::Impl::perform_stop_cleanup() {
    1690         [ -  + ]:         722 :   if (cleanup_started_.exchange(true)) return;
    1691                 :             :   // Signals completion on every exit, including the error paths below.
    1692                 :             :   struct CompletionSignal {
    1693                 :             :     Impl* impl;
    1694                 :         722 :     ~CompletionSignal() {
    1695                 :         722 :       impl->cleanup_finished_ = true;
    1696         [ +  + ]:         722 :       if (impl->pending_io_ == 0) impl->mark_cleanup_done();
    1697                 :         722 :     }
    1698                 :         722 :   } completion_signal{this};
    1699                 :             :   try {
    1700                 :         722 :     retry_timer_.cancel();
    1701                 :         722 :     connect_timer_.cancel();
    1702                 :         722 :     cancel_idle_timer();
    1703                 :         722 :     resolver_.cancel();
    1704                 :         722 :     boost::system::error_code ec_cancel;
    1705                 :         722 :     socket_.cancel(ec_cancel);
    1706                 :         722 :     close_socket();
    1707                 :         722 :     tx_.clear();
    1708                 :         722 :     queue_bytes_ = 0;
    1709                 :         722 :     pending_.clear();
    1710                 :         722 :     pending_bytes_ = 0;
    1711                 :         722 :     writing_ = false;
    1712                 :         722 :     active_write_.reset();
    1713                 :         722 :     mark_disconnected();
    1714                 :             :     // Deliberately does NOT fire on_bp_ here, unlike UDP/server sessions'
    1715                 :             :     // terminal drain (#434): this is an explicit, tested contract
    1716                 :             :     // (ContractComplianceTest.TcpClient_Backpressure_Contract) - a
    1717                 :             :     // Reliable-mode caller blocked in send_blocking() is woken instead via
    1718                 :             :     // the wrapper's own bp_cv_.notify_all()/is_connected() check, not a
    1719                 :             :     // relief callback. Don't "fix" this to match the other transports
    1720                 :             :     // without updating that contract test first.
    1721                 :         722 :     backpressure_active_ = false;
    1722                 :             : 
    1723   [ +  +  +  +  :         722 :     if (owns_ioc_ && work_guard_) {
                   +  + ]
    1724                 :         163 :       work_guard_->reset();
    1725                 :             :     }
    1726                 :         722 :     transition_to(LinkState::Closed);
    1727                 :           0 :   } catch (const std::exception& e) {
    1728                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "stop_cleanup", fmt::format("Cleanup error: {}", e.what()));
    1729                 :           0 :     record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::SYSTEM, "stop_cleanup", {},
    1730                 :           0 :                  fmt::format("Cleanup error: {}", e.what()), false, 0);
    1731                 :           0 :     diagnostics::error_reporting::report_system_error("tcp_client", "stop_cleanup",
    1732                 :           0 :                                                       fmt::format("Exception in stop cleanup: {}", e.what()));
    1733                 :           0 :   } catch (...) {
    1734                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "stop_cleanup", "Unknown error in stop cleanup");
    1735                 :           0 :     diagnostics::error_reporting::report_system_error("tcp_client", "stop_cleanup", "Unknown error in stop cleanup");
    1736                 :           0 :   }
    1737                 :         722 : }
    1738                 :             : 
    1739                 :         700 : void TcpClient::Impl::reset_start_state() {
    1740                 :         700 :   cleanup_started_.store(false);
    1741                 :         700 :   cleanup_finished_ = false;
    1742                 :         700 :   inflight_bytes_.store(0);
    1743                 :             :   {
    1744                 :         700 :     std::lock_guard<std::mutex> lock(stop_mtx_);
    1745                 :         700 :     cleanup_done_ = false;
    1746                 :         700 :   }
    1747                 :         700 :   stop_requested_.store(false);
    1748                 :         700 :   stopping_.store(false);
    1749                 :         700 :   terminal_state_notified_.store(false);
    1750                 :         700 :   reconnect_pending_.store(false);
    1751                 :         700 :   retry_attempts_ = 0;
    1752                 :         700 :   reconnect_attempt_count_ = 0;
    1753                 :         700 :   mark_disconnected();
    1754                 :         700 :   writing_ = false;
    1755                 :         700 :   queue_bytes_ = 0;
    1756                 :         700 :   pending_.clear();
    1757                 :         700 :   pending_bytes_ = 0;
    1758                 :         700 :   backpressure_active_ = false;
    1759                 :         700 :   state_.set(LinkState::Idle);
    1760                 :         700 : }
    1761                 :             : 
    1762                 :        1922 : void TcpClient::Impl::join_ioc_thread(bool allow_detach) {
    1763   [ +  +  +  +  :        1922 :   if (!owns_ioc_ || !ioc_thread_.joinable()) {
                   +  + ]
    1764                 :        1759 :     return;
    1765                 :             :   }
    1766                 :             : 
    1767         [ -  + ]:         163 :   if (std::this_thread::get_id() == ioc_thread_.get_id()) {
    1768         [ #  # ]:           0 :     if (allow_detach) {
    1769                 :           0 :       ioc_thread_.detach();
    1770                 :             :     }
    1771                 :           0 :     return;
    1772                 :             :   }
    1773                 :             : 
    1774                 :             :   try {
    1775                 :         163 :     ioc_thread_.join();
    1776                 :           0 :   } catch (const std::exception& e) {
    1777                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "join", "Join failed: " + std::string(e.what()));
    1778                 :           0 :   } catch (...) {
    1779                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "join", "Join failed with unknown error");
    1780                 :           0 :   }
    1781                 :             : }
    1782                 :             : 
    1783                 :        1974 : void TcpClient::Impl::notify_state() {
    1784   [ +  +  -  +  :        2426 :   if (stop_requested_.load() || stopping_.load()) return;
                   +  + ]
    1785                 :             : 
    1786                 :        1284 :   interface::SharedCallback<OnState> on_state;
    1787                 :             :   {
    1788                 :        1284 :     std::lock_guard<std::mutex> lock(callback_mtx_);
    1789                 :        1284 :     on_state = on_state_;
    1790                 :        1284 :   }
    1791         [ +  + ]:        1284 :   if (!on_state) return;
    1792                 :             : 
    1793                 :             :   try {
    1794                 :         832 :     (*on_state)(state_.get());
    1795                 :           5 :   } catch (const std::exception& e) {
    1796                 :          12 :     WIRESTEAD_LOG_ERROR("tcp_client", "on_state", "Exception in state callback: " + std::string(e.what()));
    1797                 :           5 :   } catch (...) {
    1798                 :           1 :     WIRESTEAD_LOG_ERROR("tcp_client", "on_state", "Unknown exception in state callback");
    1799                 :           1 :   }
    1800                 :        1284 : }
    1801                 :             : 
    1802                 :         230 : void TcpClient::Impl::record_error(diagnostics::ErrorLevel lvl, diagnostics::ErrorCategory cat,
    1803                 :             :                                    std::string_view operation, const boost::system::error_code& ec,
    1804                 :             :                                    std::string_view msg, bool retryable, uint32_t retry_count) {
    1805                 :         230 :   error_info_holder_.record_error(lvl, cat, operation, ec, msg, retryable, retry_count);
    1806                 :         230 : }
    1807                 :             : 
    1808                 :         495 : void TcpClient::Impl::reset_io_objects() {
    1809                 :             :   try {
    1810                 :         495 :     boost::system::error_code ec_cancel;
    1811                 :         495 :     socket_.cancel(ec_cancel);
    1812                 :         495 :     close_socket();
    1813                 :         495 :     socket_ = tcp::socket(strand_);
    1814                 :         495 :     resolver_.cancel();
    1815                 :         495 :     resolver_ = tcp::resolver(strand_);
    1816                 :         495 :     retry_timer_ = net::steady_timer(strand_);
    1817                 :         495 :     connect_timer_ = net::steady_timer(strand_);
    1818                 :         495 :     idle_timer_ = net::steady_timer(strand_);
    1819                 :         495 :     tx_.clear();
    1820                 :         495 :     queue_bytes_ = 0;
    1821                 :         495 :     pending_.clear();
    1822                 :         495 :     pending_bytes_ = 0;
    1823                 :         495 :     writing_ = false;
    1824                 :         495 :     backpressure_active_ = false;
    1825                 :           0 :   } catch (const std::exception& e) {
    1826                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "reset_io_objects", fmt::format("Reset error: {}", e.what()));
    1827                 :           0 :     record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::SYSTEM, "reset_io_objects", {},
    1828                 :           0 :                  fmt::format("Reset error: {}", e.what()), false, 0);
    1829                 :           0 :     diagnostics::error_reporting::report_system_error(
    1830                 :           0 :         "tcp_client", "reset_io_objects", fmt::format("Exception while resetting io objects: {}", e.what()));
    1831                 :           0 :   } catch (...) {
    1832                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_client", "reset_io_objects", "Unknown reset error");
    1833                 :           0 :     diagnostics::error_reporting::report_system_error("tcp_client", "reset_io_objects",
    1834                 :             :                                                       "Unknown error while resetting io objects");
    1835                 :           0 :   }
    1836                 :         495 : }
    1837                 :             : 
    1838                 :        2223 : void TcpClient::Impl::reset_idle_timer(std::shared_ptr<TcpClient> self, uint64_t seq) {
    1839                 :             :   unsigned idle_timeout_ms;
    1840                 :             :   {
    1841                 :        2223 :     std::lock_guard<std::mutex> lock(cfg_mtx_);
    1842                 :        2223 :     idle_timeout_ms = cfg_.idle_timeout_ms;
    1843                 :        2223 :   }
    1844   [ +  +  +  -  :        2223 :   if (idle_timeout_ms == 0 || !connected_.load() || stop_requested_.load() || stopping_.load()) {
          +  -  -  +  +  
                      + ]
    1845                 :        2211 :     return;
    1846                 :             :   }
    1847                 :             : 
    1848                 :          12 :   idle_timer_.cancel();
    1849                 :          12 :   idle_timer_.expires_after(std::chrono::milliseconds(idle_timeout_ms));
    1850                 :          12 :   const auto connection = connection_seq_.load();
    1851                 :          12 :   ++pending_io_;
    1852                 :          12 :   idle_timer_.async_wait([self, seq, connection](const boost::system::error_code& ec) {
    1853                 :          12 :     IoCompletion completion{self->impl_.get()};
    1854   [ +  +  +  -  :          18 :     if (ec == net::error::operation_aborted || seq != self->impl_->current_seq_.load() ||
                   +  + ]
    1855         [ -  + ]:           6 :         connection != self->impl_->connection_seq_.load()) {
    1856                 :           9 :       return;
    1857                 :             :     }
    1858         [ +  - ]:           3 :     if (!ec) {
    1859                 :           3 :       self->impl_->handle_idle_timeout(self, seq);
    1860                 :             :     }
    1861                 :          12 :   });
    1862                 :             : }
    1863                 :             : 
    1864                 :         839 : void TcpClient::Impl::cancel_idle_timer() { idle_timer_.cancel(); }
    1865                 :             : 
    1866                 :             : }  // namespace transport
    1867                 :             : }  // namespace wirestead
        

Generated by: LCOV version 2.0-1