berater 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,63 +3,28 @@ module Berater
3
3
 
4
4
  class Overrated < Overloaded; end
5
5
 
6
- attr_accessor :count, :interval
6
+ attr_accessor :interval
7
7
 
8
- def initialize(key, count, interval, **opts)
9
- super(key, **opts)
10
-
11
- self.count = count
8
+ def initialize(key, capacity, interval, **opts)
12
9
  self.interval = interval
13
- end
14
-
15
- private def count=(count)
16
- unless count.is_a? Integer
17
- raise ArgumentError, "expected Integer, found #{count.class}"
18
- end
19
-
20
- raise ArgumentError, "count must be >= 0" unless count >= 0
21
-
22
- @count = count
10
+ super(key, capacity, @interval_usec, **opts)
23
11
  end
24
12
 
25
13
  private def interval=(interval)
26
- @interval = interval.dup
27
-
28
- case @interval
29
- when Integer
30
- raise ArgumentError, "interval must be >= 0" unless @interval >= 0
31
- @interval_sec = @interval
32
- when String
33
- @interval = @interval.to_sym
34
- when Symbol
35
- else
36
- raise ArgumentError, "unexpected interval type: #{interval.class}"
37
- end
38
-
39
- if @interval.is_a? Symbol
40
- case @interval
41
- when :sec, :second, :seconds
42
- @interval = :second
43
- @interval_sec = 1
44
- when :min, :minute, :minutes
45
- @interval = :minute
46
- @interval_sec = 60
47
- when :hour, :hours
48
- @interval = :hour
49
- @interval_sec = 60 * 60
50
- else
51
- raise ArgumentError, "unexpected interval value: #{interval}"
52
- end
53
- end
14
+ @interval = interval
15
+ @interval_usec = Berater::Utils.to_usec(interval)
54
16
  end
55
17
 
56
- LUA_SCRIPT = <<~LUA.gsub(/^\s*|\s*--.*/, '')
18
+ LUA_SCRIPT = Berater::LuaScript(<<~LUA
57
19
  local key = KEYS[1]
58
20
  local ts_key = KEYS[2]
59
21
  local ts = tonumber(ARGV[1])
60
22
  local capacity = tonumber(ARGV[2])
61
- local usec_per_drip = tonumber(ARGV[3])
23
+ local interval_usec = tonumber(ARGV[3])
24
+ local cost = tonumber(ARGV[4])
62
25
  local count = 0
26
+ local allowed
27
+ local usec_per_drip = interval_usec / capacity
63
28
 
64
29
  -- timestamp of last update
65
30
  local last_ts = tonumber(redis.call('GET', ts_key))
@@ -72,61 +37,65 @@ module Berater
72
37
  count = math.max(0, count - drips)
73
38
  end
74
39
 
75
- local allowed = count + 1 <= capacity
40
+ if cost == 0 then
41
+ -- just check limit, ie. for .overlimit?
42
+ allowed = count < capacity
43
+ else
44
+ allowed = (count + cost) <= capacity
76
45
 
77
- if allowed then
78
- count = count + 1
46
+ if allowed then
47
+ count = count + cost
79
48
 
80
- -- time for bucket to empty, in milliseconds
81
- local ttl = math.ceil((count * usec_per_drip) / 1000)
49
+ -- time for bucket to empty, in milliseconds
50
+ local ttl = math.ceil((count * usec_per_drip) / 1000)
82
51
 
83
- -- update count and last_ts, with expirations
84
- redis.call('SET', key, count, 'PX', ttl)
85
- redis.call('SET', ts_key, ts, 'PX', ttl)
52
+ -- update count and last_ts, with expirations
53
+ redis.call('SET', key, count, 'PX', ttl)
54
+ redis.call('SET', ts_key, ts, 'PX', ttl)
55
+ end
86
56
  end
87
57
 
88
58
  return { count, allowed }
89
59
  LUA
60
+ )
90
61
 
91
- def limit
92
- usec_per_drip = (@interval_sec * 10**6) / @count
62
+ def limit(capacity: nil, cost: 1, &block)
63
+ capacity ||= @capacity
93
64
 
94
65
  # timestamp in microseconds
95
66
  ts = (Time.now.to_f * 10**6).to_i
96
67
 
97
- count, allowed = redis.eval(
98
- LUA_SCRIPT,
68
+ count, allowed = LUA_SCRIPT.eval(
69
+ redis,
99
70
  [ cache_key(key), cache_key("#{key}-ts") ],
100
- [ ts, @count, usec_per_drip ]
71
+ [ ts, capacity, @interval_usec, cost ]
101
72
  )
102
73
 
103
74
  raise Overrated unless allowed
104
75
 
105
- lock = Lock.new(self, "#{ts}-#{count}", count)
76
+ lock = Lock.new(self, ts, count)
77
+ yield_lock(lock, &block)
78
+ end
106
79
 
107
- if block_given?
108
- begin
109
- yield lock
110
- ensure
111
- lock.release
112
- end
113
- else
114
- lock
115
- end
80
+ def overloaded?
81
+ limit(cost: 0) { false }
82
+ rescue Overrated
83
+ true
116
84
  end
85
+ alias overrated? overloaded?
117
86
 
118
87
  def to_s
119
- msg = if @interval.is_a? Integer
120
- if @interval == 1
88
+ msg = if interval.is_a? Numeric
89
+ if interval == 1
121
90
  "every second"
122
91
  else
123
- "every #{@interval} seconds"
92
+ "every #{interval} seconds"
124
93
  end
125
94
  else
126
- "per #{@interval}"
95
+ "per #{interval}"
127
96
  end
128
97
 
129
- "#<#{self.class}(#{key}: #{count} #{msg})>"
98
+ "#<#{self.class}(#{key}: #{capacity} #{msg})>"
130
99
  end
131
100
 
132
101
  end
data/lib/berater/rspec.rb CHANGED
@@ -8,5 +8,7 @@ RSpec.configure do |config|
8
8
 
9
9
  config.after do
10
10
  Berater.expunge rescue nil
11
+ Berater.redis.script(:flush) rescue nil
12
+ Berater.reset
11
13
  end
12
14
  end
@@ -16,15 +16,17 @@ module BeraterMatchers
16
16
  res = obj.call
17
17
 
18
18
  if res.is_a? Berater::Limiter
19
- # eg. expect { Berater.new(...) }.to be_overrated
20
- res.limit {}
19
+ # eg. expect { Berater.new(...) }.to be_overloaded
20
+ res.overloaded?
21
+ else
22
+ # eg. expect { Berater(...) }.to be_overloaded
23
+ # eg. expect { limiter.limit }.to be_overloaded
24
+ false
21
25
  end
22
26
  when Berater::Limiter
23
- # eg. expect(Berater.new(...)).to be_overrated
24
- obj.limit {}
27
+ # eg. expect(Berater.new(...)).to be_overloaded
28
+ obj.overloaded?
25
29
  end
26
-
27
- false
28
30
  rescue @type
29
31
  true
30
32
  end
@@ -11,10 +11,15 @@ module Berater
11
11
  end
12
12
 
13
13
  @test_mode = mode
14
+
15
+ # overload class methods
16
+ unless Berater::Limiter.singleton_class.ancestors.include?(TestMode)
17
+ Berater::Limiter.singleton_class.prepend(TestMode)
18
+ end
14
19
  end
15
20
 
16
- class Limiter
17
- def self.new(*args, **opts)
21
+ module TestMode
22
+ def new(*args, **opts)
18
23
  return super unless Berater.test_mode
19
24
 
20
25
  # chose a stub class with desired behavior
@@ -28,13 +33,17 @@ module Berater
28
33
  # don't stub self
29
34
  return super if self < stub_klass
30
35
 
31
- # swap out limit method with stub
36
+ # swap out limit and overloaded? methods with stub
32
37
  super.tap do |instance|
33
38
  stub = stub_klass.allocate
34
39
  stub.send(:initialize, *args, **opts)
35
40
 
36
- instance.define_singleton_method(:limit) do |&block|
37
- stub.limit(&block)
41
+ instance.define_singleton_method(:limit) do |**opts, &block|
42
+ stub.limit(**opts, &block)
43
+ end
44
+
45
+ instance.define_singleton_method(:overloaded?) do
46
+ stub.overloaded?
38
47
  end
39
48
  end
40
49
  end
@@ -2,21 +2,15 @@ module Berater
2
2
  class Unlimiter < Limiter
3
3
 
4
4
  def initialize(key = :unlimiter, *args, **opts)
5
- super(key, **opts)
5
+ super(key, Float::INFINITY, **opts)
6
6
  end
7
7
 
8
- def limit
9
- lock = Lock.new(self, 0, 0)
8
+ def limit(**opts, &block)
9
+ yield_lock(Lock.new(self, Float::INFINITY, 0), &block)
10
+ end
10
11
 
11
- if block_given?
12
- begin
13
- yield lock
14
- ensure
15
- lock.release
16
- end
17
- else
18
- lock
19
- end
12
+ def overloaded?
13
+ false
20
14
  end
21
15
 
22
16
  end
@@ -0,0 +1,46 @@
1
+ module Berater
2
+ module Utils
3
+ extend self
4
+
5
+ refine Object do
6
+ def to_usec
7
+ Berater::Utils.to_usec(self)
8
+ end
9
+ end
10
+
11
+ def to_usec(val)
12
+ res = val
13
+
14
+ if val.is_a? String
15
+ # naively attempt casting, otherwise maybe it's a keyword
16
+ res = Float(val) rescue val.to_sym
17
+ end
18
+
19
+ if res.is_a? Symbol
20
+ case res
21
+ when :sec, :second, :seconds
22
+ res = 1
23
+ when :min, :minute, :minutes
24
+ res = 60
25
+ when :hour, :hours
26
+ res = 60 * 60
27
+ end
28
+ end
29
+
30
+ unless res.is_a? Numeric
31
+ raise ArgumentError, "unexpected value: #{val}"
32
+ end
33
+
34
+ if res < 0
35
+ raise ArgumentError, "expected value >= 0, found: #{val}"
36
+ end
37
+
38
+ if res == Float::INFINITY
39
+ raise ArgumentError, "infinite values not allowed"
40
+ end
41
+
42
+ (res * 10**6).to_i
43
+ end
44
+
45
+ end
46
+ end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -7,12 +7,12 @@ describe Berater do
7
7
  it { is_expected.to respond_to :configure }
8
8
 
9
9
  describe '.configure' do
10
- it 'can be set via configure' do
10
+ it 'is used with a block' do
11
11
  Berater.configure do |c|
12
12
  c.redis = :redis
13
13
  end
14
14
 
15
- expect(Berater.redis).to eq :redis
15
+ expect(Berater.redis).to be :redis
16
16
  end
17
17
  end
18
18
 
@@ -26,7 +26,7 @@ describe Berater do
26
26
 
27
27
  describe '.new' do
28
28
  context 'unlimited mode' do
29
- let(:limiter) { Berater.new(:key, :unlimited) }
29
+ let(:limiter) { Berater.new(:key, Float::INFINITY) }
30
30
 
31
31
  it 'instantiates an Unlimiter' do
32
32
  expect(limiter).to be_a Berater::Unlimiter
@@ -39,18 +39,13 @@ describe Berater do
39
39
 
40
40
  it 'accepts options' do
41
41
  redis = double('Redis')
42
- limiter = Berater.new(:key, :unlimited, redis: redis)
42
+ limiter = Berater.new(:key, Float::INFINITY, redis: redis)
43
43
  expect(limiter.redis).to be redis
44
44
  end
45
-
46
- it 'works with convinience' do
47
- expect(Berater).to receive(:new).and_return(limiter)
48
- expect {|b| Berater(:key, :unlimited, &b) }.to yield_control
49
- end
50
45
  end
51
46
 
52
47
  context 'inhibited mode' do
53
- let(:limiter) { Berater.new(:key, :inhibited) }
48
+ let(:limiter) { Berater.new(:key, 0) }
54
49
 
55
50
  it 'instantiates an Inhibitor' do
56
51
  expect(limiter).to be_a Berater::Inhibitor
@@ -63,18 +58,13 @@ describe Berater do
63
58
 
64
59
  it 'accepts options' do
65
60
  redis = double('Redis')
66
- limiter = Berater.new(:key, :inhibited, redis: redis)
61
+ limiter = Berater.new(:key, 0, redis: redis)
67
62
  expect(limiter.redis).to be redis
68
63
  end
69
-
70
- it 'works with convinience' do
71
- expect(Berater).to receive(:new).and_return(limiter)
72
- expect { Berater(:key, :inhibited) }.to be_inhibited
73
- end
74
64
  end
75
65
 
76
66
  context 'rate mode' do
77
- let(:limiter) { Berater.new(:key, :rate, 1, :second) }
67
+ let(:limiter) { Berater.new(:key, 1, :second) }
78
68
 
79
69
  it 'instantiates a RateLimiter' do
80
70
  expect(limiter).to be_a Berater::RateLimiter
@@ -87,18 +77,13 @@ describe Berater do
87
77
 
88
78
  it 'accepts options' do
89
79
  redis = double('Redis')
90
- limiter = Berater.new(:key, :rate, 1, :second, redis: redis)
80
+ limiter = Berater.new(:key, 1, :second, redis: redis)
91
81
  expect(limiter.redis).to be redis
92
82
  end
93
-
94
- it 'works with convinience' do
95
- expect(Berater).to receive(:new).and_return(limiter)
96
- expect {|b| Berater(:key, :rate, 1, :second, &b) }.to yield_control
97
- end
98
83
  end
99
84
 
100
85
  context 'concurrency mode' do
101
- let(:limiter) { Berater.new(:key, :concurrency, 1) }
86
+ let(:limiter) { Berater.new(:key, 1) }
102
87
 
103
88
  it 'instantiates a ConcurrencyLimiter' do
104
89
  expect(limiter).to be_a Berater::ConcurrencyLimiter
@@ -111,61 +96,39 @@ describe Berater do
111
96
 
112
97
  it 'accepts options' do
113
98
  redis = double('Redis')
114
- limiter = Berater.new(:key, :concurrency, 1, redis: redis)
99
+ limiter = Berater.new(:key, 1, redis: redis)
115
100
  expect(limiter.redis).to be redis
116
101
  end
117
-
118
- it 'works with convinience' do
119
- expect(Berater).to receive(:new).and_return(limiter)
120
- expect {|b| Berater(:key, :concurrency, 1, &b) }.to yield_control
121
- end
122
102
  end
103
+ end
123
104
 
124
- context 'with DSL' do
125
- it 'instatiates an Unlimiter' do
126
- limiter = Berater.new(:key) { unlimited }
127
- expect(limiter).to be_a Berater::Unlimiter
128
- expect(limiter.key).to be :key
129
- end
130
-
131
- it 'instatiates an Inhibiter' do
132
- limiter = Berater.new(:key) { inhibited }
133
- expect(limiter).to be_a Berater::Inhibitor
134
- expect(limiter.key).to be :key
135
- end
136
-
137
- it 'instatiates a RateLimiter' do
138
- limiter = Berater.new(:key) { 1.per second }
139
- expect(limiter).to be_a Berater::RateLimiter
140
- expect(limiter.key).to be :key
141
- expect(limiter.count).to be 1
142
- expect(limiter.interval).to be :second
143
- end
144
-
145
- it 'instatiates a ConcurrencyLimiter' do
146
- limiter = Berater.new(:key, timeout: 2) { 1.at_once }
147
- expect(limiter).to be_a Berater::ConcurrencyLimiter
148
- expect(limiter.key).to be :key
149
- expect(limiter.capacity).to be 1
150
- expect(limiter.timeout).to be 2
105
+ describe 'Berater() - convenience method' do
106
+ RSpec.shared_examples 'test convenience' do |klass, *args|
107
+ it 'creates a limiter' do
108
+ limiter = Berater(:key, *args)
109
+ expect(limiter).to be_a klass
151
110
  end
152
111
 
153
- it 'does not accept mode/args and dsl block' do
154
- expect {
155
- Berater.new(:key, :rate) { 1.per second }
156
- }.to raise_error(ArgumentError)
112
+ context 'with a block' do
113
+ it 'creates a limiter and calls limit' do
114
+ limiter = Berater(:key, *args)
115
+ expect(klass).to receive(:new).and_return(limiter)
116
+ expect(limiter).to receive(:limit).and_call_original
157
117
 
158
- expect {
159
- Berater.new(:key, :concurrency, 2) { 3.at_once }
160
- }.to raise_error(ArgumentError)
161
- end
162
-
163
- it 'requires either mode or dsl block' do
164
- expect {
165
- Berater.new(:key)
166
- }.to raise_error(ArgumentError)
118
+ begin
119
+ res = Berater(:key, *args) { true }
120
+ expect(res).to be true
121
+ rescue Berater::Overloaded
122
+ expect(klass).to be Berater::Inhibitor
123
+ end
124
+ end
167
125
  end
168
126
  end
127
+
128
+ include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
129
+ include_examples 'test convenience', Berater::Inhibitor, 0
130
+ include_examples 'test convenience', Berater::RateLimiter, 1, :second
131
+ include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
169
132
  end
170
133
 
171
134
  end