LCOV - code coverage report
Current view: top level - wirestead/transport/base - bp_state_machine.hpp (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 100.0 % 38 38
Test Date: 2026-10-29 11:36:05 Functions: 100.0 % 24 24
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 91.7 % 36 33

             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                 :             : #pragma once
      18                 :             : 
      19                 :             : #include <atomic>
      20                 :             : #include <cstddef>
      21                 :             : 
      22                 :             : #include "wirestead/base/constants.hpp"
      23                 :             : #include "wirestead/diagnostics/callback.hpp"
      24                 :             : #include "wirestead/diagnostics/runtime_stats_counter.hpp"
      25                 :             : #include "wirestead/interface/channel.hpp"
      26                 :             : #include "wirestead/transport/base/bp_utils.hpp"
      27                 :             : 
      28                 :             : // Shared "pre-check -> route to tx/pending -> report backpressure -> drain on
      29                 :             : // terminate" state machine, factored out of ~6 independently-maintained
      30                 :             : // per-transport copies (jwsung91/wirestead#434). Deliberately non-owning/
      31                 :             : // ref-based rather than template-owning the queues themselves: buffer
      32                 :             : // element types genuinely differ across transports (UDP's TxItem carries a
      33                 :             : // destination endpoint that stream transports have no analogue for), so
      34                 :             : // this header only decides what a transport's own enqueue/write-completion
      35                 :             : // code should do, and calls back into transport-supplied hooks for the
      36                 :             : // pieces that must stay transport-specific (moving a buffer element from
      37                 :             : // pending_ into tx_, kicking off a write).
      38                 :             : namespace wirestead {
      39                 :             : namespace transport {
      40                 :             : namespace queue_util {
      41                 :             : 
      42                 :             : // The subset of a transport's Impl state this state machine needs to read
      43                 :             : // and mutate. All fields are references into the transport's own members -
      44                 :             : // this struct never owns anything and is cheap to construct per call.
      45                 :             : struct BackpressureFields {
      46                 :             :   std::atomic<size_t>& queue_bytes;
      47                 :             :   std::atomic<size_t>& pending_bytes;
      48                 :             :   std::atomic<bool>& backpressure_active;
      49                 :             :   size_t bp_high;
      50                 :             :   size_t bp_low;
      51                 :             :   size_t bp_limit;
      52                 :             :   ::wirestead::base::constants::BackpressureStrategy strategy;
      53                 :             :   std::mutex* reservation_mutex = nullptr;
      54                 :             : };
      55                 :             : 
      56                 :             : enum class EnqueueDecision {
      57                 :             :   Immediate,  // push the buffer onto tx_ and proceed to write it now
      58                 :             :   Pending,    // Backpressure already active: preserve accepted work in pending_
      59                 :             :   Rejected,   // Defensive hard-limit guard; fixed-limit reserved admission prevents this
      60                 :             : };
      61                 :             : 
      62                 :             : // Acceptance already reserved hard-limit capacity. Strategy determines caller
      63                 :             : // admission (try/refuse versus wait), never removal of accepted queue entries.
      64                 :             : // Keep the projection/observer parameters for existing queue element adapters.
      65                 :             : template <typename Deque, typename Project = IdentityProjection, typename OnDrop = IgnoreDroppedBuffer>
      66                 :       17265 : inline EnqueueDecision decide_enqueue(BackpressureFields& f, size_t added, Deque&, DropAccounting& dropped_out,
      67                 :             :                                       Project = Project{}, OnDrop = OnDrop{}) {
      68                 :       17265 :   dropped_out = {};
      69                 :       17265 :   const size_t queued = f.queue_bytes.load(std::memory_order_relaxed);
      70                 :       17265 :   const size_t pending = f.pending_bytes.load(std::memory_order_relaxed);
      71   [ +  -  +  -  :       17265 :   if (queued > f.bp_limit || pending > f.bp_limit - queued || added > f.bp_limit - queued - pending)
                   +  + ]
      72                 :           1 :     return EnqueueDecision::Rejected;
      73         [ +  + ]:       17264 :   return f.backpressure_active.load(std::memory_order_relaxed) ? EnqueueDecision::Pending : EnqueueDecision::Immediate;
      74                 :             : }
      75                 :             : 
      76                 :             : // Runs the ON / OFF-with-reflush / re-ARM state machine and fires on_bp
      77                 :             : // accordingly, mirroring UdpChannel::report_backpressure() (the
      78                 :             : // best-behaved existing copy). `flush_pending_into_tx` is called exactly
      79                 :             : // once, only on the OFF transition, and must move every element out of the
      80                 :             : // transport's own pending_ deque into tx_ (preserving whatever
      81                 :             : // transport-specific fields those elements carry) and return the number of
      82                 :             : // bytes moved; `kick_write` is called if the OFF transition leaves anything
      83                 :             : // in tx_ and nothing is currently being written.
      84                 :             : template <typename FlushFn, typename KickFn>
      85                 :      286065 : inline void report_backpressure(BackpressureFields& f, size_t queued_bytes,
      86                 :             :                                 const interface::Channel::OnBackpressure& on_bp,
      87                 :             :                                 diagnostics::RuntimeStatsCounters& stats, FlushFn&& flush_pending_into_tx,
      88                 :             :                                 KickFn&& kick_write) {
      89   [ +  +  +  +  :      286065 :   if (!f.backpressure_active.load(std::memory_order_relaxed) && queued_bytes >= f.bp_high) {
                   +  + ]
      90                 :         655 :     f.backpressure_active.store(true, std::memory_order_relaxed);
      91                 :         655 :     stats.record_backpressure_event();
      92         [ +  + ]:         655 :     if (on_bp) {
      93                 :         567 :       diagnostics::invoke_callback("transport", "on_backpressure", on_bp, queued_bytes);
      94                 :             :     }
      95                 :         655 :     return;
      96                 :             :   }
      97                 :             : 
      98   [ +  +  +  +  :      285410 :   if (f.backpressure_active.load(std::memory_order_relaxed) && queued_bytes <= f.bp_low) {
                   +  + ]
      99                 :             :     {
     100                 :             :       // Admission must not observe the gap between removing pending bytes and
     101                 :             :       // adding them to tx. Callbacks and kick_write run after releasing this lock.
     102                 :         333 :       std::unique_lock<std::mutex> reservation;
     103   [ +  +  +  - ]:         333 :       if (f.reservation_mutex) reservation = std::unique_lock<std::mutex>(*f.reservation_mutex);
     104                 :         333 :       const size_t moved = flush_pending_into_tx();
     105                 :         333 :       f.queue_bytes.fetch_add(moved, std::memory_order_relaxed);
     106                 :         333 :     }
     107                 :         333 :     f.backpressure_active.store(false, std::memory_order_relaxed);
     108                 :         333 :     stats.record_backpressure_event();
     109         [ +  + ]:         333 :     if (on_bp) {
     110                 :         274 :       diagnostics::invoke_callback("transport", "on_backpressure", on_bp,
     111                 :             :                                    queued_bytes);  // fire OFF with pre-flush queue size, matching every existing copy
     112                 :             :     }
     113                 :             : 
     114                 :         333 :     const size_t post_flush = f.queue_bytes.load(std::memory_order_relaxed);
     115         [ +  + ]:         333 :     if (post_flush >= f.bp_high) {
     116                 :          52 :       f.backpressure_active.store(true, std::memory_order_relaxed);
     117                 :          52 :       stats.record_backpressure_event();
     118         [ +  + ]:          52 :       if (on_bp) {
     119                 :          21 :         diagnostics::invoke_callback("transport", "on_backpressure", on_bp, post_flush);
     120                 :             :       }
     121                 :             :     }
     122                 :         333 :     kick_write();
     123                 :             :   }
     124                 :             : }
     125                 :             : 
     126                 :             : // Drops all queued and pending writes and clears backpressure, firing on_bp
     127                 :             : // once (with a final queued-bytes value of 0) if it was active - and doing
     128                 :             : // nothing at all if it wasn't. This is the *terminal* drain path (stop/error),
     129                 :             : // deliberately distinct from report_backpressure()'s normal-operation flush:
     130                 :             : // report_backpressure() moves pending_ back into tx_ and can immediately
     131                 :             : // re-arm backpressure_active_, which would strand a Reliable-mode sender
     132                 :             : // blocked on a condition variable forever once nothing will ever call
     133                 :             : // do_write() again (jwsung91/wirestead#427, #452). `clear_queues` must clear
     134                 :             : // every transport-specific queue (tx_, pending_, and their byte counters).
     135                 :             : template <typename ClearFn>
     136                 :        2032 : inline void drain_and_clear_backpressure(BackpressureFields& f, const interface::Channel::OnBackpressure& on_bp,
     137                 :             :                                          ClearFn&& clear_queues) {
     138                 :        2032 :   clear_queues();
     139                 :        2032 :   const bool had_backpressure = f.backpressure_active.load(std::memory_order_relaxed);
     140                 :        2032 :   f.backpressure_active.store(false, std::memory_order_relaxed);
     141         [ +  + ]:        2032 :   if (!had_backpressure) return;
     142         [ +  + ]:         170 :   if (on_bp) {
     143                 :          79 :     diagnostics::invoke_callback("transport", "on_backpressure", on_bp, 0);
     144                 :             :   }
     145                 :             : }
     146                 :             : 
     147                 :             : }  // namespace queue_util
     148                 :             : }  // namespace transport
     149                 :             : }  // namespace wirestead
        

Generated by: LCOV version 2.0-1