Class: Tina4::RateLimiter

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

Constant Summary collapse

DEFAULT_LIMIT =
100
DEFAULT_WINDOW =

seconds

60

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(limit: nil, window: nil) ⇒ RateLimiter

Returns a new instance of RateLimiter.



10
11
12
13
14
15
16
# File 'lib/tina4/rate_limiter.rb', line 10

def initialize(limit: nil, window: nil)
  @limit = (limit || ENV["TINA4_RATE_LIMIT"] || DEFAULT_LIMIT).to_i
  @window = (window || ENV["TINA4_RATE_WINDOW"] || DEFAULT_WINDOW).to_i
  @store = {}    # ip => [timestamps]
  @mutex = Mutex.new
  @last_cleanup = Time.now
end

Instance Attribute Details

#limitObject (readonly)

Returns the value of attribute limit.



8
9
10
# File 'lib/tina4/rate_limiter.rb', line 8

def limit
  @limit
end

#windowObject (readonly)

Returns the value of attribute window.



8
9
10
# File 'lib/tina4/rate_limiter.rb', line 8

def window
  @window
end

Instance Method Details

#apply(ip, response) ⇒ Object

Apply rate limit headers to a response object and return 429 if exceeded. Returns [status, headers_hash] or nil if allowed.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/tina4/rate_limiter.rb', line 67

def apply(ip, response)
  result = check(ip)

  # Always set rate limit headers
  response.headers["X-RateLimit-Limit"] = result[:limit].to_s
  response.headers["X-RateLimit-Remaining"] = result[:remaining].to_s
  response.headers["X-RateLimit-Reset"] = result[:reset].to_s

  unless result[:allowed]
    response.headers["Retry-After"] = result[:retry_after].to_s
    response.status_code = 429
    response.headers["content-type"] = "application/json; charset=utf-8"
    response.body = JSON.generate({
      error: "Too Many Requests",
      retry_after: result[:retry_after]
    })
    return false
  end

  true
end

#before_rate_limit(request, response) ⇒ Object

Standardized middleware hook — enforces rate limiting before the route handler.



90
91
92
93
94
# File 'lib/tina4/rate_limiter.rb', line 90

def before_rate_limit(request, response)
  ip = request.respond_to?(:ip) ? request.ip : "unknown"
  apply(ip, response)
  [request, response]
end

#check(ip) ⇒ Object

Check if the given IP is rate limited. Returns a hash with rate limit info:

{ allowed: true/false, limit:, remaining:, reset:, retry_after: }


21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/tina4/rate_limiter.rb', line 21

def check(ip)
  now = Time.now
  cleanup_if_needed(now)

  @mutex.synchronize do
    @store[ip] ||= []
    entries = @store[ip]

    # Remove expired entries (sliding window)
    cutoff = now - @window
    entries.reject! { |t| t < cutoff }

    if entries.length >= @limit
      # Rate limited
      oldest = entries.first
      reset_at = (oldest + @window).to_i
      retry_after = [(oldest + @window - now).ceil, 1].max

      {
        allowed: false,
        limit: @limit,
        remaining: 0,
        reset: reset_at,
        retry_after: retry_after
      }
    else
      entries << now

      {
        allowed: true,
        limit: @limit,
        remaining: @limit - entries.length,
        reset: (now + @window).to_i,
        retry_after: nil
      }
    end
  end
end

#entry_countObject

Returns current entry count (for monitoring)



108
109
110
# File 'lib/tina4/rate_limiter.rb', line 108

def entry_count
  @mutex.synchronize { @store.length }
end

#rate_limited?(ip) ⇒ Boolean

Convenience predicate

Returns:

  • (Boolean)


61
62
63
# File 'lib/tina4/rate_limiter.rb', line 61

def rate_limited?(ip)
  !check(ip)[:allowed]
end

#reset(ip = nil) ⇒ Object

Reset tracking for a specific IP (useful for testing)



97
98
99
100
101
102
103
104
105
# File 'lib/tina4/rate_limiter.rb', line 97

def reset(ip = nil)
  @mutex.synchronize do
    if ip
      @store.delete(ip)
    else
      @store.clear
    end
  end
end