Skip to content

Commit

Permalink
Merge pull request #38142 from code-dot-org/cdo-throttle
Browse files Browse the repository at this point in the history
Add Cdo::Throttle module
  • Loading branch information
Madelyn Kasula committed Dec 9, 2020
2 parents 54f67c4 + a90339e commit 5cea064
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 0 deletions.
45 changes: 45 additions & 0 deletions lib/cdo/throttle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'cdo/shared_cache'
require 'dynamic_config/dcdo'

module Cdo
module Throttle
CACHE_PREFIX = "cdo_throttle/".freeze

# @param [String] id - Unique identifier to throttle on.
# @param [Integer] limit - Number of requests allowed over period.
# @param [Integer] period - Period of time in seconds.
# @param [Integer] throttle_for - How long id should stay throttled in seconds. Optional.
# Defaults to Cdo::Throttle.throttle_time.
# @returns [Boolean] Whether or not the request should be throttled.
def self.throttle(id, limit, period, throttle_for = throttle_time)
full_key = CACHE_PREFIX + id.to_s
value = CDO.shared_cache.read(full_key) || empty_value
now = Time.now.utc
value[:request_timestamps] << now

if value[:throttled_until]&.future?
should_throttle = true
else
value[:throttled_until] = nil
earliest = now - period
value[:request_timestamps].select! {|timestamp| timestamp >= earliest}
should_throttle = value[:request_timestamps].size > limit
value[:throttled_until] = now + throttle_for if should_throttle
end

CDO.shared_cache.write(full_key, value)
should_throttle
end

def self.empty_value
{
throttled_until: nil,
request_timestamps: []
}
end

def self.throttle_time
DCDO.get('throttle_time_default', 60)
end
end
end
37 changes: 37 additions & 0 deletions lib/test/cdo/test_throttle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require_relative '../test_helper'
require 'cdo/throttle'
require 'timecop'

class ThrottleTest < Minitest::Test
def teardown
CDO.shared_cache.clear
end

def test_throttle_with_limit_1
Timecop.freeze
refute Cdo::Throttle.throttle("my_key", 1, 2) # 1/1 reqs per 2s - not throttled
Timecop.travel(Time.now.utc + 1)
assert Cdo::Throttle.throttle("my_key", 1, 2) # 2/1 reqs per 2s - throttled
Timecop.travel(Time.now.utc + Cdo::Throttle.throttle_time - 1)
assert Cdo::Throttle.throttle("my_key", 1, 2) # still throttled
Timecop.travel(Time.now.utc + Cdo::Throttle.throttle_time)
refute Cdo::Throttle.throttle("my_key", 1, 2) # 1/1 reqs per 2s after waiting - not throttled anymore
Timecop.travel(Time.now.utc + 1)
assert Cdo::Throttle.throttle("my_key", 1, 2) # 2/1 reqs per 2s - throttled again
end

def test_throttle_with_limit_greater_than_1
Timecop.freeze
refute Cdo::Throttle.throttle("my_key", 2, 2) # 1/2 reqs per 2s - not throttled
Timecop.travel(Time.now.utc + 1)
refute Cdo::Throttle.throttle("my_key", 2, 2) # 2/2 reqs per 2s - not throttled
Timecop.travel(Time.now.utc + 0.5)
assert Cdo::Throttle.throttle("my_key", 2, 2) # 3/2 reqs per 2s - throttled
Timecop.travel(Time.now.utc + Cdo::Throttle.throttle_time)
refute Cdo::Throttle.throttle("my_key", 2, 2) # 1/2 reqs per 2s after waiting - not throttled anymore
Timecop.travel(Time.now.utc + 1)
refute Cdo::Throttle.throttle("my_key", 2, 2) # 2/2 reqs per 2s - not throttled
Timecop.travel(Time.now.utc + 0.5)
assert Cdo::Throttle.throttle("my_key", 2, 2) # 3/2 reqs per 2s - throttled again
end
end

0 comments on commit 5cea064

Please sign in to comment.