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.
- checksums.yaml +4 -4
- data/lib/berater.rb +33 -35
- data/lib/berater/concurrency_limiter.rb +63 -110
- data/lib/berater/dsl.rb +68 -0
- data/lib/berater/inhibitor.rb +9 -10
- data/lib/berater/limiter.rb +76 -0
- data/lib/berater/lock.rb +27 -0
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +74 -55
- data/lib/berater/rspec.rb +14 -0
- data/lib/berater/rspec/matchers.rb +62 -0
- data/lib/berater/test_mode.rb +52 -0
- data/lib/berater/unlimiter.rb +7 -9
- data/lib/berater/utils.rb +46 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +43 -101
- data/spec/concurrency_limiter_spec.rb +163 -99
- data/spec/dsl_refinement_spec.rb +46 -0
- data/spec/dsl_spec.rb +72 -0
- data/spec/inhibitor_spec.rb +4 -18
- data/spec/limiter_spec.rb +71 -0
- data/spec/lua_script_spec.rb +97 -0
- data/spec/{matcher_spec.rb → matchers_spec.rb} +16 -4
- data/spec/rate_limiter_spec.rb +124 -102
- data/spec/riddle_spec.rb +102 -0
- data/spec/test_mode_spec.rb +213 -0
- data/spec/unlimiter_spec.rb +6 -37
- data/spec/utils_spec.rb +78 -0
- metadata +41 -8
- data/lib/berater/base_limiter.rb +0 -32
- data/spec/concurrency_lock_spec.rb +0 -50
data/lib/berater/version.rb
CHANGED
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 '
|
10
|
+
it 'is used with a block' do
|
11
11
|
Berater.configure do |c|
|
12
|
-
c.
|
12
|
+
c.redis = :redis
|
13
13
|
end
|
14
14
|
|
15
|
-
expect(Berater.
|
15
|
+
expect(Berater.redis).to be :redis
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -24,20 +24,13 @@ describe Berater do
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
describe '.
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
context 'unlimited mode' do
|
34
|
-
before { Berater.mode = :unlimited }
|
35
|
-
|
36
|
-
describe '.new' do
|
37
|
-
let(:limiter) { Berater.new(:unlimited) }
|
27
|
+
describe '.new' do
|
28
|
+
context 'unlimited mode' do
|
29
|
+
let(:limiter) { Berater.new(:key, Float::INFINITY) }
|
38
30
|
|
39
31
|
it 'instantiates an Unlimiter' do
|
40
32
|
expect(limiter).to be_a Berater::Unlimiter
|
33
|
+
expect(limiter.key).to be :key
|
41
34
|
end
|
42
35
|
|
43
36
|
it 'inherits redis' do
|
@@ -46,35 +39,17 @@ describe Berater do
|
|
46
39
|
|
47
40
|
it 'accepts options' do
|
48
41
|
redis = double('Redis')
|
49
|
-
limiter = Berater.new(:
|
50
|
-
expect(limiter.key).to match(/key/)
|
42
|
+
limiter = Berater.new(:key, Float::INFINITY, redis: redis)
|
51
43
|
expect(limiter.redis).to be redis
|
52
44
|
end
|
53
45
|
end
|
54
46
|
|
55
|
-
|
56
|
-
|
57
|
-
expect(Berater.limit).to be_nil
|
58
|
-
end
|
59
|
-
|
60
|
-
it 'yields' do
|
61
|
-
expect {|b| Berater.limit(&b) }.to yield_control
|
62
|
-
end
|
63
|
-
|
64
|
-
it 'never limits' do
|
65
|
-
10.times { expect(Berater.limit { 123 } ).to eq 123 }
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
context 'inhibited mode' do
|
71
|
-
before { Berater.mode = :inhibited }
|
72
|
-
|
73
|
-
describe '.new' do
|
74
|
-
let(:limiter) { Berater.new(:inhibited) }
|
47
|
+
context 'inhibited mode' do
|
48
|
+
let(:limiter) { Berater.new(:key, 0) }
|
75
49
|
|
76
50
|
it 'instantiates an Inhibitor' do
|
77
51
|
expect(limiter).to be_a Berater::Inhibitor
|
52
|
+
expect(limiter.key).to be :key
|
78
53
|
end
|
79
54
|
|
80
55
|
it 'inherits redis' do
|
@@ -83,27 +58,17 @@ describe Berater do
|
|
83
58
|
|
84
59
|
it 'accepts options' do
|
85
60
|
redis = double('Redis')
|
86
|
-
limiter = Berater.new(:
|
87
|
-
expect(limiter.key).to match(/key/)
|
61
|
+
limiter = Berater.new(:key, 0, redis: redis)
|
88
62
|
expect(limiter.redis).to be redis
|
89
63
|
end
|
90
64
|
end
|
91
65
|
|
92
|
-
|
93
|
-
|
94
|
-
expect { Berater.limit }.to be_inhibited
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
context 'rate mode' do
|
100
|
-
before { Berater.mode = :rate }
|
101
|
-
|
102
|
-
describe '.limiter' do
|
103
|
-
let(:limiter) { Berater.new(:rate, 1, :second) }
|
66
|
+
context 'rate mode' do
|
67
|
+
let(:limiter) { Berater.new(:key, 1, :second) }
|
104
68
|
|
105
69
|
it 'instantiates a RateLimiter' do
|
106
70
|
expect(limiter).to be_a Berater::RateLimiter
|
71
|
+
expect(limiter.key).to be :key
|
107
72
|
end
|
108
73
|
|
109
74
|
it 'inherits redis' do
|
@@ -112,44 +77,17 @@ describe Berater do
|
|
112
77
|
|
113
78
|
it 'accepts options' do
|
114
79
|
redis = double('Redis')
|
115
|
-
limiter = Berater.new(:
|
116
|
-
expect(limiter.key).to match(/key/)
|
80
|
+
limiter = Berater.new(:key, 1, :second, redis: redis)
|
117
81
|
expect(limiter.redis).to be redis
|
118
82
|
end
|
119
83
|
end
|
120
84
|
|
121
|
-
|
122
|
-
|
123
|
-
expect(Berater.limit(1, :second)).to eq 1
|
124
|
-
end
|
125
|
-
|
126
|
-
it 'yields' do
|
127
|
-
expect {|b| Berater.limit(2, :second, &b) }.to yield_control
|
128
|
-
expect(Berater.limit(2, :second) { 123 }).to eq 123
|
129
|
-
end
|
130
|
-
|
131
|
-
it 'limits excessive calls' do
|
132
|
-
expect(Berater.limit(1, :second)).to eq 1
|
133
|
-
expect { Berater.limit(1, :second) }.to be_overrated
|
134
|
-
end
|
135
|
-
|
136
|
-
it 'accepts options' do
|
137
|
-
redis = double('Redis')
|
138
|
-
expect(redis).to receive(:multi)
|
139
|
-
|
140
|
-
Berater.limit(1, :second, redis: redis) rescue nil
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
context 'concurrency mode' do
|
146
|
-
before { Berater.mode = :concurrency }
|
147
|
-
|
148
|
-
describe '.limiter' do
|
149
|
-
let(:limiter) { Berater.new(:concurrency, 1) }
|
85
|
+
context 'concurrency mode' do
|
86
|
+
let(:limiter) { Berater.new(:key, 1) }
|
150
87
|
|
151
88
|
it 'instantiates a ConcurrencyLimiter' do
|
152
89
|
expect(limiter).to be_a Berater::ConcurrencyLimiter
|
90
|
+
expect(limiter.key).to be :key
|
153
91
|
end
|
154
92
|
|
155
93
|
it 'inherits redis' do
|
@@ -158,35 +96,39 @@ describe Berater do
|
|
158
96
|
|
159
97
|
it 'accepts options' do
|
160
98
|
redis = double('Redis')
|
161
|
-
limiter = Berater.new(:
|
162
|
-
expect(limiter.key).to match(/key/)
|
99
|
+
limiter = Berater.new(:key, 1, redis: redis)
|
163
100
|
expect(limiter.redis).to be redis
|
164
101
|
end
|
165
102
|
end
|
103
|
+
end
|
166
104
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
expect(
|
172
|
-
end
|
173
|
-
|
174
|
-
it 'yields' do
|
175
|
-
expect {|b| Berater.limit(1, &b) }.to yield_control
|
176
|
-
end
|
177
|
-
|
178
|
-
it 'limits excessive calls' do
|
179
|
-
Berater.limit(1)
|
180
|
-
expect { Berater.limit(1) }.to be_incapacitated
|
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
|
181
110
|
end
|
182
111
|
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
186
117
|
|
187
|
-
|
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
|
188
125
|
end
|
189
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
|
190
132
|
end
|
191
133
|
|
192
134
|
end
|
@@ -1,22 +1,23 @@
|
|
1
1
|
describe Berater::ConcurrencyLimiter do
|
2
|
-
|
2
|
+
it_behaves_like 'a limiter', described_class.new(:key, 1)
|
3
|
+
it_behaves_like 'a limiter', described_class.new(:key, 1, timeout: 1)
|
3
4
|
|
4
5
|
describe '.new' do
|
5
|
-
let(:limiter) { described_class.new(1) }
|
6
|
+
let(:limiter) { described_class.new(:key, 1) }
|
6
7
|
|
7
8
|
it 'initializes' do
|
9
|
+
expect(limiter.key).to be :key
|
8
10
|
expect(limiter.capacity).to be 1
|
9
11
|
end
|
10
12
|
|
11
13
|
it 'has default values' do
|
12
|
-
expect(limiter.key).to eq described_class.to_s
|
13
14
|
expect(limiter.redis).to be Berater.redis
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
18
|
describe '#capacity' do
|
18
19
|
def expect_capacity(capacity)
|
19
|
-
limiter = described_class.new(capacity)
|
20
|
+
limiter = described_class.new(:key, capacity)
|
20
21
|
expect(limiter.capacity).to eq capacity
|
21
22
|
end
|
22
23
|
|
@@ -27,7 +28,7 @@ describe Berater::ConcurrencyLimiter do
|
|
27
28
|
context 'with erroneous values' do
|
28
29
|
def expect_bad_capacity(capacity)
|
29
30
|
expect do
|
30
|
-
described_class.new(capacity)
|
31
|
+
described_class.new(:key, capacity)
|
31
32
|
end.to raise_error ArgumentError
|
32
33
|
end
|
33
34
|
|
@@ -39,150 +40,213 @@ describe Berater::ConcurrencyLimiter do
|
|
39
40
|
end
|
40
41
|
|
41
42
|
describe '#timeout' do
|
42
|
-
|
43
|
-
limiter = described_class.new(1, timeout: timeout)
|
44
|
-
expect(limiter.timeout).to eq timeout
|
45
|
-
end
|
46
|
-
|
47
|
-
it { expect_timeout(0) }
|
48
|
-
it { expect_timeout(1) }
|
49
|
-
it { expect_timeout(10_000) }
|
43
|
+
# see spec/utils_spec.rb
|
50
44
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
45
|
+
it 'saves the interval in original and microsecond format' do
|
46
|
+
limiter = described_class.new(:key, 1, timeout: 3)
|
47
|
+
expect(limiter.timeout).to be 3
|
48
|
+
expect(limiter.instance_variable_get(:@timeout_usec)).to be (3 * 10**6)
|
49
|
+
end
|
57
50
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
51
|
+
it 'handles infinity' do
|
52
|
+
limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
|
53
|
+
expect(limiter.timeout).to be Float::INFINITY
|
54
|
+
expect(limiter.instance_variable_get(:@timeout_usec)).to be 0
|
62
55
|
end
|
63
56
|
end
|
64
57
|
|
65
58
|
describe '#limit' do
|
66
|
-
let(:limiter) { described_class.new(2, timeout:
|
59
|
+
let(:limiter) { described_class.new(:key, 2, timeout: 30) }
|
67
60
|
|
68
61
|
it 'works' do
|
69
62
|
expect {|b| limiter.limit(&b) }.to yield_control
|
70
63
|
end
|
71
64
|
|
72
|
-
it 'works many times if workers
|
65
|
+
it 'works many times if workers release locks' do
|
73
66
|
30.times do
|
74
67
|
expect {|b| limiter.limit(&b) }.to yield_control
|
75
68
|
end
|
69
|
+
|
70
|
+
30.times do
|
71
|
+
lock = limiter.limit
|
72
|
+
lock.release
|
73
|
+
end
|
76
74
|
end
|
77
75
|
|
78
76
|
it 'limits excessive calls' do
|
79
|
-
expect(limiter.limit).to be_a Berater::
|
80
|
-
expect(limiter.limit).to be_a Berater::
|
77
|
+
expect(limiter.limit).to be_a Berater::Lock
|
78
|
+
expect(limiter.limit).to be_a Berater::Lock
|
81
79
|
|
82
|
-
expect
|
80
|
+
expect(limiter).to be_incapacitated
|
83
81
|
end
|
84
82
|
|
85
|
-
|
86
|
-
|
87
|
-
expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
88
|
-
expect { limiter }.to be_incapacitated
|
89
|
-
|
90
|
-
Timecop.travel(1)
|
83
|
+
context 'with capacity 0' do
|
84
|
+
let(:limiter) { described_class.new(:key, 0) }
|
91
85
|
|
92
|
-
|
93
|
-
|
94
|
-
|
86
|
+
it 'always fails' do
|
87
|
+
expect(limiter).to be_incapacitated
|
88
|
+
end
|
95
89
|
end
|
96
|
-
end
|
97
90
|
|
98
|
-
|
99
|
-
|
100
|
-
|
91
|
+
it 'limit resets over time' do
|
92
|
+
2.times { limiter.limit }
|
93
|
+
expect(limiter).to be_incapacitated
|
94
|
+
|
95
|
+
Timecop.freeze(30)
|
96
|
+
|
97
|
+
2.times { limiter.limit }
|
98
|
+
expect(limiter).to be_incapacitated
|
99
|
+
end
|
101
100
|
|
102
|
-
it
|
101
|
+
it 'limit resets with millisecond precision' do
|
102
|
+
2.times { limiter.limit }
|
103
|
+
expect(limiter).to be_incapacitated
|
103
104
|
|
104
|
-
|
105
|
-
|
105
|
+
# travel forward to just before first lock times out
|
106
|
+
Timecop.freeze(29.999)
|
107
|
+
expect(limiter).to be_incapacitated
|
106
108
|
|
107
|
-
|
108
|
-
|
109
|
+
# traveling one more millisecond will decrement the count
|
110
|
+
Timecop.freeze(0.001)
|
111
|
+
2.times { limiter.limit }
|
112
|
+
expect(limiter).to be_incapacitated
|
109
113
|
end
|
110
|
-
end
|
111
114
|
|
112
|
-
|
113
|
-
|
115
|
+
context 'with cost parameter' do
|
116
|
+
it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
|
117
|
+
|
118
|
+
it 'works within limit' do
|
119
|
+
limiter.limit(cost: 2)
|
120
|
+
expect(limiter).to be_incapacitated
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'releases full cost' do
|
124
|
+
lock = limiter.limit(cost: 2)
|
125
|
+
expect(limiter).to be_incapacitated
|
126
|
+
|
127
|
+
lock.release
|
128
|
+
expect(limiter).not_to be_incapacitated
|
114
129
|
|
115
|
-
|
116
|
-
|
117
|
-
|
130
|
+
lock = limiter.limit(cost: 2)
|
131
|
+
expect(limiter).to be_incapacitated
|
132
|
+
end
|
118
133
|
|
119
|
-
|
120
|
-
|
134
|
+
it 'respects timeout' do
|
135
|
+
limiter.limit(cost: 2)
|
136
|
+
expect(limiter).to be_incapacitated
|
121
137
|
|
122
|
-
|
123
|
-
|
138
|
+
Timecop.freeze(30)
|
139
|
+
expect(limiter).not_to be_incapacitated
|
124
140
|
|
125
|
-
|
126
|
-
|
141
|
+
limiter.limit(cost: 2)
|
142
|
+
expect(limiter).to be_incapacitated
|
143
|
+
end
|
127
144
|
|
128
|
-
|
129
|
-
|
130
|
-
expect { limiter.limit(key: :two) {} }.to be_incapacitated
|
145
|
+
it 'accepts a dynamic capacity' do
|
146
|
+
limiter = described_class.new(:key, 1)
|
131
147
|
|
132
|
-
|
133
|
-
|
134
|
-
|
148
|
+
expect { limiter.limit(capacity: 0) }.to be_incapacitated
|
149
|
+
5.times { limiter.limit(capacity: 10) }
|
150
|
+
expect { limiter }.to be_incapacitated
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'with same key, different limiters' do
|
155
|
+
let(:limiter_one) { described_class.new(:key, 1) }
|
156
|
+
let(:limiter_two) { described_class.new(:key, 1) }
|
157
|
+
|
158
|
+
it { expect(limiter_one.key).to eq limiter_two.key }
|
159
|
+
|
160
|
+
it 'works as expected' do
|
161
|
+
expect(limiter_one.limit).to be_a Berater::Lock
|
162
|
+
|
163
|
+
expect(limiter_one).to be_incapacitated
|
164
|
+
expect(limiter_two).to be_incapacitated
|
165
|
+
end
|
135
166
|
end
|
136
|
-
end
|
137
167
|
|
138
|
-
|
139
|
-
|
140
|
-
|
168
|
+
context 'with same key, different capacities' do
|
169
|
+
let(:limiter_one) { described_class.new(:key, 1) }
|
170
|
+
let(:limiter_two) { described_class.new(:key, 2) }
|
141
171
|
|
142
|
-
|
172
|
+
it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
|
143
173
|
|
144
|
-
|
145
|
-
|
146
|
-
|
174
|
+
it 'works as expected' do
|
175
|
+
one_lock = limiter_one.limit
|
176
|
+
expect(one_lock).to be_a Berater::Lock
|
147
177
|
|
148
|
-
|
149
|
-
|
178
|
+
expect(limiter_one).to be_incapacitated
|
179
|
+
expect(limiter_two).not_to be_incapacitated
|
150
180
|
|
151
|
-
|
152
|
-
|
181
|
+
two_lock = limiter_two.limit
|
182
|
+
expect(two_lock).to be_a Berater::Lock
|
153
183
|
|
154
|
-
|
155
|
-
|
184
|
+
expect(limiter_one).to be_incapacitated
|
185
|
+
expect(limiter_two).to be_incapacitated
|
156
186
|
|
157
|
-
|
158
|
-
|
159
|
-
|
187
|
+
one_lock.release
|
188
|
+
expect(limiter_one).to be_incapacitated
|
189
|
+
expect(limiter_two).not_to be_incapacitated
|
160
190
|
|
161
|
-
|
162
|
-
|
163
|
-
|
191
|
+
two_lock.release
|
192
|
+
expect(limiter_one).not_to be_incapacitated
|
193
|
+
expect(limiter_two).not_to be_incapacitated
|
194
|
+
end
|
164
195
|
end
|
165
|
-
end
|
166
196
|
|
167
|
-
|
168
|
-
|
169
|
-
|
197
|
+
context 'with different keys, different limiters' do
|
198
|
+
let(:limiter_one) { described_class.new(:one, 1) }
|
199
|
+
let(:limiter_two) { described_class.new(:two, 1) }
|
170
200
|
|
171
|
-
|
172
|
-
|
173
|
-
|
201
|
+
it 'works as expected' do
|
202
|
+
expect(limiter_one).not_to be_incapacitated
|
203
|
+
expect(limiter_two).not_to be_incapacitated
|
174
204
|
|
175
|
-
|
176
|
-
|
205
|
+
one_lock = limiter_one.limit
|
206
|
+
expect(one_lock).to be_a Berater::Lock
|
177
207
|
|
178
|
-
|
179
|
-
|
208
|
+
expect(limiter_one).to be_incapacitated
|
209
|
+
expect(limiter_two).not_to be_incapacitated
|
180
210
|
|
181
|
-
|
182
|
-
|
211
|
+
two_lock = limiter_two.limit
|
212
|
+
expect(two_lock).to be_a Berater::Lock
|
183
213
|
|
184
|
-
|
185
|
-
|
214
|
+
expect(limiter_one).to be_incapacitated
|
215
|
+
expect(limiter_two).to be_incapacitated
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe '#overloaded?' do
|
221
|
+
let(:limiter) { described_class.new(:key, 1, timeout: 30) }
|
222
|
+
|
223
|
+
it 'works' do
|
224
|
+
expect(limiter.overloaded?).to be false
|
225
|
+
lock = limiter.limit
|
226
|
+
expect(limiter.overloaded?).to be true
|
227
|
+
lock.release
|
228
|
+
expect(limiter.overloaded?).to be false
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'respects timeout' do
|
232
|
+
expect(limiter.overloaded?).to be false
|
233
|
+
lock = limiter.limit
|
234
|
+
expect(limiter.overloaded?).to be true
|
235
|
+
Timecop.freeze(30)
|
236
|
+
expect(limiter.overloaded?).to be false
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe '#to_s' do
|
241
|
+
def check(capacity, expected)
|
242
|
+
expect(
|
243
|
+
described_class.new(:key, capacity).to_s
|
244
|
+
).to match(expected)
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'works' do
|
248
|
+
check(1, /1 at a time/)
|
249
|
+
check(3, /3 at a time/)
|
186
250
|
end
|
187
251
|
end
|
188
252
|
|