Kurlyk
Loading...
Searching...
No Matches
HttpRateLimiter.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef KURLYK_HEADER_KURLYK_HTTP_HTTP_REQUEST_MANAGER_HTTP_RATE_LIMITER_HPP_INCLUDED
3#define KURLYK_HEADER_KURLYK_HTTP_HTTP_REQUEST_MANAGER_HTTP_RATE_LIMITER_HPP_INCLUDED
4
7
8namespace kurlyk {
9
23 public:
30 HttpRateLimitHandlePtr create_limit_handle(long requests_per_period, long period_ms, bool sequential = false) {
31 std::lock_guard<std::mutex> lock(m_mutex);
32
33 const long id = m_next_id++;
34
35 m_limits[id] = LimitData{
36 requests_per_period,
37 period_ms,
38 sequential,
39 false,
40 {}
41 };
42
43 // Do not use make_shared here: private constructor access through
44 // std::make_shared can be problematic on some compilers.
47 id,
48 [this](long limit_id) {
49 remove_limit_internal(limit_id);
50 }
51 )
52 );
53
54 m_owned_handles[id] = handle;
55 return handle;
56 }
57
63 long create_limit(long requests_per_period, long period_ms) {
64 const auto handle = create_limit_handle(requests_per_period, period_ms);
65 return handle ? handle->id() : 0;
66 }
67
76 bool remove_limit(long limit_id) {
77 HttpRateLimitHandlePtr retired_handle;
78
79 std::unique_lock<std::mutex> lock(m_mutex);
80
81 auto it = m_owned_handles.find(limit_id);
82 if (it == m_owned_handles.end()) {
83 return false;
84 }
85
86 retired_handle = std::move(it->second);
87 m_owned_handles.erase(it);
88
89 lock.unlock();
90
91 // retired_handle is destroyed after m_mutex is released.
92 // If it is the last shared_ptr, its destructor calls
93 // remove_limit_internal(limit_id).
94 return true;
95 }
96
101 return handle ? remove_limit(handle->id()) : false;
102 }
103
108 std::lock_guard<std::mutex> lock(m_mutex);
109
110 auto it = m_owned_handles.find(limit_id);
111 if (it == m_owned_handles.end()) {
112 return HttpRateLimitHandlePtr();
113 }
114
115 return it->second;
116 }
117
129 const HttpRateLimitHandlePtr& general_limit,
130 const HttpRateLimitHandlePtr& specific_limit,
131 uint64_t in_flight_token,
132 const std::string& general_key,
133 const std::string& specific_key
134 ) {
135 std::lock_guard<std::mutex> lock(m_mutex);
136
137 const long general_id = general_limit ? general_limit->id() : 0;
138 const long specific_id = specific_limit ? specific_limit->id() : 0;
139
140 auto general_it = general_id != 0 ? m_limits.find(general_id) : m_limits.end();
141 auto specific_it = specific_id != 0 ? m_limits.find(specific_id) : m_limits.end();
142
143 if (general_it == m_limits.end() && specific_it == m_limits.end()) {
144 return true;
145 }
146
147 const auto now = std::chrono::steady_clock::now();
148
149 bool general_limit_allowed = true;
150 bool specific_limit_allowed = true;
151
152 if (general_it != m_limits.end()) {
153 if (general_it->second.removed) {
154 general_limit_allowed = true;
155 } else {
156 general_limit_allowed = can_pass(general_it->second, general_key, in_flight_token, now);
157 }
158 }
159
160 if (specific_it != m_limits.end()) {
161 if (specific_it->second.removed) {
162 specific_limit_allowed = true;
163 } else {
164 specific_limit_allowed = can_pass(specific_it->second, specific_key, in_flight_token, now);
165 }
166 }
167
168 if (!general_limit_allowed || !specific_limit_allowed) {
169 return false;
170 }
171
172 const bool same_limit =
173 general_id != 0 &&
174 general_id == specific_id &&
175 general_key == specific_key;
176
177 if (general_it != m_limits.end() && !general_it->second.removed) {
178 commit_limit(general_it->second, general_key, in_flight_token, now);
179 }
180
181 if (!same_limit && specific_it != m_limits.end() && !specific_it->second.removed) {
182 commit_limit(specific_it->second, specific_key, in_flight_token, now);
183 }
184
185 // Periodic garbage collection of stale keys.
186 if ((++m_gc_counter & 63) == 0) {
187 gc_stale_keys(now);
188 }
189
190 return true;
191 }
192
195 const HttpRateLimitHandlePtr& general_limit,
196 const HttpRateLimitHandlePtr& specific_limit
197 ) {
198 return allow_request(general_limit, specific_limit, 0, std::string(), std::string());
199 }
200
205 bool allow_request(long general_rate_limit_id, long specific_rate_limit_id) {
206 return allow_request(
207 get_limit(general_rate_limit_id),
208 get_limit(specific_rate_limit_id),
209 0,
210 std::string(),
211 std::string()
212 );
213 }
214
224 const HttpRateLimitHandlePtr& general_limit,
225 const HttpRateLimitHandlePtr& specific_limit,
226 uint64_t in_flight_token,
227 const std::string& general_key,
228 const std::string& specific_key) {
229 if (in_flight_token == 0) return;
230
231 std::lock_guard<std::mutex> lock(m_mutex);
232
233 const long general_id = general_limit ? general_limit->id() : 0;
234 const long specific_id = specific_limit ? specific_limit->id() : 0;
235
236 const bool same_limit =
237 general_id != 0 &&
238 general_id == specific_id &&
239 general_key == specific_key;
240
241 auto general_it = general_id != 0 ? m_limits.find(general_id) : m_limits.end();
242 auto specific_it = specific_id != 0 ? m_limits.find(specific_id) : m_limits.end();
243
244 if (general_it != m_limits.end() && general_it->second.sequential) {
245 release_key(general_it->second, general_key, in_flight_token);
246 }
247
248 if (!same_limit && specific_it != m_limits.end() && specific_it->second.sequential) {
249 release_key(specific_it->second, specific_key, in_flight_token);
250 }
251 }
252
255 const HttpRateLimitHandlePtr& general_limit,
256 const HttpRateLimitHandlePtr& specific_limit,
257 uint64_t in_flight_token) {
258 release_request(general_limit, specific_limit, in_flight_token, std::string(), std::string());
259 }
260
270 template<typename Duration = std::chrono::milliseconds>
272 const HttpRateLimitHandlePtr& general_limit,
273 const HttpRateLimitHandlePtr& specific_limit,
274 const std::string& general_key,
275 const std::string& specific_key
276 ) {
277 std::lock_guard<std::mutex> lock(m_mutex);
278
279 const auto now = std::chrono::steady_clock::now();
281 result.duration = Duration{0};
282 result.sequential_blocked = false;
283
284 const long general_id = general_limit ? general_limit->id() : 0;
285 const long specific_id = specific_limit ? specific_limit->id() : 0;
286
287 auto it = general_id != 0 ? m_limits.find(general_id) : m_limits.end();
288 if (it != m_limits.end()) {
289 const Duration general_delay = time_until_limit_allows<Duration>(it->second, general_key, now);
290 result.duration = (std::max)(result.duration, general_delay);
291 if (general_delay == (Duration::max)()) {
292 result.sequential_blocked = true;
293 }
294 }
295
296 it = specific_id != 0 ? m_limits.find(specific_id) : m_limits.end();
297 if (it != m_limits.end()) {
298 const Duration specific_delay = time_until_limit_allows<Duration>(it->second, specific_key, now);
299 result.duration = (std::max)(result.duration, specific_delay);
300 if (specific_delay == (Duration::max)()) {
301 result.sequential_blocked = true;
302 }
303 }
304
305 return result;
306 }
307
309 template<typename Duration = std::chrono::milliseconds>
311 const HttpRateLimitHandlePtr& general_limit,
312 const HttpRateLimitHandlePtr& specific_limit
313 ) {
314 return time_until_next_allowed<Duration>(general_limit, specific_limit, std::string(), std::string());
315 }
316
323 template<typename Duration = std::chrono::milliseconds>
324 RateLimitDelay<Duration> time_until_next_allowed(long general_rate_limit_id, long specific_rate_limit_id) {
326 get_limit(general_rate_limit_id),
327 get_limit(specific_rate_limit_id),
328 std::string(),
329 std::string()
330 );
331 }
332
340 template<typename Duration = std::chrono::milliseconds>
342 std::lock_guard<std::mutex> lock(m_mutex);
343
344 const auto now = std::chrono::steady_clock::now();
345
347 result.duration = Duration{0};
348 result.sequential_blocked = false;
349
350 Duration min_delay = (Duration::max)();
351 bool has_positive_delay = false;
352
353 for (const auto& pair : m_limits) {
354 const auto& limit = pair.second;
355 for (const auto& key_pair : limit.keys) {
356 const Duration delay = time_until_key_allows<Duration>(limit, key_pair.second, now);
357 if (delay.count() <= 0) {
358 continue;
359 }
360 has_positive_delay = true;
361 if (delay < min_delay) {
362 min_delay = delay;
363 }
364 }
365 }
366
367 if (has_positive_delay) {
368 result.duration = min_delay;
369 result.sequential_blocked = (min_delay == (Duration::max)());
370 }
371
372 return result;
373 }
374
375 private:
376 using time_point_t = std::chrono::steady_clock::time_point;
377
380 struct KeyState {
381 long count = 0;
383 std::unordered_set<uint64_t> in_flight_tokens;
384 };
385
388 struct LimitData {
390 long period_ms = 0;
391 bool sequential = false;
392 bool removed = false;
393 std::unordered_map<std::string, KeyState> keys;
394
396
398 long request_limit,
399 long period,
400 bool is_sequential,
401 bool is_removed,
402 const std::unordered_map<std::string, KeyState>& key_states)
403 : requests_per_period(request_limit),
404 period_ms(period),
405 sequential(is_sequential),
406 removed(is_removed),
407 keys(key_states) {}
408 };
409
418 bool remove_limit_internal(long limit_id) {
419 std::lock_guard<std::mutex> lock(m_mutex);
420 auto it = m_limits.find(limit_id);
421 if (it == m_limits.end()) {
422 return false;
423 }
424 auto& limit = it->second;
425 limit.removed = true;
426 // Erase only if no keys hold runtime state.
427 if (!limit.keys.empty()) {
428 return false;
429 }
430 return m_limits.erase(limit_id) > 0;
431 }
432
434 KeyState& get_key_state(LimitData& limit, const std::string& key) {
435 return limit.keys[key];
436 }
437
439 const KeyState* find_key_state(const LimitData& limit, const std::string& key) const {
440 auto it = limit.keys.find(key);
441 if (it == limit.keys.end()) {
442 return nullptr;
443 }
444 return &it->second;
445 }
446
448 bool can_pass(const LimitData& limit, const std::string& key, uint64_t token, const time_point_t& now) const {
449 const KeyState* state = find_key_state(limit, key);
450 if (!state) {
451 // No state yet: only need to check the base limit parameters.
452 if (limit.requests_per_period == 0) {
453 return true;
454 }
455 return true; // count is 0, so always under limit.
456 }
457 if (limit.sequential && token != 0) {
458 if (!state->in_flight_tokens.empty() &&
459 state->in_flight_tokens.count(token) == 0) {
460 return false;
461 }
462 }
463 return check_key(limit, *state, now);
464 }
465
466 bool check_key(const LimitData& limit_data, const KeyState& state, const time_point_t& now) const {
467 if (limit_data.requests_per_period == 0) {
468 return true;
469 }
470
471 const auto elapsed_time =
472 std::chrono::duration_cast<std::chrono::milliseconds>(
473 now - state.start_time
474 );
475
476 if (elapsed_time.count() >= limit_data.period_ms) {
477 return true;
478 }
479
480 return state.count < limit_data.requests_per_period;
481 }
482
484 void commit_limit(LimitData& limit, const std::string& key, uint64_t token, const time_point_t& now) {
485 KeyState& state = get_key_state(limit, key);
486 if (limit.sequential && token != 0) {
487 state.in_flight_tokens.insert(token);
488 }
489
490 // Retry attempts may reuse the same in-flight token to avoid self-blocking
491 // sequential limits, but each actual HTTP attempt still consumes the
492 // count-based rate limit.
493 update_key(limit, state, now);
494 }
495
496 void update_key(LimitData& limit_data, KeyState& state, const time_point_t& now) {
497 if (limit_data.requests_per_period == 0) {
498 return;
499 }
500
501 const auto elapsed_time =
502 std::chrono::duration_cast<std::chrono::milliseconds>(
503 now - state.start_time
504 );
505
506 if (elapsed_time.count() >= limit_data.period_ms) {
507 state.start_time = now;
508 state.count = 0;
509 }
510
511 ++state.count;
512 }
513
515 void release_key(LimitData& limit, const std::string& key, uint64_t token) {
516 auto it = limit.keys.find(key);
517 if (it == limit.keys.end()) {
518 return;
519 }
520 auto& state = it->second;
521 state.in_flight_tokens.erase(token);
522 if (state.in_flight_tokens.empty() && state.count == 0) {
523 limit.keys.erase(it);
524 }
525 if (limit.removed && limit.keys.empty()) {
526 // We cannot erase `limit` here because we are iterating or
527 // the caller holds a reference. Deferred to remove_limit_internal
528 // or next gc pass. In practice remove_limit_internal already
529 // tries; here we rely on gc_stale_keys or the next remove_limit_internal.
530 }
531 }
532
533 template<typename Duration>
535 const LimitData& limit,
536 const std::string& key,
537 const time_point_t& now
538 ) const {
539 const KeyState* state = find_key_state(limit, key);
540 if (!state) {
541 // No state means no in-flight tokens and count is 0.
542 if (limit.sequential) {
543 return Duration{0};
544 }
545 if (limit.requests_per_period == 0) {
546 return Duration{0};
547 }
548 return Duration{0};
549 }
550 return time_until_key_allows<Duration>(limit, *state, now);
551 }
552
553 template<typename Duration>
555 const LimitData& limit,
556 const KeyState& state,
557 const time_point_t& now
558 ) const {
559 if (limit.sequential &&
560 !state.in_flight_tokens.empty()) {
561 return (Duration::max)();
562 }
563
564 if (limit.requests_per_period == 0) {
565 return Duration{0};
566 }
567
568 const auto elapsed =
569 std::chrono::duration_cast<Duration>(now - state.start_time);
570
571 const auto period_duration =
572 std::chrono::duration_cast<Duration>(
573 std::chrono::milliseconds(limit.period_ms)
574 );
575
576 if (elapsed >= period_duration ||
577 state.count < limit.requests_per_period) {
578 return Duration{0};
579 }
580
581 return period_duration - elapsed;
582 }
583
585 void gc_stale_keys(const time_point_t& now) {
586 for (auto limit_it = m_limits.begin(); limit_it != m_limits.end(); ) {
587 auto& limit = limit_it->second;
588 for (auto key_it = limit.keys.begin(); key_it != limit.keys.end(); ) {
589 const auto& state = key_it->second;
590 if (state.in_flight_tokens.empty() && state.count == 0) {
591 key_it = limit.keys.erase(key_it);
592 } else if (state.in_flight_tokens.empty()) {
593 const auto elapsed =
594 std::chrono::duration_cast<std::chrono::milliseconds>(
595 now - state.start_time
596 );
597 if (elapsed.count() >= limit.period_ms) {
598 key_it = limit.keys.erase(key_it);
599 } else {
600 ++key_it;
601 }
602 } else {
603 ++key_it;
604 }
605 }
606
607 if (limit.removed && limit.keys.empty()) {
608 limit_it = m_limits.erase(limit_it);
609 } else {
610 ++limit_it;
611 }
612 }
613 }
614
615 private:
616 mutable std::mutex m_mutex;
617
618 long m_next_id = 1;
619
623 std::unordered_map<long, LimitData> m_limits;
624
629 std::unordered_map<long, HttpRateLimitHandlePtr> m_owned_handles;
630
631 size_t m_gc_counter = 0;
632 };
633
634} // namespace kurlyk
635
636#endif // KURLYK_HEADER_KURLYK_HTTP_HTTP_REQUEST_MANAGER_HTTP_RATE_LIMITER_HPP_INCLUDED
RAII handle that owns a registered HTTP rate-limit ID.
Manages rate limits for HTTP requests.
std::unordered_map< long, LimitData > m_limits
Physically alive limit data.
void gc_stale_keys(const time_point_t &now)
Erases keys that are empty or whose period has expired.
bool remove_limit(long limit_id)
Releases manager-owned handle for the specified limit ID.
HttpRateLimitHandlePtr get_limit(long limit_id)
Returns manager-owned handle by ID.
bool allow_request(long general_rate_limit_id, long specific_rate_limit_id)
Legacy API: checks if request is allowed by two limit IDs.
std::chrono::steady_clock::time_point time_point_t
HttpRateLimitHandlePtr create_limit_handle(long requests_per_period, long period_ms, bool sequential=false)
Creates a new rate limit and returns its RAII handle.
bool check_key(const LimitData &limit_data, const KeyState &state, const time_point_t &now) const
bool remove_limit_internal(long limit_id)
Marks a limit as removed and erases it only when no key state remains.
void commit_limit(LimitData &limit, const std::string &key, uint64_t token, const time_point_t &now)
Commits in-flight token and count/period state after a successful can_pass.
bool allow_request(const HttpRateLimitHandlePtr &general_limit, const HttpRateLimitHandlePtr &specific_limit)
Handle-based overload without explicit token or keys (token = 0, keys empty).
bool can_pass(const LimitData &limit, const std::string &key, uint64_t token, const time_point_t &now) const
Checks if a key within a limit can pass both sequential and count/period constraints.
RateLimitDelay< Duration > time_until_next_allowed(long general_rate_limit_id, long specific_rate_limit_id)
Legacy API: calculates delay by limit IDs.
void update_key(LimitData &limit_data, KeyState &state, const time_point_t &now)
void release_request(const HttpRateLimitHandlePtr &general_limit, const HttpRateLimitHandlePtr &specific_limit, uint64_t in_flight_token, const std::string &general_key, const std::string &specific_key)
Releases in-flight tokens for sequential rate limits.
RateLimitDelay< Duration > time_until_any_limit_allows()
Finds the shortest delay among all physically alive limits.
bool allow_request(const HttpRateLimitHandlePtr &general_limit, const HttpRateLimitHandlePtr &specific_limit, uint64_t in_flight_token, const std::string &general_key, const std::string &specific_key)
Checks if a request is allowed by two optional rate-limit handles with in-flight token tracking for s...
KeyState & get_key_state(LimitData &limit, const std::string &key)
Looks up a KeyState by key, creating it lazily if necessary.
long create_limit(long requests_per_period, long period_ms)
Legacy API: creates a new rate limit and returns only its ID.
void release_key(LimitData &limit, const std::string &key, uint64_t token)
Releases an in-flight token from a specific key and erases the key if it has no runtime state.
Duration time_until_limit_allows(const LimitData &limit, const std::string &key, const time_point_t &now) const
Duration time_until_key_allows(const LimitData &limit, const KeyState &state, const time_point_t &now) const
bool remove_limit(const HttpRateLimitHandlePtr &handle)
Releases manager-owned handle for the specified limit handle.
std::unordered_map< long, HttpRateLimitHandlePtr > m_owned_handles
Manager-owned handles for limits created through create_limit_handle().
RateLimitDelay< Duration > time_until_next_allowed(const HttpRateLimitHandlePtr &general_limit, const HttpRateLimitHandlePtr &specific_limit)
Legacy overload without explicit partition keys (uses default shared state).
const KeyState * find_key_state(const LimitData &limit, const std::string &key) const
Looks up a KeyState by key for read-only access (no insertion).
RateLimitDelay< Duration > time_until_next_allowed(const HttpRateLimitHandlePtr &general_limit, const HttpRateLimitHandlePtr &specific_limit, const std::string &general_key, const std::string &specific_key)
void release_request(const HttpRateLimitHandlePtr &general_limit, const HttpRateLimitHandlePtr &specific_limit, uint64_t in_flight_token)
Legacy overload without keys.
Primary namespace for the Kurlyk library, encompassing initialization, request management,...
std::shared_ptr< HttpRateLimitHandle > HttpRateLimitHandlePtr
Shared RAII handle for HTTP rate limits.
Per-key mutable runtime state inside a rate limit.
std::unordered_set< uint64_t > in_flight_tokens
Immutable limit parameters plus per-key mutable state.
bool removed
`true` when the manager-owned handle has been released; physical erase is deferred until all keys are...
std::unordered_map< std::string, KeyState > keys
Mutable state per partition key.
LimitData(long request_limit, long period, bool is_sequential, bool is_removed, const std::unordered_map< std::string, KeyState > &key_states)
bool sequential
When `true`, blocks other requests until the current one finishes.
Result type for time-until-allowed queries.
Duration duration
Delay until the limit allows a request. 0 means ready now.
bool sequential_blocked
true if duration reflects Duration::max() because a sequential in-flight request is blocking.