class RateLimit::Limiter
Public Class Methods
new(apikey:, on_error: :log_and_pass, logger: nil, debug: false, stats: nil, shared_cache: nil, in_process_cache: nil, use_expiry_cache: true, local: false )
click to toggle source
# File lib/ratelimit-ruby.rb, line 10 def initialize(apikey:, on_error: :log_and_pass, logger: nil, debug: false, stats: nil, # receives increment("it.ratelim.limitcheck", {:tags=>["policy_group:page_view", "pass:true"]}) shared_cache: nil, # Something that quacks like Rails.cache ideally memcached in_process_cache: nil, # ideally ActiveSupport::Cache::MemoryStore.new(size: 2.megabytes) use_expiry_cache: true, # must have shared_cache defined local: false # local development ) @on_error = on_error @logger = (logger || Logger.new($stdout)).tap do |log| log.progname = "RateLimit" if log.respond_to? :progname= end @stats = (stats || NoopStats.new) @shared_cache = (shared_cache || NoopCache.new) @in_process_cache = (in_process_cache || NoopCache.new) @use_expiry_cache = use_expiry_cache @conn = Faraday.new(:url => self.base_url(local)) do |faraday| faraday.request :json # form-encode POST params faraday.headers["accept"] = "application/json" faraday.response :logger if debug faraday.options[:open_timeout] = 2 faraday.options[:timeout] = 5 faraday.adapter Faraday.default_adapter # make requests with Net::HTTP end (@account_id, pass) = apikey.split("|") @conn.basic_auth(@account_id, pass) end
Public Instance Methods
acquire(group, acquire_amount, allow_partial_response: false)
click to toggle source
# File lib/ratelimit-ruby.rb, line 85 def acquire(group, acquire_amount, allow_partial_response: false) expiry_cache_key = "it.ratelim.expiry:#{group}" if @use_expiry_cache expiry = @shared_cache.read(expiry_cache_key) if !expiry.nil? && Integer(expiry) > Time.now.utc.to_f * 1000 @stats.increment("it.ratelim.limitcheck.expirycache.hit", tags: []) return OpenStruct.new(passed: false, amount: 0) end end result = @conn.post '/api/v1/limitcheck', { acquireAmount: acquire_amount, groups: [group], allowPartialResponse: allow_partial_response }.to_json handle_failure(result) unless result.success? res =JSON.parse(result.body, object_class: OpenStruct) res.amount ||= 0 @stats.increment("it.ratelim.limitcheck", tags: ["policy_group:#{res.policyGroup}", "pass:#{res.passed}"]) if @use_expiry_cache reset = result.headers['X-Rate-Limit-Reset'] @shared_cache.write(expiry_cache_key, reset) unless reset.nil? end return res rescue => e handle_error(e) end
acquire_or_wait(key:, acquire_amount:, max_wait_secs:, init_backoff: 0)
click to toggle source
# File lib/ratelimit-ruby.rb, line 113 def acquire_or_wait(key:, acquire_amount:, max_wait_secs:, init_backoff: 0) start = Time.now sleep = init_backoff while Time.now - start < max_wait_secs sleep(sleep) res = acquire(key, acquire_amount) if res.passed return res end sleep += rand * WAIT_INCR_MAX end raise RateLimit::WaitExceeded end
base_url(local)
click to toggle source
# File lib/ratelimit-ruby.rb, line 166 def base_url(local) local ? 'http://localhost:8080' : 'http://www.ratelim.it' end
create_limit(group, limit, policy, burst: nil)
click to toggle source
create only. does not overwrite if it already exists
# File lib/ratelimit-ruby.rb, line 52 def create_limit(group, limit, policy, burst: nil) upsert(LimitDefinition.new(group, limit, policy, false, burst || limit), :post) end
create_returnable_limit(group, total_tokens, seconds_to_refill_one_token)
click to toggle source
# File lib/ratelimit-ruby.rb, line 41 def create_returnable_limit(group, total_tokens, seconds_to_refill_one_token) upsert_returnable_limit(group, total_tokens, seconds_to_refill_one_token, method: :post) end
feature_is_on?(feature)
click to toggle source
# File lib/ratelimit-ruby.rb, line 137 def feature_is_on?(feature) feature_is_on_for?(feature, nil) end
feature_is_on_for?(feature, lookup_key, attributes: [])
click to toggle source
# File lib/ratelimit-ruby.rb, line 141 def feature_is_on_for?(feature, lookup_key, attributes: []) @stats.increment("it.ratelim.featureflag.on", tags: ["feature:#{feature}"]) cache_key = "it.ratelim.ff:#{feature}.#{lookup_key}.#{attributes}" @in_process_cache.fetch(cache_key, expires_in: 60) do next uncached_feature_is_on_for?(feature, lookup_key, attributes) if @shared_cache.class == NoopCache feature_obj = get_feature(feature) if feature_obj.nil? next false end attributes << lookup_key if lookup_key if (attributes & feature_obj.whitelisted).size > 0 next true end if lookup_key next get_user_pct(feature, lookup_key) < feature_obj.pct end next feature_obj.pct > rand() end end
pass?(group)
click to toggle source
# File lib/ratelimit-ruby.rb, line 80 def pass?(group) result = acquire(group, 1) return result.passed end
return(limit_result)
click to toggle source
# File lib/ratelimit-ruby.rb, line 127 def return(limit_result) result = @conn.post '/api/v1/limitreturn', { enforcedGroup: limit_result.enforcedGroup, expiresAt: limit_result.expiresAt, amount: limit_result.amount }.to_json handle_failure(result) unless result.success? rescue => e handle_error(e) end
upsert(limit_definition, method)
click to toggle source
# File lib/ratelimit-ruby.rb, line 61 def upsert(limit_definition, method) to_send = { limit: limit_definition.limit, group: limit_definition.group, burst: limit_definition.burst, policyName: limit_definition.policy, safetyLevel: limit_definition.safety_level, returnable: limit_definition.returnable }.to_json result= @conn.send(method, '/api/v1/limits', to_send) if !result.success? if method == :put handle_failure(result) elsif result.status != 409 # conflicts routinely expected on create handle_failure(result) end end rescue => e handle_error(e) end
upsert_limit(group, limit, policy, burst: nil)
click to toggle source
upsert. overwrite whatever is there
# File lib/ratelimit-ruby.rb, line 57 def upsert_limit(group, limit, policy, burst: nil) upsert(LimitDefinition.new(group, limit, policy, false, burst || limit), :put) end
upsert_returnable_limit(group, total_tokens, seconds_to_refill_one_token, method: :put)
click to toggle source
# File lib/ratelimit-ruby.rb, line 45 def upsert_returnable_limit(group, total_tokens, seconds_to_refill_one_token, method: :put) recharge_rate = (24*60*60)/seconds_to_refill_one_token recharge_policy = DAILY_ROLLING upsert(LimitDefinition.new(group, recharge_rate, recharge_policy, true, total_tokens), method) end
Private Instance Methods
get_all_features()
click to toggle source
# File lib/ratelimit-ruby.rb, line 189 def get_all_features @shared_cache.fetch("it.ratelim:get_all_features", expires_in: 60) do result = @conn.get "/api/v1/featureflags" @stats.increment("it.ratelim.featureflag.getall.req", tags: ["success:#{result.success?}"]) if result.success? res =JSON.parse(result.body, object_class: OpenStruct) Hash[res.map { |r| [r.feature, r] }] else @logger.error("failed to fetch feature flags #{result.status}") {} end end end
get_feature(feature)
click to toggle source
# File lib/ratelimit-ruby.rb, line 185 def get_feature(feature) get_all_features[feature] end
get_user_pct(feature, lookup_key)
click to toggle source
# File lib/ratelimit-ruby.rb, line 203 def get_user_pct(feature, lookup_key) int_value = Murmur3.murmur3_32("#{@account_id}#{feature}#{lookup_key}") int_value / 4294967294.0 end
handle_error(e)
click to toggle source
# File lib/ratelimit-ruby.rb, line 222 def handle_error(e) @stats.increment("it.ratelim.error", tags: ["type:limit"]) case @on_error when :log_and_pass @logger.warn(e) OpenStruct.new(passed: true, amount: 0) when :log_and_hit @logger.warn(e) OpenStruct.new(passed: false, amount: 0) when :throw raise e end end
handle_failure(result)
click to toggle source
# File lib/ratelimit-ruby.rb, line 208 def handle_failure(result) @stats.increment("it.ratelim.failure", tags: ["type:limit"]) case @on_error when :log_and_pass @logger.warn("returned #{result.status}") OpenStruct.new(passed: true, amount: 0) when :log_and_hit @logger.warn("returned #{result.status}") OpenStruct.new(passed: false, amount: 0) when :throw raise "#{result.status} calling RateLim.it" end end
handle_feature_failure(result)
click to toggle source
# File lib/ratelimit-ruby.rb, line 237 def handle_feature_failure(result) @stats.increment("it.ratelim.failure", tags: ["type:featureflag"]) case @on_error when :log_and_pass @logger.warn("returned #{result.status}") true when :log_and_hit @logger.warn("returned #{result.status}") false when :throw raise "#{result.status} calling feature flag RateLim.it" end end
uncached_feature_is_on_for?(feature, lookup_key, attributes)
click to toggle source
# File lib/ratelimit-ruby.rb, line 172 def uncached_feature_is_on_for?(feature, lookup_key, attributes) to_send = {} to_send[:lookupKey] = lookup_key unless lookup_key.nil? to_send[:attributes] = attributes if attributes.any? result = @conn.get "/api/v1/featureflags/#{feature}/on", to_send @stats.increment("it.ratelim.featureflag.on.req", tags: ["success:#{result.success?}"]) if result.success? result.body == "true" else handle_feature_failure(result) end end