Class: NbApiClient::RateLimiter
- Inherits:
-
Object
- Object
- NbApiClient::RateLimiter
- Defined in:
- lib/nb_api_client/rate_limiter.rb
Overview
A global (cross-process, cross-thread) distributed rate limiter backed by configuration.cache_store (the same store used for unauthorized-response tracking; defaults to Rails.cache). All callers share one window (configure via NbApiClient.configure).
If cache_store is Redis-backed and its underlying client supports Lua scripts (i.e. responds to #eval — true for ActiveSupport::Cache::RedisCacheStore and for a bare Redis/ConnectionPool passed in directly), a true sliding window is enforced atomically via a Lua script (ZADD/ZREMRANGEBYSCORE/ZCARD on a sorted set).
Otherwise, a "sliding window counter" algorithm is used instead: each window is divided into fixed buckets, and the previous bucket's count is weighted by how much of it still overlaps the sliding window. This approximates a true sliding window without requiring sorted-set support, so it works with any ActiveSupport::Cache::Store (memory, Memcached, ...).
If no cache_store is configured, rate limiting is disabled entirely (every request is allowed through immediately).
Defined Under Namespace
Classes: RateLimitExhausted
Constant Summary collapse
- ACQUIRE_SLOT_SCRIPT =
- Remove all entries older than (now - window) to slide the window
- Count remaining entries in the set
- If under the limit, add a new entry (scored by timestamp) and return the new count
- If at/over the limit, return -1 (rejected)
The sorted set key is given a TTL of 2*window as a safety net to avoid unbounded memory growth if traffic stops entirely.
<<~LUA local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local member = ARGV[4] -- Prune entries outside the sliding window redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window) local count = redis.call('ZCARD', key) if count < limit then redis.call('ZADD', key, now, member) -- Safety-net expiry so the key doesn't live forever if traffic stops redis.call('EXPIRE', key, window * 2) return count + 1 else return -1 end LUA
Class Method Summary collapse
-
.with_limit(&block) ⇒ Object
Blocks until a request slot is available, then yields to the caller.
Instance Method Summary collapse
-
#initialize ⇒ RateLimiter
constructor
A new instance of RateLimiter.
- #with_limit ⇒ Object
Constructor Details
#initialize ⇒ RateLimiter
Returns a new instance of RateLimiter.
61 62 63 |
# File 'lib/nb_api_client/rate_limiter.rb', line 61 def initialize @key = "nb_rate_limit:window" end |
Class Method Details
.with_limit(&block) ⇒ Object
Blocks until a request slot is available, then yields to the caller. Raises RateLimitExhausted if the wait exceeds configuration.rate_limit_max_wait_seconds.
57 58 59 |
# File 'lib/nb_api_client/rate_limiter.rb', line 57 def self.with_limit(&block) new.with_limit(&block) end |
Instance Method Details
#with_limit ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/nb_api_client/rate_limiter.rb', line 65 def with_limit config = NbApiClient.configuration waited = 0.0 loop do result = try_acquire_slot if result > 0 return yield else if waited >= config.rate_limit_max_wait_seconds raise RateLimitExhausted, "Rate limit exhausted after waiting #{waited}s" end # Sleep with jitter to spread retries and avoid thundering herd sleep_time = config.rate_limit_poll_interval + rand(0.0..config.rate_limit_max_jitter) sleep(sleep_time) waited += sleep_time end end rescue RateLimitExhausted raise rescue StandardError => e # Fail open: if the cache store is unavailable, allow the request through. # NbApiClient::Request's 429 handling acts as a safety net. config.logger&.warn("[NbApiClient::RateLimiter] cache store error, failing open: #{e.class} - #{e.}") yield end |