Skip to content

Commit bc9ab2d

Browse files
committed
dynamic limits
1 parent c67af90 commit bc9ab2d

File tree

7 files changed

+269
-11
lines changed

7 files changed

+269
-11
lines changed

lib/berater.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ module Berater
99

1010
class Overloaded < StandardError; end
1111

12-
attr_accessor :redis
12+
attr_accessor :redis, :dynamic_limits
1313

1414
def configure
1515
yield self
1616
end
1717

1818
def reset
19-
@redis = nil
19+
@redis = @dynamic_limits = nil
2020
end
2121

2222
def new(key, capacity, interval = nil, **opts)
@@ -37,6 +37,10 @@ def new(key, capacity, interval = nil, **opts)
3737
end
3838
end
3939

40+
def load_limits(key, redis: Berater.redis)
41+
Berater::Limiter.load_limits(key, redis: redis)
42+
end
43+
4044
def expunge
4145
redis.scan_each(match: "#{self.name}*") do |key|
4246
redis.del key

lib/berater/concurrency_limiter.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,25 @@ def initialize(key, capacity, **opts)
2020
LUA_SCRIPT = Berater::LuaScript(<<~LUA
2121
local key = KEYS[1]
2222
local lock_key = KEYS[2]
23+
local conf_key = KEYS[3]
2324
local capacity = tonumber(ARGV[1])
2425
local ts = tonumber(ARGV[2])
2526
local ttl = tonumber(ARGV[3])
2627
local cost = tonumber(ARGV[4])
2728
local lock_ids = {}
2829
30+
if conf_key then
31+
local config = redis.call('GET', conf_key)
32+
33+
if config then
34+
-- use dynamic capacity limit
35+
capacity = tonumber(config)
36+
37+
-- reset ttl for a week
38+
redis.call('EXPIRE', conf_key, 604800)
39+
end
40+
end
41+
2942
-- purge stale hosts
3043
if ttl > 0 then
3144
redis.call('ZREMRANGEBYSCORE', key, '-inf', ts - ttl)
@@ -64,15 +77,17 @@ def initialize(key, capacity, **opts)
6477
)
6578

6679
def limit(capacity: nil, cost: 1, &block)
80+
limit_key = if capacity.nil? && dynamic_limits
81+
config_key
82+
end
6783
capacity ||= @capacity
68-
# cost is Integer >= 0
6984

7085
# timestamp in microseconds
7186
ts = (Time.now.to_f * 10**6).to_i
7287

7388
count, *lock_ids = LUA_SCRIPT.eval(
7489
redis,
75-
[ cache_key(key), cache_key('lock_id') ],
90+
[ cache_key(key), cache_key('lock_id'), limit_key ],
7691
[ capacity, ts, @timeout_usec, cost ]
7792
)
7893

lib/berater/limiter.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
module Berater
22
class Limiter
33

4+
CONF_TTL = 60 * 60 * 24 * 7 # 1 week
5+
46
attr_reader :key, :capacity, :options
57

68
def redis
79
options[:redis] || Berater.redis
810
end
911

12+
def dynamic_limits
13+
options.fetch(:dynamic_limits, Berater.dynamic_limits) || false
14+
end
15+
1016
def limit
1117
raise NotImplementedError
1218
end
@@ -19,11 +25,27 @@ def to_s
1925
"#<#{self.class}>"
2026
end
2127

28+
def save_limits
29+
limit = [ capacity, *@args ].map(&:to_s).join(':')
30+
redis.setex(config_key, CONF_TTL, limit)
31+
end
32+
33+
def self.load_limits(key, redis: Berater.redis)
34+
res = redis.get(config_key(key))
35+
case res
36+
when "Infinity"
37+
[ Float::INFINITY ]
38+
when String
39+
res.split(':').map(&:to_i)
40+
end
41+
end
42+
2243
protected
2344

24-
def initialize(key, capacity, **opts)
45+
def initialize(key, capacity, *args, **opts)
2546
@key = key
2647
self.capacity = capacity
48+
@args = args
2749
@options = opts
2850
end
2951

@@ -38,7 +60,19 @@ def capacity=(capacity)
3860
end
3961

4062
def cache_key(key)
41-
"#{self.class}:#{key}"
63+
self.class.cache_key(key)
64+
end
65+
66+
def self.cache_key(key)
67+
"Berater:#{key}"
68+
end
69+
70+
def config_key
71+
self.class.config_key(key)
72+
end
73+
74+
def self.config_key(key)
75+
cache_key("#{key}-conf")
4276
end
4377

4478
def yield_lock(lock, &block)

lib/berater/rate_limiter.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ class Overrated < Overloaded; end
66
attr_accessor :interval
77

88
def initialize(key, capacity, interval, **opts)
9-
super(key, capacity, **opts)
10-
119
self.interval = interval
10+
11+
super(key, capacity, @interval_usec, **opts)
1212
end
1313

1414
private def interval=(interval)
@@ -19,12 +19,28 @@ def initialize(key, capacity, interval, **opts)
1919
LUA_SCRIPT = Berater::LuaScript(<<~LUA
2020
local key = KEYS[1]
2121
local ts_key = KEYS[2]
22+
local conf_key = KEYS[3]
2223
local ts = tonumber(ARGV[1])
2324
local capacity = tonumber(ARGV[2])
2425
local interval_usec = tonumber(ARGV[3])
2526
local cost = tonumber(ARGV[4])
2627
local count = 0
2728
local allowed
29+
30+
if conf_key then
31+
local config = redis.call('GET', conf_key)
32+
33+
if config then
34+
-- use dynamic capacity limit
35+
capacity, interval_usec = string.match(config, "(%d+):(%d+)")
36+
capacity = tonumber(capacity)
37+
interval_usec = tonumber(interval_usec)
38+
39+
-- reset ttl for a week
40+
redis.call('EXPIRE', conf_key, 604800)
41+
end
42+
end
43+
2844
local usec_per_drip = interval_usec / capacity
2945
3046
-- timestamp of last update
@@ -61,14 +77,17 @@ def initialize(key, capacity, interval, **opts)
6177
)
6278

6379
def limit(capacity: nil, cost: 1, &block)
80+
limit_key = if capacity.nil? && dynamic_limits
81+
config_key
82+
end
6483
capacity ||= @capacity
6584

6685
# timestamp in microseconds
6786
ts = (Time.now.to_f * 10**6).to_i
6887

6988
count, allowed = LUA_SCRIPT.eval(
7089
redis,
71-
[ cache_key(key), cache_key("#{key}-ts") ],
90+
[ cache_key(key), cache_key("#{key}-ts"), limit_key ],
7291
[ ts, capacity, @interval_usec, cost ]
7392
)
7493

spec/benchmark.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,34 @@
88

99
COUNT = 10_000
1010

11+
Berater.expunge
12+
1113
Benchmark.bmbm(30) do |x|
1214
x.report('RateLimiter') do
1315
COUNT.times do |i|
1416
Berater(:key, COUNT, :second) { i }
1517
end
1618
end
1719

20+
x.report('RateLimiter(dynamic_limits: true)') do
21+
Berater.new(:key, COUNT * 2, :second).save_limits
22+
COUNT.times do |i|
23+
Berater(:key, COUNT, :second, dynamic_limits: true) { i }
24+
end
25+
end
26+
1827
x.report('ConcurrencyLimiter') do
1928
COUNT.times do |i|
2029
Berater(:key, COUNT) { i }
2130
end
2231
end
32+
33+
x.report('ConcurrencyLimiter(dynamic_limits: true)') do
34+
Berater.new(:key, COUNT * 2).save_limits
35+
COUNT.times do |i|
36+
Berater(:key, COUNT, dynamic_limits: true) { i }
37+
end
38+
end
2339
end
2440

2541
Berater.expunge

spec/concurrency_limiter_spec.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,90 @@ def expect_bad_capacity(capacity)
237237
end
238238
end
239239

240+
describe 'dynamic_limits' do
241+
let(:limiter) { described_class.new(:key, 1) }
242+
243+
describe '#save_limits' do
244+
it 'saves to redis' do
245+
expect(limiter.redis).to receive(:setex)
246+
expect(limiter).to receive(:config_key)
247+
limiter.save_limits
248+
end
249+
end
250+
251+
describe '.load_limits' do
252+
it 'loads from redis' do
253+
limiter.save_limits
254+
capacity = Berater.load_limits(limiter.key)[0]
255+
expect(capacity).to eq limiter.capacity
256+
end
257+
end
258+
259+
it 'is disabled by default' do
260+
expect(limiter.dynamic_limits).to be false
261+
expect(limiter).not_to receive(:config_key)
262+
limiter.limit
263+
end
264+
265+
it 'can be enabled per instance' do
266+
limiter = described_class.new(:key, 1, dynamic_limits: true)
267+
expect(limiter).to receive(:config_key)
268+
limiter.limit
269+
end
270+
271+
context 'with dynamic_limits enabled' do
272+
before do
273+
Berater.configure do |c|
274+
c.dynamic_limits = true
275+
end
276+
277+
limiter.save_limits
278+
end
279+
280+
it 'is enabled' do
281+
expect(limiter).to receive(:config_key)
282+
limiter.limit
283+
end
284+
285+
it 'overrides instance limit' do
286+
limiter.limit
287+
expect { limiter }.to be_incapacitated
288+
289+
limiter_two = described_class.new(:key, 2)
290+
expect { limiter_two }.to be_incapacitated
291+
end
292+
293+
it 'yields to limit parameter' do
294+
limiter.limit
295+
expect { limiter }.to be_incapacitated
296+
297+
limiter.limit(capacity: 2)
298+
end
299+
300+
describe '#overloaded?' do
301+
let(:limiter) { described_class.new(:key, 2) }
302+
303+
before do
304+
2.times { limiter.limit }
305+
end
306+
307+
it 'respects limit' do
308+
expect(limiter).to be_incapacitated
309+
end
310+
311+
it 'respects saved limit override' do
312+
expect(
313+
described_class.new(:key, 1)
314+
).to be_incapacitated
315+
316+
expect(
317+
described_class.new(:key, 3)
318+
).to be_incapacitated
319+
end
320+
end
321+
end
322+
end
323+
240324
describe '#to_s' do
241325
def check(capacity, expected)
242326
expect(

0 commit comments

Comments
 (0)