Class: SpreeMenuChat::RateLimiter

Inherits:
Object
  • Object
show all
Defined in:
app/services/spree_menu_chat/rate_limiter.rb

Overview

Fixed-window rate limiter — SpreeMenuChat::Config.rate_limit_per_hour requests per (store, identifier) per rolling hour, Postgres-backed (no Redis anywhere in this stack, same rationale as Solid Queue running inside Postgres rather than a separate broker).

Called from SpreeMenuChat::ChatController with request.remote_ip as the identifier — the widget is anonymous (no session/customer model yet), so IP is what's actually available before any answer is generated.

Defined Under Namespace

Classes: LimitExceededError

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(identifier, store:) ⇒ RateLimiter

Returns a new instance of RateLimiter.



17
18
19
20
# File 'app/services/spree_menu_chat/rate_limiter.rb', line 17

def initialize(identifier, store:)
  @identifier = identifier
  @store = store
end

Class Method Details

.check!(identifier, store: Spree::Store.default) ⇒ Object



13
14
15
# File 'app/services/spree_menu_chat/rate_limiter.rb', line 13

def self.check!(identifier, store: Spree::Store.default)
  new(identifier, store: store).check!
end

Instance Method Details

#check!Object

Raises LimitExceededError once the identifier has made rate_limit_per_hour requests within the current window; otherwise increments the counter and returns normally. Fixed window (resets a full hour after the first request in it), not a true sliding window — simpler, and standard practice for a limit this size.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'app/services/spree_menu_chat/rate_limiter.rb', line 27

def check!
  ActiveRecord::Base.transaction do
    record = SpreeMenuChat::RateLimit.lock.find_or_initialize_by(store: @store, identifier: @identifier)
    reset_if_window_expired(record)

    raise LimitExceededError if record.count >= SpreeMenuChat::Config.rate_limit_per_hour

    record.count += 1
    record.save!
  end
rescue ActiveRecord::RecordNotUnique
  # Lost a race with a concurrent first-request from the same
  # identifier — the row exists now, retry against it. Same pattern as
  # SpreeSquare::WebhooksController#find_or_log_event.
  retry
end