LCOV - code coverage report
Current view: top level - wirestead/transport/tcp_server - tcp_server_session.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 97.0 % 498 483
Test Date: 2026-10-29 11:36:05 Functions: 100.0 % 66 66
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 82.0 % 350 287

             Branch data     Line data    Source code
       1                 :             : /*
       2                 :             :  * Copyright 2025 Jinwoo Sung
       3                 :             :  *
       4                 :             :  * Licensed under the Apache License, Version 2.0 (the "License");
       5                 :             :  * you may not use this file except in compliance with the License.
       6                 :             :  * You may obtain a copy of the License at
       7                 :             :  *
       8                 :             :  *     http://www.apache.org/licenses/LICENSE-2.0
       9                 :             :  *
      10                 :             :  * Unless required by applicable law or agreed to in writing, software
      11                 :             :  * distributed under the License is distributed on an "AS IS" BASIS,
      12                 :             :  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13                 :             :  * See the License for the specific language governing permissions and
      14                 :             :  * limitations under the License.
      15                 :             :  */
      16                 :             : 
      17                 :             : #include "wirestead/transport/tcp_server/tcp_server_session.hpp"
      18                 :             : 
      19                 :             : #include <cstring>
      20                 :             : #include <iostream>
      21                 :             : 
      22                 :             : #include "wirestead/diagnostics/callback.hpp"
      23                 :             : #include "wirestead/memory/memory_pool.hpp"
      24                 :             : #include "wirestead/transport/base/bp_utils.hpp"
      25                 :             : #include "wirestead/transport/base/stop_test_hook.hpp"
      26                 :             : #include "wirestead/transport/tcp_server/boost_tcp_socket.hpp"
      27                 :             : 
      28                 :             : namespace wirestead {
      29                 :             : namespace transport {
      30                 :             : 
      31                 :         316 : TcpServerSession::TcpServerSession(net::io_context& ioc, tcp::socket sock, size_t backpressure_threshold,
      32                 :             :                                    int idle_timeout_ms, base::constants::BackpressureStrategy strategy,
      33                 :         316 :                                    bool enable_memory_pool, size_t read_buffer_size)
      34                 :         316 :     : ioc_(ioc),
      35                 :         316 :       strand_(ioc.get_executor()),
      36                 :         316 :       idle_timer_(ioc),
      37                 :         316 :       socket_(std::make_unique<BoostTcpSocket>(std::move(sock))),
      38                 :         316 :       enable_memory_pool_(enable_memory_pool),
      39                 :         316 :       writing_(false),
      40                 :         316 :       queue_bytes_(0),
      41                 :         316 :       bp_strategy_(strategy),
      42                 :         316 :       bp_high_(backpressure_threshold),
      43                 :         316 :       idle_timeout_ms_(idle_timeout_ms),
      44                 :         316 :       alive_(false),
      45                 :        1264 :       cleanup_done_(false) {
      46                 :         316 :   bp_limit_ = std::min(std::max(bp_high_ * 4, base::constants::DEFAULT_BACKPRESSURE_THRESHOLD),
      47                 :             :                        base::constants::MAX_BUFFER_SIZE);
      48         [ +  - ]:         316 :   bp_low_ = bp_high_ > 1 ? bp_high_ / 2 : bp_high_;
      49         [ -  + ]:         316 :   if (bp_low_ == 0) bp_low_ = 1;
      50                 :         316 :   rx_.resize(
      51                 :         316 :       std::clamp(read_buffer_size, base::constants::MIN_READ_BUFFER_SIZE, base::constants::MAX_READ_BUFFER_SIZE));
      52                 :         316 : }
      53                 :             : 
      54                 :         206 : TcpServerSession::TcpServerSession(net::io_context& ioc, std::unique_ptr<interface::TcpSocketInterface> socket,
      55                 :             :                                    size_t backpressure_threshold, int idle_timeout_ms,
      56                 :             :                                    base::constants::BackpressureStrategy strategy, bool enable_memory_pool,
      57                 :         206 :                                    size_t read_buffer_size)
      58                 :         206 :     : ioc_(ioc),
      59                 :         206 :       strand_(ioc.get_executor()),
      60                 :         206 :       idle_timer_(ioc),
      61                 :         206 :       socket_(std::move(socket)),
      62                 :         206 :       enable_memory_pool_(enable_memory_pool),
      63                 :         206 :       writing_(false),
      64                 :         206 :       queue_bytes_(0),
      65                 :         206 :       bp_strategy_(strategy),
      66                 :         206 :       bp_high_(backpressure_threshold),
      67                 :         206 :       idle_timeout_ms_(idle_timeout_ms),
      68                 :         206 :       alive_(false),
      69                 :         824 :       cleanup_done_(false) {
      70                 :         206 :   bp_limit_ = std::min(std::max(bp_high_ * 4, base::constants::DEFAULT_BACKPRESSURE_THRESHOLD),
      71                 :             :                        base::constants::MAX_BUFFER_SIZE);
      72         [ +  - ]:         206 :   bp_low_ = bp_high_ > 1 ? bp_high_ / 2 : bp_high_;
      73         [ -  + ]:         206 :   if (bp_low_ == 0) bp_low_ = 1;
      74                 :         206 :   rx_.resize(
      75                 :         206 :       std::clamp(read_buffer_size, base::constants::MIN_READ_BUFFER_SIZE, base::constants::MAX_READ_BUFFER_SIZE));
      76                 :         206 : }
      77                 :             : 
      78                 :         174 : void TcpServerSession::start() { start_with_notification({}); }
      79                 :             : 
      80                 :         522 : void TcpServerSession::start_with_notification(std::function<void()> notify) {
      81         [ -  + ]:         522 :   if (alive_.exchange(true)) return;
      82                 :         522 :   auto self = shared_from_this();
      83                 :         522 :   net::post(strand_, [self, notify = std::move(notify)] {
      84   [ +  +  -  +  :         522 :     if (self->closing_ || !self->alive_) return;
                   +  + ]
      85                 :         490 :     self->reset_idle_timer();
      86                 :             :     // No-op on a plain socket, the TLS handshake on an encrypted one. Reading
      87                 :             :     // before it completes would hand the session ciphertext, so the first read
      88                 :             :     // waits on it - and a failed handshake closes rather than reads.
      89                 :         490 :     self->socket_->async_handshake([self](const boost::system::error_code& ec) {
      90                 :             :       // Plain sockets may complete inline. Queue read startup so connection
      91                 :             :       // notification finishes first, just as it does for an async TLS handshake.
      92                 :         490 :       net::post(self->strand_, [self, ec] {
      93   [ +  +  -  +  :         490 :         if (self->closing_ || !self->alive_) return;
                   +  + ]
      94         [ +  + ]:         471 :         if (ec) {
      95                 :          30 :           WIRESTEAD_LOG_WARNING("tcp_server_session", "handshake", "Handshake failed: " + ec.message());
      96                 :          30 :           self->do_close();
      97                 :          30 :           return;
      98                 :             :         }
      99                 :         441 :         self->reset_idle_timer();
     100                 :         441 :         self->start_read();
     101                 :             :       });
     102                 :         490 :     });
     103                 :         490 :     diagnostics::invoke_callback("tcp_server_session", "on_connect", notify);
     104                 :             :   });
     105                 :         522 : }
     106                 :             : 
     107                 :         894 : bool TcpServerSession::async_write_copy(memory::ConstByteSpan data) {
     108                 :         894 :   const auto result = write_copy(data);
     109   [ +  +  +  - ]:         894 :   if (auto hook = detail::g_tcp_session_write_result_hook.load()) hook(result);
     110                 :        1788 :   return result.accepted();
     111                 :             : }
     112                 :             : 
     113                 :        1115 : wrapper::SendResult TcpServerSession::write_copy(memory::ConstByteSpan data) {
     114                 :        1115 :   std::lock_guard<std::mutex> admission_lock(submission_mtx_);
     115   [ +  +  +  - ]:        1115 :   if (auto hook = detail::g_tcp_session_write_admission_hook.load()) hook();
     116   [ +  +  +  +  :        1115 :   if (!alive_ || closing_) {
                   +  + ]
     117                 :          30 :     stats_.record_failed_send();
     118                 :          30 :     return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     119                 :             :   }  // Don't queue writes if session is not alive
     120                 :             : 
     121                 :        1085 :   size_t size = data.size();
     122         [ +  + ]:        1085 :   if (size == 0) {
     123                 :           6 :     stats_.record_failed_send();
     124                 :           6 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     125                 :             :   }
     126         [ +  + ]:        1079 :   if (size > base::constants::MAX_BUFFER_SIZE) {
     127                 :           1 :     WIRESTEAD_LOG_ERROR("tcp_server_session", "write", "Write size exceeds maximum allowed");
     128                 :           1 :     stats_.record_failed_send();
     129                 :           1 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     130                 :             :   }
     131                 :             : 
     132                 :             :   // Use memory pool for better performance (only for reasonable sizes)
     133   [ +  +  +  + ]:        1078 :   if (size <= base::constants::LARGE_BUFFER_THRESHOLD && enable_memory_pool_) {  // Only use pool for buffers <= 64KB
     134                 :         577 :     memory::PooledBuffer pooled_buffer(size, pool_);
     135   [ +  -  +  - ]:         577 :     if (pooled_buffer.valid()) {
     136                 :             :       // Copy data to pooled buffer safely
     137                 :         577 :       base::safe_memory::safe_memcpy(pooled_buffer.data(), data.data(), size);
     138   [ +  -  +  + ]:         577 :       if (!queue_util::try_reserve_limit_bytes(write_reserve_mtx_, queue_bytes_, pending_bytes_, inflight_bytes_, size,
     139                 :             :                                                bp_limit_)) {
     140                 :          25 :         stats_.record_failed_send();
     141                 :          25 :         return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     142                 :             :       }
     143                 :         552 :       Ledger::Admission admission(send_accounting_, size);
     144                 :         552 :       stats_.record_accepted(size);
     145                 :         552 :       net::post(strand_,
     146                 :         552 :                 [self = shared_from_this(), buf = std::move(pooled_buffer), request = admission.request()]() mutable {
     147                 :         552 :                   const auto added = buf.size();
     148   [ +  +  +  +  :         552 :                   if (!self->alive_ || self->closing_) {  // Double-check in case session was closed
                   +  + ]
     149                 :           5 :                     queue_util::release_reserved_limit_bytes(self->write_reserve_mtx_, self->inflight_bytes_, added);
     150                 :           5 :                     self->stats_.record_failed_send();
     151                 :           5 :                     return;
     152                 :             :                   }
     153                 :         547 :                   self->route_enqueued_buffer(TrackedBuffer{BufferVariant{std::move(buf)}, request}, added);
     154                 :             :                 });
     155                 :         552 :       admission.commit();
     156                 :         552 :       return wrapper::SendResult::accept();
     157                 :         552 :     }
     158                 :         577 :   }
     159                 :             : 
     160                 :             :   // Fallback to regular allocation for large buffers or pool exhaustion
     161   [ +  -  +  + ]:         501 :   if (!queue_util::try_reserve_limit_bytes(write_reserve_mtx_, queue_bytes_, pending_bytes_, inflight_bytes_, size,
     162                 :             :                                            bp_limit_)) {
     163                 :          54 :     stats_.record_failed_send();
     164                 :          54 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     165                 :             :   }
     166                 :         447 :   std::vector<uint8_t> fallback(data.begin(), data.end());
     167                 :         447 :   Ledger::Admission admission(send_accounting_, size);
     168                 :         447 :   stats_.record_accepted(size);
     169                 :             : 
     170                 :         447 :   net::post(strand_,
     171                 :         447 :             [self = shared_from_this(), buf = std::move(fallback), size, request = admission.request()]() mutable {
     172   [ +  +  +  +  :         447 :               if (!self->alive_ || self->closing_) {  // Double-check in case session was closed
                   +  + ]
     173                 :           5 :                 queue_util::release_reserved_limit_bytes(self->write_reserve_mtx_, self->inflight_bytes_, size);
     174                 :           5 :                 self->stats_.record_failed_send();
     175                 :           5 :                 return;
     176                 :             :               }
     177                 :         442 :               self->route_enqueued_buffer(TrackedBuffer{BufferVariant{std::move(buf)}, request}, size);
     178                 :             :             });
     179                 :         447 :   admission.commit();
     180                 :         447 :   return wrapper::SendResult::accept();
     181                 :        1115 : }
     182                 :             : 
     183                 :       24531 : bool TcpServerSession::async_write_move(std::vector<uint8_t>&& data) {
     184                 :       24531 :   const auto result = write_move(std::move(data));
     185   [ +  +  +  - ]:       24531 :   if (auto hook = detail::g_tcp_session_write_result_hook.load()) hook(result);
     186                 :       49062 :   return result.accepted();
     187                 :             : }
     188                 :             : 
     189                 :       24531 : wrapper::SendResult TcpServerSession::write_move(std::vector<uint8_t>&& data) {
     190                 :       24531 :   std::lock_guard<std::mutex> admission_lock(submission_mtx_);
     191   [ +  +  +  - ]:       24531 :   if (auto hook = detail::g_tcp_session_write_admission_hook.load()) hook();
     192   [ +  +  +  +  :       24531 :   if (!alive_ || closing_) {
                   +  + ]
     193                 :          14 :     stats_.record_failed_send();
     194                 :          14 :     return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     195                 :             :   }
     196                 :       24517 :   const auto added = data.size();
     197         [ +  + ]:       24517 :   if (added == 0) {
     198                 :           5 :     stats_.record_failed_send();
     199                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     200                 :             :   }
     201         [ +  + ]:       24512 :   if (added > base::constants::MAX_BUFFER_SIZE) {
     202                 :           1 :     WIRESTEAD_LOG_ERROR("tcp_server_session", "write", "Write size exceeds maximum allowed");
     203                 :           1 :     stats_.record_failed_send();
     204                 :           1 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     205                 :             :   }
     206   [ +  -  +  + ]:       24511 :   if (!queue_util::try_reserve_limit_bytes(write_reserve_mtx_, queue_bytes_, pending_bytes_, inflight_bytes_, added,
     207                 :             :                                            bp_limit_)) {
     208                 :       13522 :     stats_.record_failed_send();
     209                 :       13522 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     210                 :             :   }
     211                 :       10989 :   Ledger::Admission admission(send_accounting_, added);
     212                 :       10989 :   stats_.record_accepted(added);
     213                 :       10989 :   net::post(strand_,
     214                 :       10989 :             [self = shared_from_this(), buf = std::move(data), added, request = admission.request()]() mutable {
     215   [ +  +  +  +  :       10989 :               if (!self->alive_ || self->closing_) {
                   +  + ]
     216                 :          19 :                 queue_util::release_reserved_limit_bytes(self->write_reserve_mtx_, self->inflight_bytes_, added);
     217                 :          19 :                 self->stats_.record_failed_send();
     218                 :          19 :                 return;
     219                 :             :               }
     220                 :       10970 :               self->route_enqueued_buffer(TrackedBuffer{BufferVariant{std::move(buf)}, request}, added);
     221                 :             :             });
     222                 :       10989 :   admission.commit();
     223                 :       10989 :   return wrapper::SendResult::accept();
     224                 :       24531 : }
     225                 :             : 
     226                 :         458 : bool TcpServerSession::async_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     227                 :         458 :   const auto result = write_shared(std::move(data));
     228   [ +  +  +  - ]:         458 :   if (auto hook = detail::g_tcp_session_write_result_hook.load()) hook(result);
     229                 :         916 :   return result.accepted();
     230                 :             : }
     231                 :             : 
     232                 :         458 : wrapper::SendResult TcpServerSession::write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     233                 :         458 :   std::lock_guard<std::mutex> admission_lock(submission_mtx_);
     234   [ +  +  +  - ]:         458 :   if (auto hook = detail::g_tcp_session_write_admission_hook.load()) hook();
     235   [ +  +  +  +  :         458 :   if (!alive_ || closing_) {
                   +  + ]
     236                 :           8 :     stats_.record_failed_send();
     237                 :           8 :     return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     238                 :             :   }
     239   [ +  +  +  +  :         450 :   if (!data || data->empty()) {
                   +  + ]
     240                 :           6 :     stats_.record_failed_send();
     241                 :           6 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     242                 :             :   }
     243                 :         444 :   const auto added = data->size();
     244         [ +  + ]:         444 :   if (added > base::constants::MAX_BUFFER_SIZE) {
     245                 :           1 :     WIRESTEAD_LOG_ERROR("tcp_server_session", "write", "Write size exceeds maximum allowed");
     246                 :           1 :     stats_.record_failed_send();
     247                 :           1 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     248                 :             :   }
     249   [ +  -  +  + ]:         443 :   if (!queue_util::try_reserve_limit_bytes(write_reserve_mtx_, queue_bytes_, pending_bytes_, inflight_bytes_, added,
     250                 :             :                                            bp_limit_)) {
     251                 :           6 :     stats_.record_failed_send();
     252                 :           6 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     253                 :             :   }
     254                 :         437 :   Ledger::Admission admission(send_accounting_, added);
     255                 :         437 :   stats_.record_accepted(added);
     256                 :         437 :   net::post(strand_,
     257                 :         437 :             [self = shared_from_this(), buf = std::move(data), added, request = admission.request()]() mutable {
     258   [ +  +  +  +  :         437 :               if (!self->alive_ || self->closing_) {
                   +  + ]
     259                 :           7 :                 queue_util::release_reserved_limit_bytes(self->write_reserve_mtx_, self->inflight_bytes_, added);
     260                 :           7 :                 self->stats_.record_failed_send();
     261                 :           7 :                 return;
     262                 :             :               }
     263                 :         430 :               self->route_enqueued_buffer(TrackedBuffer{BufferVariant{std::move(buf)}, request}, added);
     264                 :             :             });
     265                 :         437 :   admission.commit();
     266                 :         437 :   return wrapper::SendResult::accept();
     267                 :         458 : }
     268                 :             : 
     269                 :         457 : bool TcpServerSession::async_try_write_copy(memory::ConstByteSpan data) {
     270                 :         457 :   const auto result = try_write_copy(data);
     271   [ +  +  +  - ]:         457 :   if (auto hook = detail::g_tcp_session_write_result_hook.load()) hook(result);
     272                 :         914 :   return result.accepted();
     273                 :             : }
     274                 :             : 
     275                 :         515 : wrapper::SendResult TcpServerSession::try_write_copy(memory::ConstByteSpan data) {
     276   [ +  +  +  +  :         515 :   if (data.empty() || data.size() > base::constants::MAX_BUFFER_SIZE) {
                   +  + ]
     277                 :           6 :     stats_.record_failed_send();
     278         [ +  + ]:           6 :     return wrapper::SendResult::reject(data.empty() ? wrapper::SendRejection::InvalidArgument
     279                 :           6 :                                                     : wrapper::SendRejection::TooLarge);
     280                 :             :   }
     281                 :        1018 :   return try_write_move(std::vector<uint8_t>(data.begin(), data.end()));
     282                 :             : }
     283                 :             : 
     284                 :         471 : bool TcpServerSession::async_try_write_move(std::vector<uint8_t>&& data) {
     285                 :         471 :   const auto result = try_write_move(std::move(data));
     286   [ +  +  +  - ]:         471 :   if (auto hook = detail::g_tcp_session_write_result_hook.load()) hook(result);
     287                 :         942 :   return result.accepted();
     288                 :             : }
     289                 :             : 
     290                 :         980 : wrapper::SendResult TcpServerSession::try_write_move(std::vector<uint8_t>&& data) {
     291                 :         980 :   std::lock_guard<std::mutex> admission_lock(submission_mtx_);
     292   [ +  +  +  - ]:         980 :   if (auto hook = detail::g_tcp_session_write_admission_hook.load()) hook();
     293   [ +  +  +  +  :         980 :   if (!alive_ || closing_) {
                   +  + ]
     294                 :          16 :     stats_.record_failed_send();
     295                 :          16 :     return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     296                 :             :   }
     297                 :         964 :   const auto added = data.size();
     298   [ +  +  +  + ]:         964 :   if (added == 0 || added > base::constants::MAX_BUFFER_SIZE) {
     299                 :           6 :     stats_.record_failed_send();
     300         [ +  + ]:           6 :     return wrapper::SendResult::reject(added == 0 ? wrapper::SendRejection::InvalidArgument
     301                 :           6 :                                                   : wrapper::SendRejection::TooLarge);
     302                 :             :   }
     303                 :          22 :   const auto reject_for_pressure = [this, added]() {
     304         [ +  + ]:          22 :     if (bp_strategy_ == base::constants::BackpressureStrategy::BestEffort) {
     305                 :          14 :       stats_.record_dropped(1, added);
     306                 :             :     } else {
     307                 :           8 :       stats_.record_failed_send();
     308                 :             :     }
     309                 :         980 :   };
     310   [ +  +  +  +  :        1894 :   if (backpressure_active_.load() || queue_bytes_ + added > bp_high_ ||
                   +  + ]
     311         [ -  + ]:         936 :       queue_bytes_ + pending_bytes_ + added > bp_limit_) {
     312                 :          22 :     reject_for_pressure();
     313                 :          22 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     314                 :             :   }
     315         [ -  + ]:         936 :   if (!queue_util::try_reserve_write_bytes(write_reserve_mtx_, inflight_bytes_, queue_bytes_, pending_bytes_,
     316                 :         936 :                                            backpressure_active_, added, bp_high_, bp_limit_)) {
     317                 :           0 :     reject_for_pressure();
     318                 :           0 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     319                 :             :   }
     320                 :         936 :   Ledger::Admission admission(send_accounting_, added);
     321                 :         936 :   stats_.record_accepted(added);
     322                 :             : 
     323                 :         936 :   net::post(strand_,
     324                 :         936 :             [self = shared_from_this(), buf = std::move(data), added, request = admission.request()]() mutable {
     325   [ +  +  +  +  :         936 :               if (!self->alive_ || self->closing_) {
                   +  + ]
     326                 :          26 :                 queue_util::release_reserved_write_bytes(self->queue_bytes_, added);
     327                 :          26 :                 self->stats_.record_failed_send();
     328                 :          26 :                 return;
     329                 :             :               }
     330                 :             : 
     331                 :         910 :               self->tx_.push_back(TrackedBuffer{BufferVariant{std::move(buf)}, request});
     332                 :         910 :               self->observe_queue();
     333                 :         910 :               self->report_backpressure(self->queue_bytes_);
     334         [ +  + ]:         910 :               if (!self->writing_) self->do_write();
     335                 :             :             });
     336                 :         936 :   admission.commit();
     337                 :         936 :   return wrapper::SendResult::accept();
     338                 :         980 : }
     339                 :             : 
     340                 :         459 : bool TcpServerSession::async_try_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     341                 :         459 :   const auto result = try_write_shared(std::move(data));
     342   [ +  +  +  - ]:         459 :   if (auto hook = detail::g_tcp_session_write_result_hook.load()) hook(result);
     343                 :         918 :   return result.accepted();
     344                 :             : }
     345                 :             : 
     346                 :        1824 : wrapper::SendResult TcpServerSession::try_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     347                 :        1824 :   std::lock_guard<std::mutex> admission_lock(submission_mtx_);
     348   [ +  +  +  - ]:        1824 :   if (auto hook = detail::g_tcp_session_write_admission_hook.load()) hook();
     349   [ +  +  +  +  :        1824 :   if (!alive_ || closing_) {
                   +  + ]
     350                 :          10 :     stats_.record_failed_send();
     351                 :          10 :     return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     352                 :             :   }
     353   [ +  -  +  +  :        1814 :   if (!data || data->empty()) {
                   +  + ]
     354                 :           5 :     stats_.record_failed_send();
     355                 :           5 :     return wrapper::SendResult::reject(wrapper::SendRejection::InvalidArgument);
     356                 :             :   }
     357                 :        1809 :   const auto added = data->size();
     358         [ +  + ]:        1809 :   if (added > base::constants::MAX_BUFFER_SIZE) {
     359                 :           1 :     stats_.record_failed_send();
     360                 :           1 :     return wrapper::SendResult::reject(wrapper::SendRejection::TooLarge);
     361                 :             :   }
     362                 :         229 :   const auto reject_for_pressure = [this, added]() {
     363         [ +  + ]:         229 :     if (bp_strategy_ == base::constants::BackpressureStrategy::BestEffort) {
     364                 :           7 :       stats_.record_dropped(1, added);
     365                 :             :     } else {
     366                 :         222 :       stats_.record_failed_send();
     367                 :             :     }
     368                 :        2037 :   };
     369   [ +  +  +  +  :        3387 :   if (backpressure_active_.load() || queue_bytes_ + added > bp_high_ ||
                   +  + ]
     370         [ -  + ]:        1579 :       queue_bytes_ + pending_bytes_ + added > bp_limit_) {
     371                 :         229 :     reject_for_pressure();
     372                 :         229 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     373                 :             :   }
     374         [ -  + ]:        1579 :   if (!queue_util::try_reserve_write_bytes(write_reserve_mtx_, inflight_bytes_, queue_bytes_, pending_bytes_,
     375                 :        1579 :                                            backpressure_active_, added, bp_high_, bp_limit_)) {
     376                 :           0 :     reject_for_pressure();
     377                 :           0 :     return wrapper::SendResult::reject(wrapper::SendRejection::WouldBlock);
     378                 :             :   }
     379                 :        1579 :   Ledger::Admission admission(send_accounting_, added);
     380                 :        1579 :   stats_.record_accepted(added);
     381                 :             : 
     382                 :        1579 :   net::post(strand_,
     383                 :        1579 :             [self = shared_from_this(), buf = std::move(data), added, request = admission.request()]() mutable {
     384   [ +  +  +  +  :        1579 :               if (!self->alive_ || self->closing_) {
                   +  + ]
     385                 :          17 :                 queue_util::release_reserved_write_bytes(self->queue_bytes_, added);
     386                 :          17 :                 self->stats_.record_failed_send();
     387                 :          17 :                 return;
     388                 :             :               }
     389                 :             : 
     390                 :        1562 :               self->tx_.push_back(TrackedBuffer{BufferVariant{std::move(buf)}, request});
     391                 :        1562 :               self->observe_queue();
     392                 :        1562 :               self->report_backpressure(self->queue_bytes_);
     393         [ +  + ]:        1562 :               if (!self->writing_) self->do_write();
     394                 :             :             });
     395                 :        1579 :   admission.commit();
     396                 :        1579 :   return wrapper::SendResult::accept();
     397                 :        1824 : }
     398                 :             : 
     399                 :         353 : void TcpServerSession::on_bytes(OnBytes cb) {
     400                 :         353 :   auto self = shared_from_this();
     401                 :         353 :   net::dispatch(strand_, [self, cb = std::move(cb)]() mutable {
     402   [ +  -  -  +  :         353 :     if (self->closing_.load() || self->cleanup_done_.load()) return;
                   -  + ]
     403                 :         353 :     self->on_bytes_ = std::move(cb);
     404                 :             :   });
     405                 :         353 : }
     406                 :         350 : void TcpServerSession::on_backpressure(OnBackpressure cb) {
     407                 :         350 :   auto self = shared_from_this();
     408                 :         350 :   net::dispatch(strand_, [self, cb = std::move(cb)]() mutable {
     409   [ +  -  -  +  :         350 :     if (self->closing_.load() || self->cleanup_done_.load()) return;
                   -  + ]
     410                 :         350 :     self->on_bp_ = std::move(cb);
     411                 :             :   });
     412                 :         350 : }
     413                 :         355 : void TcpServerSession::on_close(OnClose cb) {
     414                 :         355 :   auto self = shared_from_this();
     415                 :         355 :   net::dispatch(strand_, [self, cb = std::move(cb)]() mutable {
     416   [ +  -  -  +  :         355 :     if (self->closing_.load() || self->cleanup_done_.load()) return;
                   -  + ]
     417                 :         355 :     self->on_close_ = std::move(cb);
     418                 :             :   });
     419                 :         355 : }
     420                 :             : 
     421                 :         298 : std::optional<wrapper::SendResult> TcpServerSession::poll_write_wait() const {
     422                 :         298 :   std::lock_guard<std::mutex> lock(submission_mtx_);
     423         [ +  + ]:         298 :   if (wait_ended_by_) return wrapper::SendResult::reject(*wait_ended_by_);
     424   [ +  -  -  +  :         256 :   if (!alive_ || closing_) return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
                   -  + ]
     425         [ +  + ]:         256 :   if (!backpressure_active_) return wrapper::SendResult::accept();
     426                 :         156 :   return std::nullopt;
     427                 :         298 : }
     428                 :             : 
     429                 :         136 : void TcpServerSession::request_stop() {
     430                 :         136 :   std::lock_guard<std::mutex> lock(submission_mtx_);
     431                 :         136 :   send_accounting_.end(Ledger::Cause::ExplicitStop);
     432                 :         136 :   closing_ = true;
     433         [ +  + ]:         136 :   if (!wait_ended_by_) wait_ended_by_ = wrapper::SendRejection::CancelledWhileWaiting;
     434                 :         136 : }
     435                 :             : 
     436                 :         130 : void TcpServerSession::cancel_write_wait() {
     437                 :         130 :   std::lock_guard<std::mutex> lock(submission_mtx_);
     438         [ +  + ]:         130 :   if (!wait_ended_by_) wait_ended_by_ = wrapper::SendRejection::CancelledWhileWaiting;
     439                 :         130 : }
     440                 :             : 
     441                 :        2299 : bool TcpServerSession::alive() const { return alive_.load(); }
     442                 :             : 
     443                 :        1120 : wrapper::RuntimeStats TcpServerSession::stats() const {
     444                 :             :   auto snapshot =
     445                 :        3360 :       stats_.snapshot(queue_bytes_.load(std::memory_order_relaxed), pending_bytes_.load(std::memory_order_relaxed),
     446                 :        1120 :                       backpressure_active_.load(std::memory_order_relaxed));
     447                 :        1120 :   snapshot.send_accounting = send_accounting_.snapshot();
     448                 :        1120 :   return snapshot;
     449                 :             : }
     450                 :             : 
     451                 :          26 : void TcpServerSession::reset_stats() {
     452                 :          26 :   std::lock_guard<std::mutex> lock(submission_mtx_);
     453                 :          26 :   send_accounting_.reset();
     454                 :          78 :   stats_.reset(queue_bytes_.load(std::memory_order_relaxed) + pending_bytes_.load(std::memory_order_relaxed));
     455                 :          26 : }
     456                 :             : 
     457                 :         229 : void TcpServerSession::stop() { async_stop({}); }
     458                 :             : 
     459                 :         368 : void TcpServerSession::async_stop(std::function<void()> completion) {
     460                 :         368 :   std::lock_guard<std::mutex> admission_lock(submission_mtx_);
     461                 :         368 :   send_accounting_.end(Ledger::Cause::ExplicitStop);
     462                 :         368 :   closing_.store(true);
     463         [ +  + ]:         368 :   if (!wait_ended_by_) wait_ended_by_ = wrapper::SendRejection::NotReady;
     464                 :         368 :   auto self = shared_from_this();
     465                 :         368 :   net::post(strand_, [self, completion = std::move(completion)] {
     466                 :         358 :     self->on_bytes_ = nullptr;
     467                 :         358 :     self->on_bp_ = nullptr;
     468                 :         358 :     self->on_close_ = nullptr;
     469                 :         358 :     self->idle_timer_.cancel();
     470                 :         358 :     self->do_close();
     471         [ +  + ]:         358 :     if (completion) completion();
     472                 :         358 :   });
     473                 :         368 : }
     474                 :             : 
     475                 :           2 : void TcpServerSession::cancel() {
     476                 :           2 :   auto self = shared_from_this();
     477                 :           2 :   net::dispatch(strand_, [self] {
     478                 :           2 :     self->idle_timer_.cancel();
     479                 :           2 :     boost::system::error_code ec;
     480                 :             :     // Cancelling the socket via close() causes ongoing operations to complete with operation_aborted.
     481                 :             :     // Unlike stop(), this does NOT set closing_ flag immediately, allowing the
     482                 :             :     // error handler to run normally and trigger do_close() via the error path.
     483         [ +  - ]:           2 :     if (self->socket_) {
     484                 :           2 :       self->socket_->close(ec);
     485                 :             :     }
     486                 :           2 :   });
     487                 :           2 : }
     488                 :             : 
     489                 :        3354 : void TcpServerSession::start_read() {
     490   [ +  +  -  +  :        3354 :   if (closing_ || !alive_) return;
                   +  + ]
     491                 :        3346 :   auto self = shared_from_this();
     492                 :        6692 :   socket_->async_read_some(net::buffer(rx_.data(), rx_.size()),
     493                 :        6692 :                            [self](const boost::system::error_code& ec, std::size_t n) {
     494                 :        3336 :                              net::dispatch(self->strand_, [self, ec, n] {
     495   [ +  +  -  +  :        3336 :                                if (self->closing_ || !self->alive_) return;
                   +  + ]
     496         [ +  + ]:        3127 :                                if (ec) {
     497                 :         214 :                                  self->do_close();
     498                 :         214 :                                  return;
     499                 :             :                                }
     500                 :        2913 :                                self->reset_idle_timer();
     501         [ +  - ]:        2913 :                                if (n > 0) self->stats_.record_received(n);
     502                 :        2913 :                                diagnostics::invoke_callback("tcp_server_session", "on_bytes", self->on_bytes_,
     503                 :        2913 :                                                             memory::ConstByteSpan(self->rx_.data(), n));
     504                 :        2913 :                                self->start_read();
     505                 :             :                              });
     506                 :        3336 :                            });
     507                 :        3346 : }
     508                 :             : 
     509                 :         950 : void TcpServerSession::do_write() {
     510                 :             :   // Serialize the actual handoff with stop, not only the enqueue handler.
     511                 :         950 :   std::unique_lock<std::mutex> lock(submission_mtx_);
     512   [ +  +  +  -  :         950 :   if (closing_ || !alive_ || tx_.empty() || writing_) return;
          +  +  -  +  +  
                      + ]
     513                 :         730 :   writing_ = true;
     514                 :         730 :   const size_t bytes_to_write = queue_util::take_gather_batch(tx_, current_write_batch_, current_write_views_, payload);
     515                 :             :   // Admission still fences stop/loss; amortize the ledger lock over the batch.
     516                 :        9298 :   send_accounting_.begin_batch(current_write_batch_, [](const auto& item) { return item.request; });
     517                 :         730 :   auto self = shared_from_this();
     518                 :             :   // Initiation may write inline; never hold admission across the syscall.
     519                 :             :   // The batch members are strand-owned and the completion posts back to it.
     520                 :         730 :   lock.unlock();
     521                 :             :   try {
     522                 :         751 :     socket_->async_write(current_write_views_, [self, bytes_to_write](const boost::system::error_code& ec, size_t n) {
     523                 :             :       // The interface erases associated executors. Post explicitly, including
     524                 :             :       // for endpoints that complete inline during initiation.
     525                 :         709 :       net::post(self->strand_, [self, bytes_to_write, ec, n] {
     526   [ +  +  +  + ]:         709 :         const bool failed = ec || n != bytes_to_write;
     527                 :             :         {
     528                 :         709 :           std::lock_guard<std::mutex> completion_lock(self->submission_mtx_);
     529   [ +  +  -  +  :         709 :           if (self->closing_ || !self->alive_) {
                   +  + ]
     530                 :          96 :             self->current_write_batch_.clear();
     531                 :          96 :             return;
     532                 :             :           }
     533                 :         613 :           self->send_accounting_.complete_batch(
     534                 :        4778 :               self->current_write_batch_, std::min(n, bytes_to_write), [](const auto& item) { return item.request; },
     535                 :        4165 :               [](const auto& item) {
     536                 :        8330 :                 return std::visit([](const auto& b) { return queue_util::variant_buffer_size(b); }, item.buffer);
     537                 :             :               });
     538                 :         613 :           self->current_write_batch_.clear();
     539         [ +  + ]:         613 :           if (failed) {
     540                 :          28 :             self->send_accounting_.end(Ledger::Cause::ConnectionLoss);
     541                 :          28 :             self->closing_ = true;
     542         [ +  - ]:          28 :             if (!self->wait_ended_by_) self->wait_ended_by_ = wrapper::SendRejection::NotReady;
     543                 :             :           }
     544                 :         709 :         }
     545         [ +  + ]:         613 :         if (failed) {
     546                 :          28 :           self->do_close();
     547                 :          28 :           return;
     548                 :             :         }
     549                 :         585 :         queue_util::release_reserved_write_bytes(self->queue_bytes_, bytes_to_write);
     550                 :         585 :         self->stats_.record_sent(n);
     551                 :             :         // Keep writing_ set while callbacks move pending data or enqueue writes.
     552                 :         585 :         self->report_backpressure(self->queue_bytes_);
     553                 :         585 :         self->writing_ = false;
     554   [ +  -  -  +  :         585 :         if (self->closing_ || !self->alive_) return;
                   -  + ]
     555                 :         585 :         self->reset_idle_timer();
     556                 :         585 :         self->do_write();
     557                 :             :       });
     558                 :         709 :     });
     559                 :           7 :   } catch (...) {
     560                 :           7 :     lock.lock();
     561                 :           7 :     send_accounting_.end(Ledger::Cause::ConnectionLoss);
     562                 :           7 :     closing_ = true;
     563         [ +  - ]:           7 :     if (!wait_ended_by_) wait_ended_by_ = wrapper::SendRejection::NotReady;
     564                 :           7 :     lock.unlock();
     565                 :           7 :     do_close();
     566                 :           7 :   }
     567                 :         950 : }
     568                 :             : 
     569                 :         641 : void TcpServerSession::do_close() {
     570         [ +  + ]:         641 :   if (cleanup_done_.exchange(true)) return;  // Ensures cleanup runs only once
     571                 :             : 
     572                 :             :   {
     573                 :         512 :     std::lock_guard<std::mutex> lock(submission_mtx_);
     574                 :         512 :     send_accounting_.end(Ledger::Cause::ConnectionLoss);
     575         [ +  + ]:         512 :     if (!wait_ended_by_) wait_ended_by_ = wrapper::SendRejection::NotReady;
     576                 :         512 :     alive_.store(false);
     577                 :         512 :     closing_.store(true);
     578                 :         512 :   }
     579                 :             : 
     580                 :             :   // Safely invoke on_close callback
     581                 :         512 :   auto close_cb = std::move(on_close_);
     582                 :             : 
     583                 :         512 :   WIRESTEAD_LOG_INFO("tcp_server_session", "disconnect", "Client disconnected");
     584                 :         512 :   boost::system::error_code ec;
     585                 :         512 :   socket_->shutdown(tcp::socket::shutdown_both, ec);
     586                 :         512 :   socket_->close(ec);
     587                 :             : 
     588                 :             :   // Drain queued/pending writes and unconditionally clear backpressure,
     589                 :             :   // notifying any waiter directly - shares UdpChannel's terminal-drain
     590                 :             :   // helper (#434). Must run before on_bp_ is cleared below: otherwise a
     591                 :             :   // Reliable-mode caller blocked in send_to_blocking() for this client
     592                 :             :   // would never be woken up when the client disconnects via a read error
     593                 :             :   // or idle timeout (jwsung91/wirestead#452).
     594                 :             :   {
     595                 :         512 :     auto f = bp_fields();
     596                 :         512 :     queue_util::drain_and_clear_backpressure(f, on_bp_, [&]() {
     597                 :         512 :       tx_.clear();
     598                 :         512 :       queue_bytes_ = 0;
     599                 :         512 :       pending_.clear();
     600                 :         512 :       pending_bytes_ = 0;
     601                 :         512 :     });
     602                 :             :   }
     603                 :             : 
     604                 :             :   // Clear all callbacks to prevent any further invocations
     605                 :         512 :   on_bytes_ = nullptr;
     606                 :         512 :   on_bp_ = nullptr;
     607                 :         512 :   on_close_ = nullptr;
     608                 :         512 :   idle_timer_.cancel();
     609                 :             : 
     610         [ +  + ]:         512 :   if (close_cb) {
     611                 :             :     try {
     612                 :         232 :       close_cb();
     613                 :           0 :     } catch (const std::exception& e) {
     614                 :           0 :       WIRESTEAD_LOG_ERROR("tcp_server_session", "on_close", "Exception in on_close callback: " + std::string(e.what()));
     615                 :           0 :     } catch (...) {
     616                 :           0 :       WIRESTEAD_LOG_ERROR("tcp_server_session", "on_close", "Unknown exception in on_close callback");
     617                 :           0 :     }
     618                 :             :   }
     619                 :         512 : }
     620                 :             : 
     621                 :       17844 : queue_util::BackpressureFields TcpServerSession::bp_fields() {
     622                 :       17844 :   return queue_util::BackpressureFields{queue_bytes_, pending_bytes_, backpressure_active_, bp_high_,
     623                 :       17844 :                                         bp_low_,      bp_limit_,      bp_strategy_,         &write_reserve_mtx_};
     624                 :             : }
     625                 :             : 
     626                 :       12389 : void TcpServerSession::route_enqueued_buffer(TrackedBuffer&& buf, size_t added) {
     627                 :       12389 :   auto f = bp_fields();
     628                 :       12389 :   queue_util::DropAccounting dropped;
     629                 :       12389 :   auto decision = queue_util::decide_enqueue(f, added, tx_, dropped, payload, [this](const TrackedBuffer& item) {
     630                 :             :     send_accounting_.discard(item.request, Ledger::Cause::QueuePressure);
     631                 :             :   });
     632   [ -  +  -  - ]:       12389 :   if (dropped.any()) stats_.record_dropped(dropped.messages, dropped.bytes);
     633                 :             : 
     634         [ -  + ]:       12389 :   if (decision == queue_util::EnqueueDecision::Rejected) {
     635                 :           0 :     send_accounting_.discard(buf.request, Ledger::Cause::QueuePressure);
     636                 :           0 :     WIRESTEAD_LOG_ERROR("tcp_server_session", "write", "Queue limit exceeded, dropping message");
     637                 :             :     // #448: record as dropped so it's reflected in RuntimeStats instead of
     638                 :             :     // silently vanishing after being counted as accepted.
     639                 :           0 :     stats_.record_dropped(1, added);
     640                 :           0 :     queue_util::release_reserved_limit_bytes(write_reserve_mtx_, inflight_bytes_, added);
     641                 :           0 :     report_backpressure(queue_bytes_ + added);
     642                 :       10502 :     return;
     643                 :             :   }
     644         [ +  + ]:       12389 :   if (decision == queue_util::EnqueueDecision::Pending) {
     645                 :       10502 :     queue_util::commit_reserved_limit_bytes(write_reserve_mtx_, pending_bytes_, inflight_bytes_, added);
     646                 :       10502 :     pending_.emplace_back(std::move(buf));
     647                 :       10502 :     observe_queue();
     648                 :       10502 :     return;
     649                 :             :   }
     650                 :        1887 :   queue_util::commit_reserved_limit_bytes(write_reserve_mtx_, queue_bytes_, inflight_bytes_, added);
     651                 :        1887 :   tx_.emplace_back(std::move(buf));
     652                 :        1887 :   observe_queue();
     653                 :        1887 :   report_backpressure(queue_bytes_);
     654   [ +  +  +  - ]:        1887 :   if (!writing_) do_write();
     655                 :             : }
     656                 :             : 
     657                 :       19902 : void TcpServerSession::observe_queue() {
     658                 :       59706 :   stats_.observe_queue(queue_bytes_.load(std::memory_order_relaxed) + pending_bytes_.load(std::memory_order_relaxed));
     659                 :       19902 : }
     660                 :             : 
     661                 :        4944 : void TcpServerSession::report_backpressure(size_t queued_bytes) {
     662   [ +  +  -  +  :        4944 :   if (closing_ || !alive_) return;
                   +  + ]
     663                 :        4943 :   observe_queue();
     664                 :        4943 :   auto f = bp_fields();
     665                 :        4943 :   queue_util::report_backpressure(
     666                 :        4943 :       f, queued_bytes, on_bp_, stats_,
     667                 :           0 :       [&]() -> size_t {
     668                 :          98 :         const size_t moved = pending_bytes_.exchange(0);
     669         [ +  + ]:         102 :         while (!pending_.empty()) {
     670                 :           4 :           tx_.emplace_back(std::move(pending_.front()));
     671                 :           4 :           pending_.pop_front();
     672                 :             :         }
     673                 :          98 :         return moved;
     674                 :             :       },
     675                 :        4943 :       [&]() {
     676                 :          98 :         observe_queue();
     677         [ -  + ]:          98 :         if (!writing_) do_write();
     678                 :          98 :       });
     679                 :             : }
     680                 :             : 
     681                 :        4429 : void TcpServerSession::reset_idle_timer() {
     682         [ +  + ]:        4429 :   if (idle_timeout_ms_ <= 0) return;
     683                 :             : 
     684                 :             :   // Cancel any existing timer
     685                 :           3 :   idle_timer_.cancel();
     686                 :             : 
     687                 :             :   // Reset timer
     688                 :           3 :   idle_timer_.expires_after(std::chrono::milliseconds(idle_timeout_ms_));
     689                 :             : 
     690                 :           3 :   auto self = shared_from_this();
     691                 :           3 :   idle_timer_.async_wait(net::bind_executor(strand_, [self](const boost::system::error_code& ec) {
     692         [ +  + ]:           3 :     if (ec == boost::asio::error::operation_aborted) return;
     693   [ +  -  -  +  :           1 :     if (!self->alive_ || self->closing_) return;
                   -  + ]
     694                 :             : 
     695                 :           1 :     WIRESTEAD_LOG_WARNING("tcp_server_session", "timeout", "Connection idle timeout expired, closing session");
     696                 :           1 :     self->do_close();
     697                 :             :   }));
     698                 :           3 : }
     699                 :             : 
     700                 :             : }  // namespace transport
     701                 :             : }  // namespace wirestead
        

Generated by: LCOV version 2.0-1