berater 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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