berater 0.2.0 → 0.6.1
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.
- checksums.yaml +4 -4
- data/lib/berater.rb +30 -23
- data/lib/berater/concurrency_limiter.rb +58 -46
- data/lib/berater/dsl.rb +68 -0
- data/lib/berater/inhibitor.rb +5 -3
- data/lib/berater/limiter.rb +94 -0
- data/lib/berater/lock.rb +4 -14
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +69 -52
- data/lib/berater/rspec.rb +14 -0
- data/lib/berater/rspec/matchers.rb +81 -0
- data/lib/berater/test_mode.rb +43 -0
- data/lib/berater/unlimiter.rb +9 -14
- data/lib/berater/utils.rb +46 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +37 -28
- data/spec/concurrency_limiter_spec.rb +179 -73
- data/spec/dsl_refinement_spec.rb +46 -0
- data/spec/dsl_spec.rb +72 -0
- data/spec/inhibitor_spec.rb +2 -4
- data/spec/limiter_spec.rb +71 -0
- data/spec/lua_script_spec.rb +97 -0
- data/spec/{matcher_spec.rb → matchers_spec.rb} +71 -3
- data/spec/rate_limiter_spec.rb +156 -70
- data/spec/riddle_spec.rb +102 -0
- data/spec/test_mode_spec.rb +225 -0
- data/spec/unlimiter_spec.rb +5 -12
- data/spec/utils_spec.rb +78 -0
- metadata +40 -10
- data/lib/berater/base_limiter.rb +0 -26
- data/spec/concurrency_lock_spec.rb +0 -39
- data/spec/rate_lock_spec.rb +0 -20
@@ -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,4 +1,5 @@
|
|
1
|
-
describe
|
1
|
+
describe Berater::Matchers::Overloaded do
|
2
|
+
|
2
3
|
context 'Berater::Unlimiter' do
|
3
4
|
let(:limiter) { Berater.new(:key, :unlimited) }
|
4
5
|
|
@@ -32,7 +33,7 @@ describe 'be_overloaded' do
|
|
32
33
|
end
|
33
34
|
|
34
35
|
context 'Berater::RateLimiter' do
|
35
|
-
let(:limiter) { Berater.new(:key,
|
36
|
+
let(:limiter) { Berater.new(:key, 1, :second) }
|
36
37
|
|
37
38
|
it { expect(limiter).not_to be_overloaded }
|
38
39
|
it { expect(limiter).not_to be_inhibited }
|
@@ -67,7 +68,7 @@ describe 'be_overloaded' do
|
|
67
68
|
end
|
68
69
|
|
69
70
|
context 'Berater::ConcurrencyLimiter' do
|
70
|
-
let(:limiter) { Berater.new(:key,
|
71
|
+
let(:limiter) { Berater.new(:key, 1) }
|
71
72
|
|
72
73
|
it { expect(limiter).not_to be_overloaded }
|
73
74
|
it { expect(limiter).not_to be_inhibited }
|
@@ -115,4 +116,71 @@ describe 'be_overloaded' do
|
|
115
116
|
end
|
116
117
|
end
|
117
118
|
end
|
119
|
+
|
120
|
+
context 'when matchers fail' do
|
121
|
+
let(:unlimiter) { Berater::Unlimiter.new }
|
122
|
+
let(:inhibitor) { Berater::Inhibitor.new }
|
123
|
+
|
124
|
+
it 'catches false negatives' do
|
125
|
+
expect {
|
126
|
+
expect(unlimiter).to be_overloaded
|
127
|
+
}.to fail_including('expected to be overloaded')
|
128
|
+
|
129
|
+
expect {
|
130
|
+
expect { unlimiter }.to be_overloaded
|
131
|
+
}.to fail_including('expected to be overloaded')
|
132
|
+
|
133
|
+
expect {
|
134
|
+
expect { unlimiter.limit }.to be_overloaded
|
135
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
136
|
+
|
137
|
+
expect {
|
138
|
+
expect { 123 }.to be_overloaded
|
139
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'catches false positives' do
|
143
|
+
expect {
|
144
|
+
expect(inhibitor).not_to be_overloaded
|
145
|
+
}.to fail_including('expected not to be overloaded')
|
146
|
+
|
147
|
+
expect {
|
148
|
+
expect { inhibitor }.not_to be_overloaded
|
149
|
+
}.to fail_including('expected not to be overloaded')
|
150
|
+
|
151
|
+
expect {
|
152
|
+
expect { inhibitor.limit }.not_to be_overloaded
|
153
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
154
|
+
|
155
|
+
expect {
|
156
|
+
expect { raise Berater::Overloaded }.not_to be_overloaded
|
157
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'supports different verbs' do
|
161
|
+
expect {
|
162
|
+
expect { unlimiter }.to be_overrated
|
163
|
+
}.to fail_including('expected to be overrated')
|
164
|
+
|
165
|
+
expect {
|
166
|
+
expect { unlimiter }.to be_incapacitated
|
167
|
+
}.to fail_including('expected to be incapacitated')
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'supports different exceptions' do
|
171
|
+
expect {
|
172
|
+
expect { 123 }.to be_overrated
|
173
|
+
}.to fail_including(
|
174
|
+
"expected #{Berater::RateLimiter::Overrated} to be raised"
|
175
|
+
)
|
176
|
+
|
177
|
+
expect {
|
178
|
+
expect {
|
179
|
+
raise Berater::ConcurrencyLimiter::Incapacitated
|
180
|
+
}.not_to be_incapacitated
|
181
|
+
}.to fail_including(
|
182
|
+
"did not expect #{Berater::ConcurrencyLimiter::Incapacitated} to be raised"
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
118
186
|
end
|
data/spec/rate_limiter_spec.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
describe Berater::RateLimiter do
|
2
|
+
it_behaves_like 'a limiter', Berater.new(:key, 3, :second)
|
2
3
|
|
3
4
|
describe '.new' do
|
4
5
|
let(:limiter) { described_class.new(:key, 1, :second) }
|
5
6
|
|
6
7
|
it 'initializes' do
|
7
8
|
expect(limiter.key).to be :key
|
8
|
-
expect(limiter.
|
9
|
-
expect(limiter.interval).to eq
|
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
|
@@ -14,71 +15,48 @@ describe Berater::RateLimiter do
|
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
|
-
describe '#
|
18
|
-
def
|
19
|
-
limiter = described_class.new(:key,
|
20
|
-
expect(limiter.
|
18
|
+
describe '#capacity' do
|
19
|
+
def expect_capacity(capacity)
|
20
|
+
limiter = described_class.new(:key, capacity, :second)
|
21
|
+
expect(limiter.capacity).to eq capacity
|
21
22
|
end
|
22
23
|
|
23
|
-
it {
|
24
|
-
it {
|
25
|
-
it {
|
24
|
+
it { expect_capacity(0) }
|
25
|
+
it { expect_capacity(1) }
|
26
|
+
it { expect_capacity(1.5) }
|
27
|
+
it { expect_capacity(100) }
|
26
28
|
|
27
29
|
context 'with erroneous values' do
|
28
|
-
def
|
30
|
+
def expect_bad_capacity(capacity)
|
29
31
|
expect do
|
30
|
-
described_class.new(:key,
|
32
|
+
described_class.new(:key, capacity, :second)
|
31
33
|
end.to raise_error ArgumentError
|
32
34
|
end
|
33
35
|
|
34
|
-
it {
|
35
|
-
it {
|
36
|
-
it {
|
37
|
-
it { expect_bad_count(:one) }
|
36
|
+
it { expect_bad_capacity(-1) }
|
37
|
+
it { expect_bad_capacity('1') }
|
38
|
+
it { expect_bad_capacity(:one) }
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
42
|
describe '#interval' do
|
42
|
-
|
43
|
-
limiter = described_class.new(:key, 1, interval)
|
44
|
-
expect(limiter.interval).to eq expected
|
45
|
-
end
|
46
|
-
|
47
|
-
context 'with ints' do
|
48
|
-
it { expect_interval(0, 0) }
|
49
|
-
it { expect_interval(1, 1) }
|
50
|
-
it { expect_interval(33, 33) }
|
51
|
-
end
|
52
|
-
|
53
|
-
context 'with symbols' do
|
54
|
-
it { expect_interval(:sec, 1) }
|
55
|
-
it { expect_interval(:second, 1) }
|
56
|
-
it { expect_interval(:seconds, 1) }
|
43
|
+
# see spec/utils_spec.rb for more
|
57
44
|
|
58
|
-
|
59
|
-
it { expect_interval(:minute, 60) }
|
60
|
-
it { expect_interval(:minutes, 60) }
|
45
|
+
subject { described_class.new(:key, 1, :second) }
|
61
46
|
|
62
|
-
|
63
|
-
|
47
|
+
it 'saves the interval in original and millisecond format' do
|
48
|
+
expect(subject.interval).to be :second
|
49
|
+
expect(subject.instance_variable_get(:@interval_msec)).to be 10**3
|
64
50
|
end
|
65
51
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
context 'with erroneous values' do
|
73
|
-
def expect_bad_interval(interval)
|
74
|
-
expect do
|
75
|
-
described_class.new(:key, 1, interval)
|
76
|
-
end.to raise_error(ArgumentError)
|
77
|
-
end
|
52
|
+
it 'must be > 0' do
|
53
|
+
expect {
|
54
|
+
described_class.new(:key, 1, 0)
|
55
|
+
}.to raise_error(ArgumentError)
|
78
56
|
|
79
|
-
|
80
|
-
|
81
|
-
|
57
|
+
expect {
|
58
|
+
described_class.new(:key, 1, -1)
|
59
|
+
}.to raise_error(ArgumentError)
|
82
60
|
end
|
83
61
|
end
|
84
62
|
|
@@ -100,43 +78,151 @@ describe Berater::RateLimiter do
|
|
100
78
|
expect(limiter).to be_overrated
|
101
79
|
end
|
102
80
|
|
103
|
-
it 'limit
|
81
|
+
it 'resets limit over time' do
|
104
82
|
3.times { limiter.limit }
|
105
83
|
expect(limiter).to be_overrated
|
106
84
|
|
107
|
-
# travel forward a second
|
108
85
|
Timecop.freeze(1)
|
109
86
|
|
110
87
|
3.times { limiter.limit }
|
111
88
|
expect(limiter).to be_overrated
|
112
89
|
end
|
113
|
-
end
|
114
90
|
|
115
|
-
|
116
|
-
|
117
|
-
|
91
|
+
context 'with millisecond precision' do
|
92
|
+
it 'resets limit over time' do
|
93
|
+
3.times { limiter.limit }
|
94
|
+
expect(limiter).to be_overrated
|
95
|
+
|
96
|
+
# travel forward to just before the count decrements
|
97
|
+
Timecop.freeze(0.333)
|
98
|
+
expect(limiter).to be_overrated
|
99
|
+
|
100
|
+
# traveling one more millisecond will decrement the count
|
101
|
+
Timecop.freeze(0.001)
|
102
|
+
limiter.limit
|
103
|
+
expect(limiter).to be_overrated
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'works when drip rate is < 1 per millisecond' do
|
107
|
+
limiter = described_class.new(:key, 2_000, :second)
|
118
108
|
|
119
|
-
|
120
|
-
|
109
|
+
limiter.capacity.times { limiter.limit }
|
110
|
+
expect(limiter).to be_overrated
|
121
111
|
|
122
|
-
|
123
|
-
|
112
|
+
Timecop.freeze(0.001)
|
113
|
+
expect(limiter).not_to be_overrated
|
114
|
+
|
115
|
+
2.times { limiter.limit }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when capacity is a Float' do
|
120
|
+
let(:limiter) { described_class.new(:key, 1.5, :second) }
|
121
|
+
|
122
|
+
it 'still works' do
|
123
|
+
limiter.limit
|
124
|
+
expect(limiter).not_to be_overrated
|
125
|
+
|
126
|
+
expect { limiter.limit }.to be_overrated
|
127
|
+
|
128
|
+
limiter.limit(cost: 0.5)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'accepts a dynamic capacity' do
|
133
|
+
limiter = described_class.new(:key, 1, :second)
|
134
|
+
|
135
|
+
expect { limiter.limit(capacity: 0) }.to be_overrated
|
136
|
+
5.times { limiter.limit(capacity: 10) }
|
137
|
+
expect { limiter }.to be_overrated
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'works with cost parameter' do
|
141
|
+
it { expect { limiter.limit(cost: 4) }.to be_overrated }
|
142
|
+
|
143
|
+
it 'works within limit' do
|
144
|
+
limiter.limit(cost: 3)
|
145
|
+
expect { limiter.limit }.to be_overrated
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'resets over time' do
|
149
|
+
limiter.limit(cost: 3)
|
150
|
+
expect(limiter).to be_overrated
|
151
|
+
|
152
|
+
Timecop.freeze(1)
|
153
|
+
expect(limiter).not_to be_overrated
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'can be a Float' do
|
157
|
+
2.times { limiter.limit(cost: 1.5) }
|
158
|
+
expect(limiter).to be_overrated
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'with same key, different limiters' do
|
163
|
+
let(:limiter_one) { described_class.new(:key, 1, :second) }
|
164
|
+
let(:limiter_two) { described_class.new(:key, 1, :second) }
|
165
|
+
|
166
|
+
it 'works as expected' do
|
167
|
+
expect(limiter_one.limit).not_to be_overrated
|
168
|
+
|
169
|
+
expect(limiter_one).to be_overrated
|
170
|
+
expect(limiter_two).to be_overrated
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'with different keys, different limiters' do
|
175
|
+
let(:limiter_one) { described_class.new(:one, 1, :second) }
|
176
|
+
let(:limiter_two) { described_class.new(:two, 2, :second) }
|
177
|
+
|
178
|
+
it 'works as expected' do
|
179
|
+
expect(limiter_one.limit).not_to be_overrated
|
180
|
+
expect(limiter_two.limit).not_to be_overrated
|
181
|
+
|
182
|
+
expect(limiter_one).to be_overrated
|
183
|
+
expect(limiter_two.limit).not_to be_overrated
|
184
|
+
|
185
|
+
expect(limiter_one).to be_overrated
|
186
|
+
expect(limiter_two).to be_overrated
|
187
|
+
end
|
124
188
|
end
|
125
189
|
end
|
126
190
|
|
127
|
-
|
128
|
-
let(:
|
129
|
-
let(:limiter_two) { described_class.new(:two, 2, :second) }
|
191
|
+
describe '#overloaded?' do
|
192
|
+
let(:limiter) { described_class.new(:key, 1, :second) }
|
130
193
|
|
131
|
-
it 'works
|
132
|
-
expect(
|
133
|
-
|
194
|
+
it 'works' do
|
195
|
+
expect(limiter.overloaded?).to be false
|
196
|
+
limiter.limit
|
197
|
+
expect(limiter.overloaded?).to be true
|
198
|
+
Timecop.freeze(1)
|
199
|
+
expect(limiter.overloaded?).to be false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe '#to_s' do
|
204
|
+
def check(capacity, interval, expected)
|
205
|
+
expect(
|
206
|
+
described_class.new(:key, capacity, interval).to_s
|
207
|
+
).to match(expected)
|
208
|
+
end
|
134
209
|
|
135
|
-
|
136
|
-
|
210
|
+
it 'works with symbols' do
|
211
|
+
check(1, :second, /1 per second/)
|
212
|
+
check(1, :minute, /1 per minute/)
|
213
|
+
check(1, :hour, /1 per hour/)
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'works with strings' do
|
217
|
+
check(1, 'second', /1 per second/)
|
218
|
+
check(1, 'minute', /1 per minute/)
|
219
|
+
check(1, 'hour', /1 per hour/)
|
220
|
+
end
|
137
221
|
|
138
|
-
|
139
|
-
|
222
|
+
it 'works with integers' do
|
223
|
+
check(1, 1, /1 every second/)
|
224
|
+
check(1, 2, /1 every 2 seconds/)
|
225
|
+
check(2, 3, /2 every 3 seconds/)
|
140
226
|
end
|
141
227
|
end
|
142
228
|
|