Class: TranslationDiff::RateLimiters::ActiveRecord

Inherits:
Object
  • Object
show all
Includes:
ActiveRecord::Support
Defined in:
lib/translation_diff/rate_limiters/active_record.rb

Overview

Throttles by counting characters into namespaced, time-bucketed rows in the application's own database.

Constant Summary collapse

DEFAULT_THRESHOLD =
8000
DEFAULT_INTERVAL =
60
BUCKET_FRACTION =

A bucket a twelfth of the interval wide caps a window's slop at under 10%, the same shape the ratelimit gem gets from its own fixed five-second buckets at the default 60-second interval.

12

Constants included from ActiveRecord::Support

ActiveRecord::Support::MINIMUM_ACTIVE_RECORD

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ActiveRecord::Support

#model

Constructor Details

#initialize(namespace:, table_name:, threshold: DEFAULT_THRESHOLD, interval: DEFAULT_INTERVAL, base: nil, clock: -> { Time.now }) ⇒ ActiveRecord

Returns a new instance of ActiveRecord.



20
21
22
23
24
25
26
27
28
29
# File 'lib/translation_diff/rate_limiters/active_record.rb', line 20

def initialize(namespace:, table_name:, threshold: DEFAULT_THRESHOLD, interval: DEFAULT_INTERVAL, base: nil,
               clock: -> { Time.now })
  @namespace = namespace
  @table_name = table_name
  @threshold = threshold
  @interval = interval
  @base = base
  @clock = clock
  @bucket_width = [@interval / BUCKET_FRACTION, 1].max
end

Class Method Details

.build(config) ⇒ Object

An unset rate_limit must mean DEFAULT_THRESHOLD, not the nil that would override that keyword default.



13
14
15
16
17
18
# File 'lib/translation_diff/rate_limiters/active_record.rb', line 13

def self.build(config)
  options = { namespace: config.cache_namespace, table_name: config.rate_limit_table_name,
              interval: config.rate_interval, base: config.active_record_base }
  options[:threshold] = config.rate_limit unless config.rate_limit.nil?
  new(**options)
end

Instance Method Details

#check(size) ⇒ Object

A sliding window: every bucket covering the last interval seconds is summed, not just the current one.



32
33
34
35
36
37
38
39
40
# File 'lib/translation_diff/rate_limiters/active_record.rb', line 32

def check(size)
  raise TranslationDiff::RateLimitExceeded, exceeded_message if current_total >= @threshold

  add(size)
rescue StandardError => e
  raise unless ar_error?(e)

  raise redacted_error(e), cause: nil
end

#exceeded_message ⇒ Object

Counts and settings, never a character of what was being translated.



51
52
53
# File 'lib/translation_diff/rate_limiters/active_record.rb', line 51

def exceeded_message
  "rate limit reached for #{@namespace}: #{@threshold} characters per #{@interval} seconds"
end

#prune ⇒ Object

Buckets that have fully aged out of the window as of now; the oldest bucket itself is still counted by it.



56
# File 'lib/translation_diff/rate_limiters/active_record.rb', line 56

def prune = model.where(namespace: @namespace).where(bucket: ...oldest_bucket).delete_all

#redacted_error(error) ⇒ Object

The limiter's own statements carry counts, not content -- but a ReadOnlyError quotes the statement, and a raw ActiveRecord error from inside a translate call tells a caller nothing about which gem it came from.



44
45
46
47
48
# File 'lib/translation_diff/rate_limiters/active_record.rb', line 44

def redacted_error(error)
  adapter_error = error.cause&.class || error.class
  TranslationDiff::Error.new("the rate limit check failed (#{adapter_error}): a read or upsert on " \
                             "#{@table_name}(namespace, bucket, characters)")
end