FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
rate_limiter.hpp
Go to the documentation of this file.
1#pragma once
2// RateLimiter (6.4/6.5): client-side accounting of venue rate limits so we refuse to send
3// before the venue does. Two kinds of buckets:
4// * weight buckets - Binance REQUEST_WEIGHT per interval (X-MBX-USED-WEIGHT-1M),
5// Bybit per-endpoint X-Bapi-Limit / X-Bapi-Limit-Status;
6// * order-count buckets - Binance ORDERS per 10 s / per day (X-MBX-ORDER-COUNT-10S),
7// Bybit create/cancel per second per UID.
8// The venue's own counts (response headers, WS API rateLimits) correct the local estimate
9// (on_headers): raised to the venue's count, and lowered to it plus what was sent in the last
10// kHeaderLagNs, which the count may not include yet. Windows roll where the venue's do, on its
11// clock (set_clock_offset: Binance's minute and 10 s windows start on the UTC minute and second).
12// A cooldown (429 Retry-After, -1003, 10006) blocks every send until it expires, and a second one
13// soon after the first waits longer each time; a hard stop (418 IP ban) blocks until explicitly
14// cleared. Bulk requests (a start-up's depth snapshots and history queries, one per symbol) check
15// against kBulkShare of the weight, and what this limiter sent in the window (every account of
16// the process, on a shared IP) against that share paced over it (kBulkPaceFloor), so the rest of
17// the window stays with the orders and the bulk does not pile up at the start of each window. The
18// pace leaves out what other processes on the IP spent: a second process early in a minute (a warm
19// standby, a restart) would otherwise find the paced share spent by the first and wait seconds for
20// its first depth snapshot. Several accounts behind one IP (an account pool) share the IP's weight:
21// share_ip() points the weight buckets, the cooldown and the hard stop at one SharedRate per host
22// (shared_rate()), so a 429 seen by one account pauses them all; the order-count buckets stay the
23// account's.
24#include "fastmm/core/spin_mutex.hpp"
25
26#include <algorithm>
27#include <array>
28#include <cmath>
29#include <cstddef>
30#include <cstdint>
31#include <functional>
32#include <map>
33#include <memory>
34#include <mutex>
35#include <optional>
36#include <string>
37#include <string_view>
38
39namespace fastmm::venues {
40
41struct SharedRate;
42
43struct RateBucket {
44 std::uint32_t limit = 0; // 0 = unlimited
45 std::int64_t window_ns = 0; // fixed window length
46 std::uint32_t used = 0; // consumed in the current window
47 std::int64_t window_start = 0; // ns; the window rolls when now - window_start >= window_ns
48 bool active = false;
49 // The venue's clock minus the local one (RateLimiter::set_clock_offset): windows start where
50 // now + offset is a multiple of window_ns, as the venue's do.
51 std::int64_t offset = 0;
52 // What this limiter sent in the current window, which `used` includes: the venue's counts
53 // (calibrate) move `used` to the IP's, other processes' requests too, and leave this alone.
54 std::uint32_t own = 0;
55
56 // The start of the venue's window that holds `now`.
57 [[nodiscard]] std::int64_t aligned_start(std::int64_t now) const noexcept {
58 const std::int64_t r = (now + offset) % window_ns;
59 return now - (r < 0 ? r + window_ns : r);
60 }
61 void roll(std::int64_t now) noexcept {
62 if (window_ns > 0 && now - window_start >= window_ns) {
64 used = 0;
65 own = 0;
66 }
67 }
68 // A new clock offset: the window holding `now` starts where the venue's does. The count stays
69 // (the orders in it were sent within one window length of now either way).
70 void realign(std::int64_t off, std::int64_t now) noexcept {
71 offset = off;
72 if (window_ns <= 0) return;
73 if (now - window_start >= window_ns) {
74 roll(now);
75 } else {
77 }
78 }
79 [[nodiscard]] bool would_exceed(std::uint32_t add, double threshold) const noexcept {
80 if (limit == 0) return false;
81 return static_cast<double>(used + add) > static_cast<double>(limit) * threshold;
82 }
83 // The same for this limiter's own weight in the window.
84 [[nodiscard]] bool own_would_exceed(std::uint32_t add, double threshold) const noexcept {
85 if (limit == 0) return false;
86 return static_cast<double>(own + add) > static_cast<double>(limit) * threshold;
87 }
88 // The venue's count `count` for this window, `recent` sent in the last kHeaderLagNs: raised to
89 // the count, lowered to count + recent. Ignored within `lag` of the window's start, where it may
90 // be the previous window's.
91 void calibrate(std::uint32_t count,
92 std::uint32_t recent,
93 std::int64_t now,
94 std::int64_t lag) noexcept {
95 roll(now);
96 if (window_ns > 0 && now - window_start < lag) return;
97 used = std::clamp(used, count, count + recent);
98 }
99};
100
101// What was sent in the last couple of seconds (RateLimiter::kHeaderLagNs), in 125 ms slots.
102class RecentSpend {
103 public:
104 static constexpr std::size_t kSlots = 32;
105 static constexpr std::int64_t kSlotNs = 125'000'000;
106 void add(std::uint32_t n, std::int64_t now) noexcept {
107 const std::int64_t t = now / kSlotNs;
108 Slot& s = slots_[static_cast<std::size_t>(t) % kSlots];
109 if (s.t != t) s = Slot{t, 0};
110 s.n += n;
111 }
112 // Sent from `now - lag` on (whole slots: a little more, never less).
113 [[nodiscard]] std::uint32_t since(std::int64_t now, std::int64_t lag) const noexcept {
114 const std::int64_t from = (now - lag) / kSlotNs - 1;
115 std::uint32_t sum = 0;
116 for (const Slot& s : slots_) {
117 if (s.t >= from && s.t <= now / kSlotNs) sum += s.n;
118 }
119 return sum;
120 }
121
122 private:
123 struct Slot {
124 std::int64_t t = -1;
125 std::uint32_t n = 0;
126 };
127 std::array<Slot, kSlots> slots_{};
128};
129
130// Why can_send refused (RateLimiter::check): the IP or account is paused (429 cooldown, 418), a
131// weight window is at its share, or an order-count window is (RateCheck::window_ns says which).
132enum class RateRefusal : std::uint8_t { None, Paused, Weight, Orders };
133struct RateCheck {
135 std::int64_t window_ns = 0; // Orders: the window at its threshold
136 [[nodiscard]] bool ok() const noexcept { return refusal == RateRefusal::None; }
139class RateLimiter {
140 public:
141 static constexpr std::size_t kMaxBuckets = 4;
142 // The part of each weight window a bulk request may fill (can_send's `share`): with 170
143 // symbols' snapshots and history queries sent as fast as they fit, the other half of the window
144 // still takes a cancel or a REST order.
145 static constexpr double kBulkShare = 0.5;
146 // A pause asked (429) within this long after the last one ended is the same incident going on:
147 // each such pause waits kBackoffBaseNs << n on top of Retry-After, up to kBackoffMaxNs. The
148 // venue's Retry-After names a second; a client back at once earns the next 429, then the ban.
149 static constexpr std::int64_t kStreakWindowNs = 60'000'000'000;
150 static constexpr std::int64_t kBackoffBaseNs = 1'000'000'000;
151 static constexpr std::int64_t kBackoffMaxNs = 120'000'000'000;
152 // The part of kBulkShare this limiter's own weight in a window may fill before a bulk request
153 // grows with the time into the window, from this part at its start to all of it at its end: a
154 // backlog of history queries waiting for room no longer spends the share the moment the window
155 // opens, while the orders are busiest there.
156 static constexpr double kBulkPaceFloor = 0.2;
157 // How late a venue's count (on_headers) may be: what was sent in this long before it arrived
158 // may be missing from it.
159 static constexpr std::int64_t kHeaderLagNs = 2'000'000'000;
160
161 explicit RateLimiter(double threshold = 0.9) noexcept
162 : threshold_(threshold), order_threshold_(threshold) {}
164 // The IP's weight, cooldown and hard stop live in `ip` from now on (null: this limiter's own).
165 void share_ip(std::shared_ptr<SharedRate> ip) noexcept;
166
167 // Buckets are added at startup from exchangeInfo.rateLimits / the Bybit rate limit table.
168 bool add_weight_bucket(std::uint32_t limit, std::int64_t window_ns) noexcept {
169 if (ip_) return ip_add_weight_bucket(limit, window_ns);
170 return add(weight_, limit, window_ns, offset_);
171 }
172 bool add_order_bucket(std::uint32_t limit, std::int64_t window_ns) noexcept {
173 return add(orders_, limit, window_ns, offset_);
175 // The venue's clock minus the local (now) clock, ns: every window, the IP's included, rolls
176 // where the venue's does from now on. 0 (the default): windows of the local clock.
177 void set_clock_offset(std::int64_t venue_minus_local_ns, std::int64_t now) noexcept;
178 void set_threshold(double t) noexcept { threshold_ = std::clamp(t, 0.0, 1.0); }
179 // The share of each order-count limit (ORDERS buckets) can_send fills; the weight keeps
180 // threshold(). An order count past its limit costs a refused order, the weight a ban.
181 void set_order_threshold(double t) noexcept { order_threshold_ = std::clamp(t, 0.0, 1.0); }
182 [[nodiscard]] double order_threshold() const noexcept { return order_threshold_; }
183 // The share of each limit can_send fills before it refuses.
184 [[nodiscard]] double threshold() const noexcept { return threshold_; }
185 // The count a bucket of `limit` admits under that share: can_send refuses the order or request
186 // that would take `used` past it (RateBucket::would_exceed).
187 [[nodiscard]] std::int64_t cap_of(std::uint32_t limit) const noexcept {
188 return static_cast<std::int64_t>(std::floor(static_cast<double>(limit) * threshold_));
190 // cap_of for an order-count bucket.
191 [[nodiscard]] std::int64_t order_cap_of(std::uint32_t limit) const noexcept {
192 return static_cast<std::int64_t>(std::floor(static_cast<double>(limit) * order_threshold_));
194
195 // True if a request of `weight` (and one order if is_order) fits under threshold * limit
196 // in every bucket and no cooldown/hard stop is active. `share` < 1 (kBulkShare) leaves that
197 // part of each weight window to other requests.
198 [[nodiscard]] bool can_send(std::uint32_t weight,
199 std::int64_t now,
200 bool is_order = false,
201 double share = 1.0) noexcept {
202 return check(weight, now, is_order, share).ok();
203 }
204 // can_send, saying which limit refused.
205 [[nodiscard]] RateCheck check(std::uint32_t weight,
206 std::int64_t now,
207 bool is_order = false,
208 double share = 1.0) noexcept {
209 const RateRefusal w = ip_ ? ip_weight_fits(weight, now, share)
210 : weight_fits(weight, now, threshold_ * share, share < 1.0);
211 if (w != RateRefusal::None) return {w, 0};
212 if (is_order) {
213 for (RateBucket& b : orders_) {
214 if (!b.active) continue;
215 b.roll(now);
216 if (b.would_exceed(1, order_threshold_)) return {RateRefusal::Orders, b.window_ns};
217 }
218 }
219 return {};
220 }
221
222 // Local accounting after a send.
223 void on_sent(std::uint32_t weight, std::int64_t now, bool is_order = false) noexcept {
224 if (ip_) {
225 ip_on_sent(weight, now);
226 } else {
227 spend(weight_, weight, now);
228 recent_weight_.add(weight, now);
229 }
230 if (is_order) {
231 spend(orders_, 1, now);
232 recent_orders_.add(1, now);
233 }
234 }
235
236 // The venue's counts from a response (RateBucket::calibrate). `bucket` selects the window
237 // (index in insertion order); negative values are ignored (header absent).
238 void on_headers(std::int64_t used_weight,
239 std::int64_t order_count,
240 std::int64_t now,
241 std::size_t weight_bucket = 0,
242 std::size_t order_bucket = 0) noexcept {
243 if (used_weight >= 0 && ip_) {
244 ip_on_headers(used_weight, now, weight_bucket);
245 } else if (used_weight >= 0 && weight_bucket < weight_.size() &&
246 weight_[weight_bucket].active) {
247 weight_[weight_bucket].calibrate(static_cast<std::uint32_t>(used_weight),
248 recent_weight_.since(now, kHeaderLagNs),
249 now,
251 }
252 if (order_count >= 0 && order_bucket < orders_.size() && orders_[order_bucket].active) {
253 orders_[order_bucket].calibrate(static_cast<std::uint32_t>(order_count),
254 recent_orders_.since(now, kHeaderLagNs),
255 now,
257 }
258 }
259 // Bybit style: remaining requests for this endpoint (X-Bapi-Limit-Status) and the limit.
260 void on_remaining(std::int64_t limit, std::int64_t remaining, std::int64_t now) noexcept {
261 if (limit < 0 || remaining < 0 || weight_.empty() || !weight_[0].active) return;
262 RateBucket& b = weight_[0];
263 b.roll(now);
264 b.limit = static_cast<std::uint32_t>(limit);
265 b.used = static_cast<std::uint32_t>(std::max<std::int64_t>(0, limit - remaining));
266 }
267
268 // 429 / -1003 / 10006: block sends until now + retry_after, or longer when the last pause
269 // ended less than kStreakWindowNs ago (the streak). A pause asked while one is on only extends
270 // it: the answers to the requests already in flight are one incident.
271 void cooldown(std::int64_t retry_after_ns, std::int64_t now) noexcept {
272 if (ip_) {
273 ++cooldowns_; // this account's count of the pauses it was asked for
274 return ip_cooldown(retry_after_ns, now);
275 }
276 if (now >= cooldown_until_) {
277 streak_ = cooldowns_ > 0 && now - cooldown_until_ <= kStreakWindowNs ? streak_ + 1 : 0;
278 }
279 const std::int64_t backoff =
280 streak_ == 0 ? 0 : std::min(kBackoffMaxNs, kBackoffBaseNs << std::min(streak_, 20U));
281 cooldown_until_ = std::max(cooldown_until_, now + std::max(retry_after_ns, backoff));
282 ++cooldowns_;
283 }
284 [[nodiscard]] std::int64_t cooldown_until() const noexcept;
285 // Pauses in the current streak after the first.
286 [[nodiscard]] std::uint32_t streak() const noexcept;
287 [[nodiscard]] bool in_cooldown(std::int64_t now) const noexcept { return now < cooldown_until(); }
288
289 // 418: the IP is banned; stop REST entirely until an operator clears it.
290 void hard_stop() noexcept;
291 void clear_hard_stop() noexcept;
292 [[nodiscard]] bool hard_stopped() const noexcept;
293
294 [[nodiscard]] std::uint64_t cooldowns() const noexcept { return cooldowns_; }
295 // Copies of the i-th bucket (insertion order), empty past the last; a shared IP's is copied
296 // under its lock, since the other accounts' threads write it.
297 [[nodiscard]] std::optional<RateBucket> weight_bucket(std::size_t i) const noexcept;
298 [[nodiscard]] std::optional<RateBucket> order_bucket(std::size_t i) const noexcept {
299 if (i < orders_.size() && orders_[i].active) return orders_[i];
300 return std::nullopt;
301 }
302
303 private:
304 static bool add(std::array<RateBucket, kMaxBuckets>& arr,
305 std::uint32_t limit,
306 std::int64_t window_ns,
307 std::int64_t offset) noexcept {
308 for (RateBucket& b : arr) {
309 if (b.active) continue;
310 b = RateBucket{limit, window_ns, 0, 0, true, offset, 0};
311 return true;
312 }
313 return false;
314 }
315
316 static void spend(std::array<RateBucket, kMaxBuckets>& arr,
317 std::uint32_t n,
318 std::int64_t now) noexcept {
319 for (RateBucket& b : arr) {
320 if (!b.active) continue;
321 b.roll(now);
322 b.used += n;
323 b.own += n;
324 }
325 }
326 // This limiter's own weight: no cooldown or hard stop, and `weight` under threshold * limit;
327 // `paced` (a bulk request): what this limiter sent in the window, too, under the part of it the
328 // time into the window allows (kBulkPaceFloor). A shared IP calls it on SharedRate::r under the
329 // lock.
330 RateRefusal weight_fits(std::uint32_t weight,
331 std::int64_t now,
332 double threshold,
333 bool paced) noexcept {
334 if (hard_stopped_ || now < cooldown_until_) return RateRefusal::Paused;
335 for (RateBucket& b : weight_) {
336 if (!b.active) continue;
337 b.roll(now);
338 if (b.would_exceed(weight, threshold)) return RateRefusal::Weight;
339 if (paced && b.window_ns > 0) {
340 const double into =
341 static_cast<double>(now - b.window_start) / static_cast<double>(b.window_ns);
342 if (b.own_would_exceed(weight, threshold * std::clamp(into, kBulkPaceFloor, 1.0)))
343 return RateRefusal::Weight;
344 }
345 }
346 return RateRefusal::None;
347 }
348 // Adds a weight bucket unless one of the same limit and window is there: each account that
349 // shares an IP adds the same buckets, in the same order (on_headers' index).
350 bool add_weight_bucket_once(std::uint32_t limit, std::int64_t window_ns) noexcept {
351 for (const RateBucket& b : weight_) {
352 if (b.active && b.limit == limit && b.window_ns == window_ns) return true;
353 }
354 return add(weight_, limit, window_ns, offset_);
355 }
356
357 // The IP-wide parts, under SharedRate's lock (defined below).
358 RateRefusal ip_weight_fits(std::uint32_t weight, std::int64_t now, double share) noexcept;
359 void ip_on_sent(std::uint32_t weight, std::int64_t now) noexcept;
360 void ip_on_headers(std::int64_t used, std::int64_t now, std::size_t bucket) noexcept;
361 bool ip_add_weight_bucket(std::uint32_t limit, std::int64_t window_ns) noexcept;
362 void ip_cooldown(std::int64_t retry_after_ns, std::int64_t now) noexcept;
363
364 double threshold_;
365 double order_threshold_;
366 std::shared_ptr<SharedRate> ip_;
367 std::array<RateBucket, kMaxBuckets> weight_{};
368 std::array<RateBucket, kMaxBuckets> orders_{};
369 RecentSpend recent_weight_; // this limiter's own weight (a shared IP's lives in its limiter)
370 RecentSpend recent_orders_;
371 std::int64_t offset_ = 0; // set_clock_offset
372 std::int64_t cooldown_until_ = 0;
373 std::uint64_t cooldowns_ = 0;
374 std::uint32_t streak_ = 0;
375 bool hard_stopped_ = false;
376};
377
378// One IP's request weight, shared by every account that sends from it (RateLimiter::share_ip).
379struct SharedRate {
380 // Every order of every account on the IP takes it twice (ip_weight_fits, ip_on_sent), from
381 // their network threads at once when a requote spreads over the pool: a spinning lock, so a
382 // waiter does not sleep in the kernel for a section of a few dozen nanoseconds.
383 SpinMutex m;
384 RateLimiter r{1.0}; // its weight buckets, cooldown and hard stop; never shared itself
387// The key of one IP's weight at one venue: the connector kind, its REST host and the address its
388// connections leave from (net::source_label; empty: the host's default), so accounts sending
389// from different addresses count apart and those sending from the same one count together.
390[[nodiscard]] inline std::string ip_weight_key(std::string_view kind,
391 std::string_view rest_url,
392 std::string_view source) {
393 std::string key(kind);
394 key += ' ';
395 key += rest_url;
396 if (!source.empty()) {
397 key += " from ";
398 key += source;
399 }
400 return key;
401}
402
403// The SharedRate of `key` (ip_weight_key), made on first use; process-wide.
404inline std::shared_ptr<SharedRate> shared_rate(std::string_view key) {
405 static std::mutex m;
406 static std::map<std::string, std::shared_ptr<SharedRate>, std::less<>> by_key;
407 const std::lock_guard<std::mutex> lock(m);
408 auto it = by_key.find(key);
409 if (it == by_key.end())
410 it = by_key.emplace(std::string(key), std::make_shared<SharedRate>()).first;
411 return it->second;
412}
413
414inline void RateLimiter::share_ip(std::shared_ptr<SharedRate> ip) noexcept {
415 if (ip) {
416 const std::lock_guard<SpinMutex> lock(ip->m);
417 for (const RateBucket& b : weight_) {
418 if (b.active) static_cast<void>(ip->r.add_weight_bucket_once(b.limit, b.window_ns));
419 }
420 }
421 ip_ = std::move(ip);
422}
423inline RateRefusal RateLimiter::ip_weight_fits(std::uint32_t weight,
424 std::int64_t now,
425 double share) noexcept {
426 const std::lock_guard<SpinMutex> lock(ip_->m);
427 return ip_->r.weight_fits(weight, now, threshold_ * share, share < 1.0);
428}
429inline void RateLimiter::ip_on_sent(std::uint32_t weight, std::int64_t now) noexcept {
430 const std::lock_guard<SpinMutex> lock(ip_->m);
431 spend(ip_->r.weight_, weight, now);
432 ip_->r.recent_weight_.add(weight, now);
433}
434inline void RateLimiter::ip_on_headers(std::int64_t used,
435 std::int64_t now,
436 std::size_t bucket) noexcept {
437 // The IP's count when the venue answered one account: every account's requests of the last
438 // kHeaderLagNs may be missing from it, so it lowers the shared estimate only to the count plus
439 // those (an older answer lowering it further would let their concurrent burst through).
440 const std::lock_guard<SpinMutex> lock(ip_->m);
441 auto& w = ip_->r.weight_;
442 if (bucket >= w.size() || !w[bucket].active) return;
443 w[bucket].calibrate(static_cast<std::uint32_t>(used),
444 ip_->r.recent_weight_.since(now, kHeaderLagNs),
445 now,
446 kHeaderLagNs);
447}
448inline void RateLimiter::set_clock_offset(std::int64_t venue_minus_local_ns,
449 std::int64_t now) noexcept {
450 offset_ = venue_minus_local_ns;
451 for (RateBucket& b : orders_) {
452 if (b.active) b.realign(offset_, now);
453 }
454 if (!ip_) {
455 for (RateBucket& b : weight_) {
456 if (b.active) b.realign(offset_, now);
457 }
458 return;
459 }
460 const std::lock_guard<SpinMutex> lock(ip_->m);
461 ip_->r.offset_ = offset_;
462 for (RateBucket& b : ip_->r.weight_) {
463 if (b.active) b.realign(offset_, now);
464 }
465}
466inline bool RateLimiter::ip_add_weight_bucket(std::uint32_t limit,
467 std::int64_t window_ns) noexcept {
468 const std::lock_guard<SpinMutex> lock(ip_->m);
469 return ip_->r.add_weight_bucket_once(limit, window_ns);
470}
471inline void RateLimiter::ip_cooldown(std::int64_t retry_after_ns, std::int64_t now) noexcept {
472 const std::lock_guard<SpinMutex> lock(ip_->m);
473 ip_->r.cooldown(retry_after_ns, now);
474}
475inline std::int64_t RateLimiter::cooldown_until() const noexcept {
476 if (!ip_) return cooldown_until_;
477 const std::lock_guard<SpinMutex> lock(ip_->m);
478 return ip_->r.cooldown_until_;
479}
480inline void RateLimiter::hard_stop() noexcept {
481 if (!ip_) {
482 hard_stopped_ = true;
483 return;
484 }
485 const std::lock_guard<SpinMutex> lock(ip_->m);
486 ip_->r.hard_stopped_ = true;
487}
488inline void RateLimiter::clear_hard_stop() noexcept {
489 if (!ip_) {
490 hard_stopped_ = false;
491 return;
492 }
493 const std::lock_guard<SpinMutex> lock(ip_->m);
494 ip_->r.hard_stopped_ = false;
495}
496inline std::optional<RateBucket> RateLimiter::weight_bucket(std::size_t i) const noexcept {
497 if (!ip_) {
498 if (i < weight_.size() && weight_[i].active) return weight_[i];
499 return std::nullopt;
500 }
501 const std::lock_guard<SpinMutex> lock(ip_->m);
502 return ip_->r.weight_bucket(i);
503}
504inline std::uint32_t RateLimiter::streak() const noexcept {
505 if (!ip_) return streak_;
506 const std::lock_guard<SpinMutex> lock(ip_->m);
507 return ip_->r.streak_;
508}
509inline bool RateLimiter::hard_stopped() const noexcept {
510 if (!ip_) return hard_stopped_;
511 const std::lock_guard<SpinMutex> lock(ip_->m);
512 return ip_->r.hard_stopped_;
513}
514
515// A blocking start-up request (reference data, account settings, one per symbol) waits for its
516// weight to fit the bulk share instead of spending what the connections about to open will need;
517// at most `max_wait_ns`, since nothing trades yet and a window is a minute.
518template <class Sleep>
520 std::uint32_t weight,
521 std::int64_t now_ns,
522 std::int64_t max_wait_ns,
523 Sleep&& sleep_ns) noexcept {
524 const std::int64_t until = now_ns + max_wait_ns;
525 std::int64_t now = now_ns;
526 while (now < until && !r.can_send(weight, now, false, RateLimiter::kBulkShare)) {
527 constexpr std::int64_t kStep = 100'000'000;
528 sleep_ns(kStep);
529 now += kStep;
530 }
531}
532
533} // namespace fastmm::venues
Definition rate_limiter.hpp:141
static constexpr std::size_t kMaxBuckets
Definition rate_limiter.hpp:143
void hard_stop() noexcept
418: the IP is banned; stop REST entirely until an operator clears it.
Definition rate_limiter.hpp:482
bool add_weight_bucket(std::uint32_t limit, std::int64_t window_ns) noexcept
Buckets are added at startup from exchangeInfo.rateLimits / the Bybit rate limit table.
Definition rate_limiter.hpp:170
static constexpr std::int64_t kStreakWindowNs
A pause asked (429) within this long after the last one ended is the same incident going on: each suc...
Definition rate_limiter.hpp:151
static constexpr double kBulkShare
The part of each weight window a bulk request may fill (can_send's share): with 170 symbols' snapshot...
Definition rate_limiter.hpp:147
bool can_send(std::uint32_t weight, std::int64_t now, bool is_order=false, double share=1.0) noexcept
True if a request of weight (and one order if is_order) fits under threshold * limit in every bucket ...
Definition rate_limiter.hpp:200
bool add_order_bucket(std::uint32_t limit, std::int64_t window_ns) noexcept
Definition rate_limiter.hpp:174
bool in_cooldown(std::int64_t now) const noexcept
Definition rate_limiter.hpp:289
void clear_hard_stop() noexcept
Definition rate_limiter.hpp:490
std::uint64_t cooldowns() const noexcept
Definition rate_limiter.hpp:296
void set_threshold(double t) noexcept
Definition rate_limiter.hpp:180
void on_sent(std::uint32_t weight, std::int64_t now, bool is_order=false) noexcept
Local accounting after a send.
Definition rate_limiter.hpp:225
static constexpr double kBulkPaceFloor
The part of kBulkShare this limiter's own weight in a window may fill before a bulk request grows wit...
Definition rate_limiter.hpp:158
void on_remaining(std::int64_t limit, std::int64_t remaining, std::int64_t now) noexcept
Bybit style: remaining requests for this endpoint (X-Bapi-Limit-Status) and the limit.
Definition rate_limiter.hpp:262
double threshold() const noexcept
The share of each limit can_send fills before it refuses.
Definition rate_limiter.hpp:186
std::int64_t cap_of(std::uint32_t limit) const noexcept
The count a bucket of limit admits under that share: can_send refuses the order or request that would...
Definition rate_limiter.hpp:189
std::optional< RateBucket > order_bucket(std::size_t i) const noexcept
Definition rate_limiter.hpp:300
void set_clock_offset(std::int64_t venue_minus_local_ns, std::int64_t now) noexcept
The venue's clock minus the local (now) clock, ns: every window, the IP's included,...
Definition rate_limiter.hpp:450
bool hard_stopped() const noexcept
Definition rate_limiter.hpp:511
void cooldown(std::int64_t retry_after_ns, std::int64_t now) noexcept
429 / -1003 / 10006: block sends until now + retry_after, or longer when the last pause ended less th...
Definition rate_limiter.hpp:273
std::uint32_t streak() const noexcept
Pauses in the current streak after the first.
Definition rate_limiter.hpp:506
RateCheck check(std::uint32_t weight, std::int64_t now, bool is_order=false, double share=1.0) noexcept
can_send, saying which limit refused.
Definition rate_limiter.hpp:207
static constexpr std::int64_t kBackoffBaseNs
Definition rate_limiter.hpp:152
static constexpr std::int64_t kBackoffMaxNs
Definition rate_limiter.hpp:153
std::optional< RateBucket > weight_bucket(std::size_t i) const noexcept
Copies of the i-th bucket (insertion order), empty past the last; a shared IP's is copied under its l...
Definition rate_limiter.hpp:498
static constexpr std::int64_t kHeaderLagNs
How late a venue's count (on_headers) may be: what was sent in this long before it arrived may be mis...
Definition rate_limiter.hpp:161
void on_headers(std::int64_t used_weight, std::int64_t order_count, std::int64_t now, std::size_t weight_bucket=0, std::size_t order_bucket=0) noexcept
The venue's counts from a response (RateBucket::calibrate).
Definition rate_limiter.hpp:240
void set_order_threshold(double t) noexcept
The share of each order-count limit (ORDERS buckets) can_send fills; the weight keeps threshold().
Definition rate_limiter.hpp:183
std::int64_t cooldown_until() const noexcept
Definition rate_limiter.hpp:477
double order_threshold() const noexcept
Definition rate_limiter.hpp:184
std::int64_t order_cap_of(std::uint32_t limit) const noexcept
cap_of for an order-count bucket.
Definition rate_limiter.hpp:193
void share_ip(std::shared_ptr< SharedRate > ip) noexcept
The IP's weight, cooldown and hard stop live in ip from now on (null: this limiter's own).
Definition rate_limiter.hpp:416
static constexpr std::int64_t kSlotNs
Definition rate_limiter.hpp:107
static constexpr std::size_t kSlots
Definition rate_limiter.hpp:106
std::uint32_t since(std::int64_t now, std::int64_t lag) const noexcept
Sent from now - lag on (whole slots: a little more, never less).
Definition rate_limiter.hpp:115
void add(std::uint32_t n, std::int64_t now) noexcept
Definition rate_limiter.hpp:108
Definition venue.hpp:38
std::string ip_weight_key(std::string_view kind, std::string_view rest_url, std::string_view source)
The key of one IP's weight at one venue: the connector kind, its REST host and the address its connec...
Definition rate_limiter.hpp:392
void wait_for_weight(RateLimiter &r, std::uint32_t weight, std::int64_t now_ns, std::int64_t max_wait_ns, Sleep &&sleep_ns) noexcept
A blocking start-up request (reference data, account settings, one per symbol) waits for its weight t...
Definition rate_limiter.hpp:521
RateRefusal
Why can_send refused (RateLimiter::check): the IP or account is paused (429 cooldown,...
Definition rate_limiter.hpp:134
@ None
not a multicast venue
std::shared_ptr< SharedRate > shared_rate(std::string_view key)
The SharedRate of key (ip_weight_key), made on first use; process-wide.
Definition rate_limiter.hpp:406
Definition rate_limiter.hpp:45
void calibrate(std::uint32_t count, std::uint32_t recent, std::int64_t now, std::int64_t lag) noexcept
The venue's count count for this window, recent sent in the last kHeaderLagNs: raised to the count,...
Definition rate_limiter.hpp:93
void roll(std::int64_t now) noexcept
Definition rate_limiter.hpp:63
bool would_exceed(std::uint32_t add, double threshold) const noexcept
Definition rate_limiter.hpp:81
std::uint32_t own
What this limiter sent in the current window, which used includes: the venue's counts (calibrate) mov...
Definition rate_limiter.hpp:56
std::int64_t offset
The venue's clock minus the local one (RateLimiter::set_clock_offset): windows start where now + offs...
Definition rate_limiter.hpp:53
bool active
Definition rate_limiter.hpp:50
std::int64_t window_start
ns; the window rolls when now - window_start >= window_ns
Definition rate_limiter.hpp:49
void realign(std::int64_t off, std::int64_t now) noexcept
A new clock offset: the window holding now starts where the venue's does.
Definition rate_limiter.hpp:72
bool own_would_exceed(std::uint32_t add, double threshold) const noexcept
The same for this limiter's own weight in the window.
Definition rate_limiter.hpp:86
std::uint32_t used
consumed in the current window
Definition rate_limiter.hpp:48
std::int64_t aligned_start(std::int64_t now) const noexcept
The start of the venue's window that holds now.
Definition rate_limiter.hpp:59
std::int64_t window_ns
fixed window length
Definition rate_limiter.hpp:47
std::uint32_t limit
0 = unlimited
Definition rate_limiter.hpp:46
Definition rate_limiter.hpp:135
bool ok() const noexcept
Definition rate_limiter.hpp:138
RateRefusal refusal
Definition rate_limiter.hpp:136
std::int64_t window_ns
Orders: the window at its threshold.
Definition rate_limiter.hpp:137
SpinMutex m
Every order of every account on the IP takes it twice (ip_weight_fits, ip_on_sent),...
Definition rate_limiter.hpp:385
RateLimiter r
its weight buckets, cooldown and hard stop; never shared itself
Definition rate_limiter.hpp:386