berater 0.1.3 → 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.
@@ -0,0 +1,46 @@
1
+ require 'berater/dsl'
2
+
3
+ describe Berater do
4
+ using Berater::DSL
5
+
6
+ it 'instatiates an Unlimiter' do
7
+ limiter = Berater.new(:key) { unlimited }
8
+ expect(limiter).to be_a Berater::Unlimiter
9
+ expect(limiter.key).to be :key
10
+ end
11
+
12
+ it 'instatiates an Inhibiter' do
13
+ limiter = Berater.new(:key) { inhibited }
14
+ expect(limiter).to be_a Berater::Inhibitor
15
+ expect(limiter.key).to be :key
16
+ end
17
+
18
+ it 'instatiates a RateLimiter' do
19
+ limiter = Berater.new(:key) { 1.per second }
20
+ expect(limiter).to be_a Berater::RateLimiter
21
+ expect(limiter.key).to be :key
22
+ expect(limiter.capacity).to be 1
23
+ expect(limiter.interval).to be :second
24
+ end
25
+
26
+ it 'instatiates a ConcurrencyLimiter' do
27
+ limiter = Berater.new(:key, timeout: 2) { 1.at_once }
28
+ expect(limiter).to be_a Berater::ConcurrencyLimiter
29
+ expect(limiter.key).to be :key
30
+ expect(limiter.capacity).to be 1
31
+ expect(limiter.timeout).to be 2
32
+ end
33
+
34
+ it 'does not accept args and dsl block' do
35
+ expect {
36
+ Berater.new(:key, 2) { 3.at_once }
37
+ }.to raise_error(ArgumentError)
38
+ end
39
+
40
+ it 'requires either mode or dsl block' do
41
+ expect {
42
+ Berater.new(:key)
43
+ }.to raise_error(ArgumentError)
44
+ end
45
+
46
+ end
data/spec/dsl_spec.rb ADDED
@@ -0,0 +1,72 @@
1
+ require 'berater/dsl'
2
+
3
+ describe Berater::DSL do
4
+ def check(expected, &block)
5
+ expect(Berater::DSL.eval(&block)).to eq expected
6
+ end
7
+
8
+ context 'rate mode' do
9
+ it 'has keywords' do
10
+ check(:second) { second }
11
+ check(:minute) { minute }
12
+ check(:hour) { hour }
13
+ end
14
+
15
+ it 'parses' do
16
+ check([ 1, :second ]) { 1.per second }
17
+ check([ 3, :minute ]) { 3.per minute }
18
+ check([ 5, :hour ]) { 5.every hour }
19
+ end
20
+
21
+ it 'cleans up afterward' do
22
+ check([ 1, :second ]) { 1.per second }
23
+
24
+ expect(Integer).not_to respond_to(:per)
25
+ expect(Integer).not_to respond_to(:every)
26
+ end
27
+
28
+ it 'works with variables' do
29
+ count = 1
30
+ interval = :second
31
+
32
+ check([ count, interval ]) { count.per interval }
33
+ end
34
+ end
35
+
36
+ context 'concurrency mode' do
37
+ it 'parses' do
38
+ check([ 1 ]) { 1.at_once }
39
+ check([ 3 ]) { 3.at_a_time }
40
+ check([ 5 ]) { 5.concurrently }
41
+ end
42
+
43
+ it 'cleans up afterward' do
44
+ check([ 1 ]) { 1.at_once }
45
+
46
+ expect(Integer).not_to respond_to(:at_once)
47
+ expect(Integer).not_to respond_to(:at_a_time)
48
+ expect(Integer).not_to respond_to(:concurrently)
49
+ end
50
+
51
+ it 'works with constants' do
52
+ class Foo
53
+ CAPACITY = 3
54
+ end
55
+
56
+ check([ Foo::CAPACITY ]) { Foo::CAPACITY.at_once }
57
+ end
58
+ end
59
+
60
+ context 'unlimited mode' do
61
+ it 'has keywords' do
62
+ check(:unlimited) { unlimited }
63
+ end
64
+ end
65
+
66
+ context 'inhibited mode' do
67
+ it 'has keywords' do
68
+ check(:inhibited) { inhibited }
69
+ end
70
+ end
71
+
72
+ end
@@ -1,37 +1,23 @@
1
1
  describe Berater::Inhibitor do
2
- before { Berater.mode = :inhibited }
3
-
4
2
  describe '.new' do
5
3
  it 'initializes without any arguments or options' do
6
4
  expect(described_class.new).to be_a described_class
7
5
  end
8
6
 
9
7
  it 'initializes with any arguments and options' do
10
- expect(described_class.new(:abc, x: 123)).to be_a described_class
8
+ expect(described_class.new(:abc, :def, x: 123)).to be_a described_class
11
9
  end
12
10
 
13
11
  it 'has default values' do
14
- expect(described_class.new.key).to eq described_class.to_s
12
+ expect(described_class.new.key).to be :inhibitor
15
13
  expect(described_class.new.redis).to be Berater.redis
16
14
  end
17
15
  end
18
16
 
19
- describe '.limit' do
20
- it 'always limits' do
21
- expect { described_class.limit }.to be_inhibited
22
- end
23
-
24
- it 'works with any arguments or options' do
25
- expect { described_class.limit(:abc, x: 123) }.to be_inhibited
26
- end
27
- end
28
-
29
17
  describe '#limit' do
30
- let(:limiter) { described_class.new }
18
+ subject { described_class.new }
31
19
 
32
- it 'always limits' do
33
- expect { described_class.limit }.to be_inhibited
34
- end
20
+ it_behaves_like 'it is overloaded'
35
21
  end
36
22
 
37
23
  end
@@ -0,0 +1,71 @@
1
+ describe Berater::Limiter do
2
+ it 'can not be initialized' do
3
+ expect { described_class.new }.to raise_error(NotImplementedError)
4
+ end
5
+
6
+ describe 'abstract methods' do
7
+ let(:limiter) { Class.new(described_class).new(:key, 1) }
8
+
9
+ it do
10
+ expect { limiter.limit }.to raise_error(NotImplementedError)
11
+ expect { limiter.overloaded? }.to raise_error(NotImplementedError)
12
+ end
13
+ end
14
+
15
+ describe '==' do
16
+ let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
17
+
18
+ it 'equals itself' do
19
+ expect(limiter).to eq limiter
20
+ end
21
+
22
+ it 'equals something with the same initialization parameters' do
23
+ expect(limiter).to eq(
24
+ Berater::RateLimiter.new(:key, 1, :second)
25
+ )
26
+ end
27
+
28
+ it 'equals something with equvalent initialization parameters' do
29
+ expect(limiter).to eq(
30
+ Berater::RateLimiter.new(:key, 1, 1)
31
+ )
32
+ end
33
+
34
+ it 'does not equal something different' do
35
+ expect(limiter).not_to eq(
36
+ Berater::RateLimiter.new(:key, 2, :second)
37
+ )
38
+
39
+ expect(limiter).not_to eq(
40
+ Berater::RateLimiter.new(:keyz, 1, :second)
41
+ )
42
+
43
+ expect(limiter).not_to eq(
44
+ Berater::RateLimiter.new(:key, 1, :minute)
45
+ )
46
+ end
47
+
48
+ it 'does not equal something altogether different' do
49
+ expect(limiter).not_to eq(
50
+ Berater::ConcurrencyLimiter.new(:key, 1)
51
+ )
52
+ end
53
+
54
+ it 'works for ConcurrencyLimiter too' do
55
+ limiter = Berater::ConcurrencyLimiter.new(:key, 1)
56
+ expect(limiter).to eq limiter
57
+
58
+ expect(limiter).not_to eq(
59
+ Berater::ConcurrencyLimiter.new(:key, 1, timeout: 1)
60
+ )
61
+ end
62
+
63
+ it 'and the others' do
64
+ unlimiter = Berater::Unlimiter.new
65
+ expect(unlimiter).to eq unlimiter
66
+
67
+ expect(unlimiter).not_to eq Berater::Inhibitor.new
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,97 @@
1
+ describe Berater::LuaScript do
2
+ subject { Berater::LuaScript('return redis.call("PING")') }
3
+
4
+ before { redis.script(:flush) }
5
+
6
+ let(:redis) { Berater.redis }
7
+
8
+ it { is_expected.to be_a Berater::LuaScript }
9
+
10
+ describe '#eval' do
11
+ def ping
12
+ expect(subject.eval(redis)).to eq 'PONG'
13
+ end
14
+
15
+ it { ping }
16
+
17
+ it 'loads the script into redis' do
18
+ expect(redis).to receive(:evalsha).once.and_call_original
19
+ expect(redis).to receive(:eval).once.and_call_original
20
+ ping
21
+ expect(subject.loaded?(redis)).to be true
22
+ end
23
+ end
24
+
25
+ describe '#load' do
26
+ it 'loads script into redis' do
27
+ expect(redis.script(:exists, subject.sha)).to be false
28
+ subject.load(redis)
29
+ expect(redis.script(:exists, subject.sha)).to be true
30
+ end
31
+
32
+ it 'returns the sha' do
33
+ expect(subject.load(redis)).to eq subject.sha
34
+ end
35
+
36
+ it 'validates the returned sha' do
37
+ allow(redis).to receive(:script).with(:flush).and_call_original
38
+ expect(redis).to receive(:script).with(:load, String).and_return('abc')
39
+ expect { subject.load(redis) }.to raise_error(RuntimeError)
40
+ end
41
+ end
42
+
43
+ describe '#loaded?' do
44
+ it do
45
+ expect(subject.loaded?(redis)).to be false
46
+ subject.load(redis)
47
+ expect(subject.loaded?(redis)).to be true
48
+ end
49
+ end
50
+
51
+ describe '#to_s' do
52
+ it { expect(subject.to_s).to be subject.source }
53
+ end
54
+
55
+ describe '#minify' do
56
+ subject do
57
+ expect(
58
+ Berater::LuaScript(lua).send(:minify)
59
+ ).to eq expected
60
+ end
61
+
62
+ context do
63
+ let(:lua) do <<-LUA
64
+ -- this comment gets removed
65
+ redis.call('PING') -- this one too
66
+ LUA
67
+ end
68
+
69
+ let(:expected) { "redis.call('PING')" }
70
+
71
+ it { subject }
72
+ end
73
+
74
+ context 'with if statement' do
75
+ let(:lua) do <<~LUA
76
+ if condition then
77
+ call
78
+ end
79
+
80
+ return 123
81
+ LUA
82
+ end
83
+
84
+ let(:expected) do
85
+ [
86
+ 'if condition then',
87
+ 'call',
88
+ 'end',
89
+ 'return 123'
90
+ ].join "\n"
91
+ end
92
+
93
+ it { subject }
94
+ end
95
+ end
96
+
97
+ end
@@ -1,6 +1,6 @@
1
1
  describe 'be_overloaded' do
2
2
  context 'Berater::Unlimiter' do
3
- let(:limiter) { Berater.new(:unlimited) }
3
+ let(:limiter) { Berater.new(:key, :unlimited) }
4
4
 
5
5
  it { expect(limiter).not_to be_overloaded }
6
6
  it { expect(limiter).not_to be_inhibited }
@@ -16,10 +16,16 @@ describe 'be_overloaded' do
16
16
  it { expect { limiter.limit }.not_to be_inhibited }
17
17
  it { expect { limiter.limit }.not_to be_overrated }
18
18
  it { expect { limiter.limit }.not_to be_incapacitated }
19
+
20
+ it 'catches false positives' do
21
+ expect {
22
+ expect { limiter }.to be_overloaded
23
+ }.to fail
24
+ end
19
25
  end
20
26
 
21
27
  context 'Berater::Inhibitor' do
22
- let(:limiter) { Berater.new(:inhibited) }
28
+ let(:limiter) { Berater.new(:key, :inhibited) }
23
29
 
24
30
  it { expect(limiter).to be_overloaded }
25
31
  it { expect(limiter).to be_inhibited }
@@ -29,10 +35,16 @@ describe 'be_overloaded' do
29
35
 
30
36
  it { expect { limiter.limit }.to be_overloaded }
31
37
  it { expect { limiter.limit }.to be_inhibited }
38
+
39
+ it 'catches false negatives' do
40
+ expect {
41
+ expect { limiter }.not_to be_overloaded
42
+ }.to fail
43
+ end
32
44
  end
33
45
 
34
46
  context 'Berater::RateLimiter' do
35
- let(:limiter) { Berater.new(:rate, 1, :second) }
47
+ let(:limiter) { Berater.new(:key, 1, :second) }
36
48
 
37
49
  it { expect(limiter).not_to be_overloaded }
38
50
  it { expect(limiter).not_to be_inhibited }
@@ -67,7 +79,7 @@ describe 'be_overloaded' do
67
79
  end
68
80
 
69
81
  context 'Berater::ConcurrencyLimiter' do
70
- let(:limiter) { Berater.new(:concurrency, 1) }
82
+ let(:limiter) { Berater.new(:key, 1) }
71
83
 
72
84
  it { expect(limiter).not_to be_overloaded }
73
85
  it { expect(limiter).not_to be_inhibited }
@@ -1,165 +1,187 @@
1
1
  describe Berater::RateLimiter do
2
- before { Berater.mode = :rate }
2
+ it_behaves_like 'a limiter', Berater.new(:key, 3, :second)
3
3
 
4
4
  describe '.new' do
5
- let(:limiter) { described_class.new(1, :second) }
5
+ let(:limiter) { described_class.new(:key, 1, :second) }
6
6
 
7
7
  it 'initializes' do
8
- expect(limiter.count).to eq 1
9
- expect(limiter.interval).to eq 1
8
+ expect(limiter.key).to be :key
9
+ expect(limiter.capacity).to eq 1
10
+ expect(limiter.interval).to eq :second
10
11
  end
11
12
 
12
13
  it 'has default values' do
13
- expect(limiter.key).to eq described_class.to_s
14
14
  expect(limiter.redis).to be Berater.redis
15
15
  end
16
16
  end
17
17
 
18
- describe '#count' do
19
- def expect_count(count)
20
- limiter = described_class.new(count, :second)
21
- expect(limiter.count).to eq count
18
+ describe '#capacity' do
19
+ def expect_capacity(capacity)
20
+ limiter = described_class.new(:key, capacity, :second)
21
+ expect(limiter.capacity).to eq capacity
22
22
  end
23
23
 
24
- it { expect_count(0) }
25
- it { expect_count(1) }
26
- it { expect_count(100) }
24
+ it { expect_capacity(0) }
25
+ it { expect_capacity(1) }
26
+ it { expect_capacity(100) }
27
27
 
28
28
  context 'with erroneous values' do
29
- def expect_bad_count(count)
29
+ def expect_bad_capacity(capacity)
30
30
  expect do
31
- described_class.new(count, :second)
31
+ described_class.new(:key, capacity, :second)
32
32
  end.to raise_error ArgumentError
33
33
  end
34
34
 
35
- it { expect_bad_count(0.5) }
36
- it { expect_bad_count(-1) }
37
- it { expect_bad_count('1') }
38
- it { expect_bad_count(:one) }
35
+ it { expect_bad_capacity(0.5) }
36
+ it { expect_bad_capacity(-1) }
37
+ it { expect_bad_capacity('1') }
38
+ it { expect_bad_capacity(:one) }
39
39
  end
40
40
  end
41
41
 
42
42
  describe '#interval' do
43
- def expect_interval(interval, expected)
44
- limiter = described_class.new(1, interval)
45
- expect(limiter.interval).to eq expected
46
- end
47
-
48
- context 'with ints' do
49
- it { expect_interval(0, 0) }
50
- it { expect_interval(1, 1) }
51
- it { expect_interval(33, 33) }
52
- end
53
-
54
- context 'with symbols' do
55
- it { expect_interval(:sec, 1) }
56
- it { expect_interval(:second, 1) }
57
- it { expect_interval(:seconds, 1) }
43
+ # see spec/utils_spec.rb for more
58
44
 
59
- it { expect_interval(:min, 60) }
60
- it { expect_interval(:minute, 60) }
61
- it { expect_interval(:minutes, 60) }
45
+ subject { described_class.new(:key, 1, :second) }
62
46
 
63
- it { expect_interval(:hour, 3600) }
64
- it { expect_interval(:hours, 3600) }
65
- end
66
-
67
- context 'with strings' do
68
- it { expect_interval('sec', 1) }
69
- it { expect_interval('minute', 60) }
70
- it { expect_interval('hours', 3600) }
71
- end
72
-
73
- context 'with erroneous values' do
74
- def expect_bad_interval(interval)
75
- expect do
76
- described_class.new(1, interval)
77
- end.to raise_error(ArgumentError)
78
- end
79
-
80
- it { expect_bad_interval(-1) }
81
- it { expect_bad_interval(:secondz) }
82
- it { expect_bad_interval('huor') }
47
+ it 'saves the interval in original and microsecond format' do
48
+ expect(subject.interval).to be :second
49
+ expect(subject.instance_variable_get(:@interval_usec)).to be 10**6
83
50
  end
84
51
  end
85
52
 
86
53
  describe '#limit' do
87
- let(:limiter) { described_class.new(3, :second) }
54
+ let(:limiter) { described_class.new(:key, 3, :second) }
88
55
 
89
56
  it 'works' do
90
- expect(limiter.limit).to eq 1
91
- end
92
-
93
- it 'counts' do
94
- expect(limiter.limit).to eq 1
95
- expect(limiter.limit).to eq 2
96
- expect(limiter.limit).to eq 3
97
- end
98
-
99
- it 'yields' do
100
57
  expect {|b| limiter.limit(&b) }.to yield_control
101
58
  expect(limiter.limit { 123 }).to eq 123
102
59
  end
103
60
 
61
+ it 'works without a block' do
62
+ expect(limiter.limit).to be_a Berater::Lock
63
+ end
64
+
104
65
  it 'limits excessive calls' do
105
66
  3.times { limiter.limit }
106
67
 
107
- expect { limiter.limit }.to be_overrated
68
+ expect(limiter).to be_overrated
108
69
  end
109
70
 
110
- it 'limit resets over time' do
111
- expect(limiter.limit).to eq 1
112
- expect(limiter.limit).to eq 2
113
- expect(limiter.limit).to eq 3
71
+ it 'limit resets over time, with millisecond precision' do
72
+ 3.times { limiter.limit }
114
73
  expect(limiter).to be_overrated
115
74
 
116
- # travel forward a second
75
+ # travel forward to just before the count decrements
76
+ Timecop.freeze(0.333)
77
+ expect(limiter).to be_overrated
78
+
79
+ # traveling one more millisecond will decrement the count
80
+ Timecop.freeze(0.001)
81
+ limiter.limit
82
+ expect(limiter).to be_overrated
83
+
84
+ # traveling 1 second will reset the count
117
85
  Timecop.freeze(1)
118
86
 
119
- expect(limiter.limit).to eq 1
120
- expect(limiter.limit).to eq 2
121
- expect(limiter.limit).to eq 3
87
+ 3.times { limiter.limit }
122
88
  expect(limiter).to be_overrated
123
89
  end
124
- end
125
90
 
126
- context 'with same key, different limiters' do
127
- let(:limiter_one) { described_class.new(1, :second) }
128
- let(:limiter_two) { described_class.new(1, :second) }
91
+ it 'accepts a dynamic capacity' do
92
+ limiter = described_class.new(:key, 1, :second)
129
93
 
130
- it 'works as expected' do
131
- expect(limiter_one.limit).to eq 1
94
+ expect { limiter.limit(capacity: 0) }.to be_overrated
95
+ 5.times { limiter.limit(capacity: 10) }
96
+ expect { limiter }.to be_overrated
97
+ end
98
+
99
+ context 'works with cost parameter' do
100
+ it { expect { limiter.limit(cost: 4) }.to be_overrated }
101
+
102
+ it 'works within limit' do
103
+ limiter.limit(cost: 3)
104
+ expect { limiter.limit }.to be_overrated
105
+ end
106
+
107
+ it 'resets over time' do
108
+ limiter.limit(cost: 3)
109
+ expect(limiter).to be_overrated
110
+
111
+ Timecop.freeze(1)
112
+ expect(limiter).not_to be_overrated
113
+ end
132
114
 
133
- expect { limiter_one }.to be_overrated
134
- expect { limiter_two }.to be_overrated
115
+ it 'can be a Float' do
116
+ 2.times { limiter.limit(cost: 1.5) }
117
+ expect(limiter).to be_overrated
118
+ end
135
119
  end
136
- end
137
120
 
138
- context 'with different keys, same limiter' do
139
- let(:limiter) { described_class.new(1, :second) }
121
+ context 'with same key, different limiters' do
122
+ let(:limiter_one) { described_class.new(:key, 1, :second) }
123
+ let(:limiter_two) { described_class.new(:key, 1, :second) }
140
124
 
141
- it 'works as expected' do
142
- expect { limiter.limit(key: :one) }.not_to be_overrated
143
- expect { limiter.limit(key: :one) }.to be_overrated
125
+ it 'works as expected' do
126
+ expect(limiter_one.limit).not_to be_overrated
144
127
 
145
- expect { limiter.limit(key: :two) }.not_to be_overrated
146
- expect { limiter.limit(key: :two) }.to be_overrated
128
+ expect(limiter_one).to be_overrated
129
+ expect(limiter_two).to be_overrated
130
+ end
131
+ end
132
+
133
+ context 'with different keys, different limiters' do
134
+ let(:limiter_one) { described_class.new(:one, 1, :second) }
135
+ let(:limiter_two) { described_class.new(:two, 2, :second) }
136
+
137
+ it 'works as expected' do
138
+ expect(limiter_one.limit).not_to be_overrated
139
+ expect(limiter_two.limit).not_to be_overrated
140
+
141
+ expect(limiter_one).to be_overrated
142
+ expect(limiter_two.limit).not_to be_overrated
143
+
144
+ expect(limiter_one).to be_overrated
145
+ expect(limiter_two).to be_overrated
146
+ end
147
+ end
148
+ end
149
+
150
+ describe '#overloaded?' do
151
+ let(:limiter) { described_class.new(:key, 1, :second) }
152
+
153
+ it 'works' do
154
+ expect(limiter.overloaded?).to be false
155
+ limiter.limit
156
+ expect(limiter.overloaded?).to be true
157
+ Timecop.freeze(1)
158
+ expect(limiter.overloaded?).to be false
147
159
  end
148
160
  end
149
161
 
150
- context 'with different keys, different limiters' do
151
- let(:limiter_one) { described_class.new(1, :second, key: :one) }
152
- let(:limiter_two) { described_class.new(2, :second, key: :two) }
162
+ describe '#to_s' do
163
+ def check(capacity, interval, expected)
164
+ expect(
165
+ described_class.new(:key, capacity, interval).to_s
166
+ ).to match(expected)
167
+ end
153
168
 
154
- it 'works as expected' do
155
- expect(limiter_one.limit).to eq 1
156
- expect(limiter_two.limit).to eq 1
169
+ it 'works with symbols' do
170
+ check(1, :second, /1 per second/)
171
+ check(1, :minute, /1 per minute/)
172
+ check(1, :hour, /1 per hour/)
173
+ end
157
174
 
158
- expect { limiter_one.limit }.to be_overrated
159
- expect(limiter_two.limit).to eq 2
175
+ it 'works with strings' do
176
+ check(1, 'second', /1 per second/)
177
+ check(1, 'minute', /1 per minute/)
178
+ check(1, 'hour', /1 per hour/)
179
+ end
160
180
 
161
- expect { limiter_one.limit }.to be_overrated
162
- expect { limiter_two.limit }.to be_overrated
181
+ it 'works with integers' do
182
+ check(1, 1, /1 every second/)
183
+ check(1, 2, /1 every 2 seconds/)
184
+ check(2, 3, /2 every 3 seconds/)
163
185
  end
164
186
  end
165
187