|
| 1 | +// Copyright (c) 2025, The Monero Project |
| 2 | +// All rights reserved. |
| 3 | +// |
| 4 | +// Redistribution and use in source and binary forms, with or without modification, are |
| 5 | +// permitted provided that the following conditions are met: |
| 6 | +// |
| 7 | +// 1. Redistributions of source code must retain the above copyright notice, this list of |
| 8 | +// conditions and the following disclaimer. |
| 9 | +// |
| 10 | +// 2. Redistributions in binary form must reproduce the above copyright notice, this list |
| 11 | +// of conditions and the following disclaimer in the documentation and/or other |
| 12 | +// materials provided with the distribution. |
| 13 | +// |
| 14 | +// 3. Neither the name of the copyright holder nor the names of its contributors may be |
| 15 | +// used to endorse or promote products derived from this software without specific |
| 16 | +// prior written permission. |
| 17 | +// |
| 18 | +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY |
| 19 | +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| 20 | +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL |
| 21 | +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 22 | +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| 23 | +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 24 | +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| 25 | +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF |
| 26 | +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 27 | + |
| 28 | +#include "rate_limiter.h" |
| 29 | + |
| 30 | +#include <algorithm> |
| 31 | +#include <boost/asio/bind_executor.hpp> |
| 32 | +#include <boost/asio/post.hpp> |
| 33 | +#include <boost/thread/lock_guard.hpp> |
| 34 | +#include <cassert> |
| 35 | +#include <limits> |
| 36 | + |
| 37 | +#include "config.h" |
| 38 | +#include "misc_log_ex.h" |
| 39 | +namespace net |
| 40 | +{ |
| 41 | + namespace |
| 42 | + { |
| 43 | + static unsigned calls_per_second_(const std::chrono::steady_clock::time_point now, const std::chrono::steady_clock::time_point start, const unsigned calls) noexcept |
| 44 | + { |
| 45 | + const auto divisor = |
| 46 | + std::chrono::duration_cast<std::chrono::seconds>(now - start).count(); |
| 47 | + return calls / ((0 < divisor) ? divisor : 1); |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + unsigned rate_limiter::calls_per_second(const std::chrono::steady_clock::time_point now) noexcept |
| 52 | + { |
| 53 | + return calls_per_second_(now, start_, calls_); |
| 54 | + } |
| 55 | + |
| 56 | + unsigned rate_limiter::adjust_window(const std::chrono::steady_clock::time_point now) noexcept |
| 57 | + { |
| 58 | + /* make sure the # of calls decreases each adjustment period; |
| 59 | + duration_cast is iffy with asio timers. */ |
| 60 | + calls_ = std::max(calls_per_second(now), unsigned(1)) - 1; |
| 61 | + start_ = now; |
| 62 | + return calls_; |
| 63 | + } |
| 64 | + |
| 65 | + void rate_limiter::do_add_calls(std::shared_ptr<rate_limiter> more) |
| 66 | + { |
| 67 | + const boost::lock_guard<boost::mutex> lock{sync_}; |
| 68 | + if (merged.insert(more).second) |
| 69 | + { |
| 70 | + const boost::lock_guard<boost::mutex> lock2{more->sync_}; |
| 71 | + if (std::numeric_limits<unsigned>::max() - calls_ < more->calls_) |
| 72 | + calls_ = std::numeric_limits<unsigned>::max(); |
| 73 | + else |
| 74 | + calls_ += more->calls_; |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + void rate_limiter::queue(std::shared_ptr<rate_limiter> self) |
| 79 | + { |
| 80 | + // duration_cast + division works better if its at least a 3 sec window |
| 81 | + static_assert(std::chrono::seconds{3} <= lws::config::rate::max_calls_window); |
| 82 | + assert(self_ != nullptr); |
| 83 | + self->timer_.expires_after(lws::config::rate::max_calls_window); |
| 84 | + self->timer_.async_wait(boost::asio::bind_executor(*self->strand_, window{self})); |
| 85 | + } |
| 86 | + |
| 87 | + rate_limiter::rate_limiter(boost::asio::io_context& io) |
| 88 | + : strand_(std::make_shared<boost::asio::io_context::strand>(io)), |
| 89 | + timer_(io), |
| 90 | + start_(), |
| 91 | + merged(), |
| 92 | + calls_(0), |
| 93 | + sync_() |
| 94 | + {} // start_ is initialized on first call to `window::operator()` |
| 95 | + |
| 96 | + void rate_limiter::window::operator()(const boost::system::error_code error) |
| 97 | + { |
| 98 | + if (self_ && error != boost::asio::error::operation_aborted) |
| 99 | + { |
| 100 | + bool check_close = false; |
| 101 | + const auto now = std::chrono::steady_clock::now(); |
| 102 | + { |
| 103 | + const boost::lock_guard<boost::mutex> lock{self_->sync_}; |
| 104 | + check_close = !self_->adjust_window(now); |
| 105 | + } |
| 106 | + |
| 107 | + if (check_close) // api calls == 0 |
| 108 | + { |
| 109 | + /* asio keeps a copy of the handler, so we have to check for |
| 110 | + `use_count() == 1` by `post`ing a routine that only has a `weak_ptr` |
| 111 | + Since both handlers run in the strand, the posted routine is queued, |
| 112 | + the handler copy is destroyed, and then this routine executes. The |
| 113 | + second shared_ptr for the strand is needed because we don't want to |
| 114 | + destroy the strand before the post executes. */ |
| 115 | + std::shared_ptr<boost::asio::io_context::strand> strand = self_->strand_; |
| 116 | + std::weak_ptr<rate_limiter> check = self_; |
| 117 | + boost::asio::post(*self_->strand_, [strand = std::move(strand), check = std::move(check)] { |
| 118 | + auto self = check.lock(); |
| 119 | + if (self) |
| 120 | + queue(std::move(self)); |
| 121 | + else |
| 122 | + MERROR("DESTROYED"); |
| 123 | + }); |
| 124 | + } |
| 125 | + else |
| 126 | + queue(std::move(self_)); |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + bool rate_limiter::rate_limited(const unsigned max_calls, const unsigned weight) |
| 131 | + { |
| 132 | + unsigned calls = 0; |
| 133 | + std::chrono::steady_clock::time_point start{}; |
| 134 | + const auto now = std::chrono::steady_clock::now(); |
| 135 | + |
| 136 | + { |
| 137 | + const boost::lock_guard<boost::mutex> lock{sync_}; |
| 138 | + calls = calls_; |
| 139 | + if (std::numeric_limits<unsigned>::max() - calls_ < weight) |
| 140 | + { |
| 141 | + calls_ = std::numeric_limits<unsigned>::max(); |
| 142 | + calls = adjust_window(now); |
| 143 | + } |
| 144 | + else |
| 145 | + calls_ += weight; |
| 146 | + start = start_; |
| 147 | + } |
| 148 | + |
| 149 | + return max_calls < calls_per_second_(now, start, calls); |
| 150 | + } |
| 151 | + |
| 152 | +} // net |
0 commit comments