Move throttling feature from gem back to the app

This commit is contained in:
Alex Sherman 2021-11-27 14:48:10 +05:00 committed by olegphenomenon
parent 83413213d9
commit f17ef17d16
9 changed files with 230 additions and 2 deletions

27
app/lib/shunter.rb Normal file
View file

@ -0,0 +1,27 @@
module Shunter
module_function
class ThrottleError < StandardError; end
BASE_LOGGER = ::Logger.new($stdout)
ONE_MINUTE = 60
ONE_HUNDRED_REQUESTS = 100
BASE_CONNECTION = ENV['shunter_redis_connection'] || { host: 'redis', port: 6379 }
def default_timespan
ENV['shunter_default_timespan'] || ONE_MINUTE
end
def default_threshold
ENV['shunter_default_threshold'] || ONE_HUNDRED_REQUESTS
end
def default_adapter
ENV['shunter_default_adapter'] || 'Shunter::Adapters::Redis'
end
def feature_enabled?
ActiveModel::Type::Boolean.new.cast(ENV['shunter_enabled'] || 'false')
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Shunter
module Adapters
class Memory
attr_reader :store
def initialize(_options = {})
@@store ||= {}
end
def find_counter(key)
@@store[key]
end
def write_counter(key)
@@store[key] = 1
end
def increment_counter(key)
@@store[key] += 1
end
def clear!
@@store = {}
end
def expire_counter(_key, _timespan); end
end
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Shunter
module Adapters
class Redis
attr_reader :redis
def initialize(options)
@redis = ::Redis.new(options)
end
def find_counter(key)
@redis.get(key)
end
def write_counter(key)
@redis.set(key, 1)
end
def increment_counter(key)
@redis.incr(key)
end
def expire_counter(key, timespan)
@redis.expire(key, timespan)
end
end
end
end

63
app/lib/shunter/base.rb Normal file
View file

@ -0,0 +1,63 @@
# frozen_string_literal: true
module Shunter
class Base
attr_accessor :user_id, :adapter
def initialize(options = {})
@user_id = options[:user_id]
adapter_klass = Shunter.default_adapter.constantize
@adapter = adapter_klass.new(options[:conn_options])
end
def user_key
"counting_#{@user_id}"
end
def blocked_user_key
"blocked_#{@user_id}"
end
def throttle
return false if blocked?
valid_counter?
end
def blocked?
adapter.find_counter(blocked_user_key).present?
end
def valid_counter?
if adapter.find_counter(user_key)
number_of_requests = adapter.increment_counter(user_key)
if number_of_requests > allowed_requests.to_i
init_counter(blocked_user_key)
return false
end
else
init_counter(user_key)
end
true
end
private
def init_counter(key)
adapter.write_counter(key)
adapter.expire_counter(key, timespan)
end
def allowed_requests
Shunter.default_threshold
end
def timespan
Shunter.default_timespan
end
def logger
Shunter::BASE_LOGGER
end
end
end

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'active_support/concern'
module Shunter
module Integration
module Throttle
extend ActiveSupport::Concern
included do |base|
actions = base.const_defined?('THROTTLED_ACTIONS') && base.const_get('THROTTLED_ACTIONS')
return if actions.blank?
around_action :throttle, only: actions
def throttle
unless throttled_user.present? && Shunter.feature_enabled?
yield if block_given?
return
end
user_id = throttled_user.id
shunter = Shunter::Base.new(conn_options: connection_options, user_id: user_id)
if shunter.throttle
logger.info "Request from #{throttled_user.class}/#{throttled_user.id} is coming through throttling"
yield if block_given?
else
logger.info "Too many requests from #{throttled_user.class}/#{throttled_user.id}."
raise Shunter::ThrottleError
end
end
end
def connection_options
Shunter::BASE_CONNECTION
end
def logger
Shunter::BASE_LOGGER
end
end
end
end

View file

@ -41,4 +41,6 @@ Rails.application.configure do
# If set to :null_store, Setting.x returns nil after first spec runs (database is emptied) # If set to :null_store, Setting.x returns nil after first spec runs (database is emptied)
config.cache_store = :memory_store config.cache_store = :memory_store
config.log_level = :fatal
end end

View file

@ -193,7 +193,7 @@ class EppDomainInfoBaseTest < EppTestCase
<epp xmlns="#{Xsd::Schema.filename(for_prefix: 'epp-ee')}"> <epp xmlns="#{Xsd::Schema.filename(for_prefix: 'epp-ee')}">
<command> <command>
<info> <info>
<domain:info xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee')}"> <domain:info xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee', for_version: '1.2')}">
<domain:name>#{domain.name}</domain:name> <domain:name>#{domain.name}</domain:name>
</domain:info> </domain:info>
</info> </info>

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
require "test_helper"
require "action_controller"
require "pry"
class BaseTest < Minitest::Test
ENV["shunter_enabled"] = 'true'
def test_throttling_works_on_inclusion
ENV["shunter_default_adapter"] = "Shunter::Adapters::Memory"
ENV["shunter_default_threshold"] = "100"
adapter = ENV["shunter_default_adapter"].constantize.new
adapter.clear!
TestKlass.new.throttle do
TestKlass.new.test
end
end
class TestKlass < ::ActionController::Base
THROTTLED_ACTIONS = %i[test].freeze
include Shunter::Integration::Throttle
def test
"test"
end
def throttled_user
@throttled_user ||= OpenStruct.new(id: 1)
end
end
end

View file

@ -18,7 +18,6 @@ require 'capybara/minitest'
require 'webmock/minitest' require 'webmock/minitest'
require 'support/assertions/epp_assertions' require 'support/assertions/epp_assertions'
require 'sidekiq/testing' require 'sidekiq/testing'
require 'spy/integration'
Sidekiq::Testing.fake! Sidekiq::Testing.fake!