Class: NbApiClient::RateLimiter

Inherits:
Object
  • Object
show all
Defined in:
lib/nb_api_client/rate_limiter.rb

Overview

A global (cross-process, cross-thread) distributed rate limiter backed by Redis. Implements a sliding-window algorithm in a Lua script (atomic ZADD/ZREMRANGEBYSCORE/ZCARD on a sorted set) to enforce "N requests per window" across every process sharing the same Redis. All callers share one window (configure via NbApiClient.configure).

Defined Under Namespace

Classes: RateLimitExhausted

Constant Summary collapse

ACQUIRE_SLOT_SCRIPT =
  1. Remove all entries older than (now - window) to slide the window
  2. Count remaining entries in the set
  3. If under the limit, add a new entry (scored by timestamp) and return the new count
  4. 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

Instance Method Summary collapse

Constructor Details

#initializeRateLimiter

Returns a new instance of RateLimiter.



46
47
48
# File 'lib/nb_api_client/rate_limiter.rb', line 46

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.



42
43
44
# File 'lib/nb_api_client/rate_limiter.rb', line 42

def self.with_limit(&block)
  new.with_limit(&block)
end

Instance Method Details

#with_limitObject



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/nb_api_client/rate_limiter.rb', line 50

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 ConnectionPool::TimeoutError, Redis::BaseError, RedisClient::Error => e
  # Fail open: if Redis is unavailable, allow the request through.
  # NbApiClient::Request's 429 handling acts as a safety net.
  config.logger&.warn("[NbApiClient::RateLimiter] Redis error, failing open: #{e.class} - #{e.message}")
  yield
end