Kurlyk
Loading...
Searching...
No Matches
HttpRateLimiter.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef _KURLYK_HTTP_RATE_LIMITER_HPP_INCLUDED
3#define _KURLYK_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 };
395
404 bool remove_limit_internal(long limit_id) {
405 std::lock_guard<std::mutex> lock(m_mutex);
406 auto it = m_limits.find(limit_id);
407 if (it == m_limits.end()) {
408 return false;
409 }
410 auto& limit = it->second;
411 limit.removed = true;
412 // Erase only if no keys hold runtime state.
413 if (!limit.keys.empty()) {
414 return false;
415 }
416 return m_limits.erase(limit_id) > 0;
417 }
418
420 KeyState& get_key_state(LimitData& limit, const std::string& key) {
421 return limit.keys[key];
422 }
423
425 const KeyState* find_key_state(const LimitData& limit, const std::string& key) const {
426 auto it = limit.keys.find(key);
427 if (it == limit.keys.end()) {
428 return nullptr;
429 }
430 return &it->second;
431 }
432
434 bool can_pass(const LimitData& limit, const std::string& key, uint64_t token, const time_point_t& now) const {
435 const KeyState* state = find_key_state(limit, key);
436 if (!state) {
437 // No state yet: only need to check the base limit parameters.
438 if (limit.requests_per_period == 0) {
439 return true;
440 }
441 return true; // count is 0, so always under limit.
442 }
443 if (limit.sequential && token != 0) {
444 if (!state->in_flight_tokens.empty() &&
445 state->in_flight_tokens.count(token) == 0) {
446 return false;
447 }
448 }
449 return check_key(limit, *state, now);
450 }
451
452 bool check_key(const LimitData& limit_data, const KeyState& state, const time_point_t& now) const {
453 if (limit_data.requests_per_period == 0) {
454 return true;
455 }
456
457 const auto elapsed_time =
458 std::chrono::duration_cast<std::chrono::milliseconds>(
459 now - state.start_time
460 );
461
462 if (elapsed_time.count() >= limit_data.period_ms) {
463 return true;
464 }
465
466 return state.count < limit_data.requests_per_period;
467 }
468
470 void commit_limit(LimitData& limit, const std::string& key, uint64_t token, const time_point_t& now) {
471 KeyState& state = get_key_state(limit, key);
472 if (limit.sequential && token != 0) {
473 state.in_flight_tokens.insert(token);
474 }
475
476 // Retry attempts may reuse the same in-flight token to avoid self-blocking
477 // sequential limits, but each actual HTTP attempt still consumes the
478 // count-based rate limit.
479 update_key(limit, state, now);
480 }
481
482 void update_key(LimitData& limit_data, KeyState& state, const time_point_t& now) {
483 if (limit_data.requests_per_period == 0) {
484 return;
485 }
486
487 const auto elapsed_time =
488 std::chrono::duration_cast<std::chrono::milliseconds>(
489 now - state.start_time
490 );
491
492 if (elapsed_time.count() >= limit_data.period_ms) {
493 state.start_time = now;
494 state.count = 0;
495 }
496
497 ++state.count;
498 }
499
501 void release_key(LimitData& limit, const std::string& key, uint64_t token) {
502 auto it = limit.keys.find(key);
503 if (it == limit.keys.end()) {
504 return;
505 }
506 auto& state = it->second;
507 state.in_flight_tokens.erase(token);
508 if (state.in_flight_tokens.empty() && state.count == 0) {
509 limit.keys.erase(it);
510 }
511 if (limit.removed && limit.keys.empty()) {
512 // We cannot erase `limit` here because we are iterating or
513 // the caller holds a reference. Deferred to remove_limit_internal
514 // or next gc pass. In practice remove_limit_internal already
515 // tries; here we rely on gc_stale_keys or the next remove_limit_internal.
516 }
517 }
518
519 template<typename Duration>
521 const LimitData& limit,
522 const std::string& key,
523 const time_point_t& now
524 ) const {
525 const KeyState* state = find_key_state(limit, key);
526 if (!state) {
527 // No state means no in-flight tokens and count is 0.
528 if (limit.sequential) {
529 return Duration{0};
530 }
531 if (limit.requests_per_period == 0) {
532 return Duration{0};
533 }
534 return Duration{0};
535 }
536 return time_until_key_allows<Duration>(limit, *state, now);
537 }
538
539 template<typename Duration>
541 const LimitData& limit,
542 const KeyState& state,
543 const time_point_t& now
544 ) const {
545 if (limit.sequential &&
546 !state.in_flight_tokens.empty()) {
547 return (Duration::max)();
548 }
549
550 if (limit.requests_per_period == 0) {
551 return Duration{0};
552 }
553
554 const auto elapsed =
555 std::chrono::duration_cast<Duration>(now - state.start_time);
556
557 const auto period_duration =
558 std::chrono::duration_cast<Duration>(
559 std::chrono::milliseconds(limit.period_ms)
560 );
561
562 if (elapsed >= period_duration ||
563 state.count < limit.requests_per_period) {
564 return Duration{0};
565 }
566
567 return period_duration - elapsed;
568 }
569
571 void gc_stale_keys(const time_point_t& now) {
572 for (auto limit_it = m_limits.begin(); limit_it != m_limits.end(); ) {
573 auto& limit = limit_it->second;
574 for (auto key_it = limit.keys.begin(); key_it != limit.keys.end(); ) {
575 const auto& state = key_it->second;
576 if (state.in_flight_tokens.empty() && state.count == 0) {
577 key_it = limit.keys.erase(key_it);
578 } else if (state.in_flight_tokens.empty()) {
579 const auto elapsed =
580 std::chrono::duration_cast<std::chrono::milliseconds>(
581 now - state.start_time
582 );
583 if (elapsed.count() >= limit.period_ms) {
584 key_it = limit.keys.erase(key_it);
585 } else {
586 ++key_it;
587 }
588 } else {
589 ++key_it;
590 }
591 }
592
593 if (limit.removed && limit.keys.empty()) {
594 limit_it = m_limits.erase(limit_it);
595 } else {
596 ++limit_it;
597 }
598 }
599 }
600
601 private:
602 mutable std::mutex m_mutex;
603
604 long m_next_id = 1;
605
609 std::unordered_map<long, LimitData> m_limits;
610
615 std::unordered_map<long, HttpRateLimitHandlePtr> m_owned_handles;
616
617 size_t m_gc_counter = 0;
618 };
619
620} // namespace kurlyk
621
622#endif // _KURLYK_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.
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.