throttling 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +15 -8
- data/Gemfile +1 -1
- data/Gemfile.lock +80 -0
- data/README.md +136 -109
- data/Rakefile +3 -3
- data/lib/throttling/base.rb +6 -6
- data/lib/throttling/indifferent_access.rb +29 -20
- data/lib/throttling/version.rb +1 -1
- data/lib/throttling.rb +14 -14
- data/throttling.gemspec +34 -20
- data.tar.gz.sig +0 -0
- metadata +74 -68
- metadata.gz.sig +0 -0
- data/.gitignore +0 -18
- data/Guardfile +0 -6
- data/spec/base_spec.rb +0 -181
- data/spec/fixtures/throttling.yml +0 -28
- data/spec/spec_helper.rb +0 -21
- data/spec/throttling_spec.rb +0 -103
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 466a505eb343b4cd8c1a37f125064bebe09ded2c6a586f1cb191a29d0b786732
|
4
|
+
data.tar.gz: 58db8ff3fd65eeec71b4f767e629780719e2b9e0e99b944dbdacb3a6653d0527
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff7d8ec51b2a910a44455a6267859f0310adbb57b55ac0ac8239ed3ea99ebe9cfe7102191cf6c78b11f8bd82e724ab1998f1cc7d554318ce2402bbe22a46d8b4
|
7
|
+
data.tar.gz: cecbbd774a4f80f98c976f0dd8151900004532ba3cfb25ae945816f589282e3fbaa0045edb3cbe4b1e3732a990dcd60706cb184aab88d48d9ffe1fdc98e13359
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/CHANGELOG.md
CHANGED
@@ -1,30 +1,37 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.4.0 (July 6, 2023)
|
4
|
+
|
5
|
+
- Bump minimum Ruby requirement to 2.7.0
|
6
|
+
- Updated codebase to match StandardRB style guide
|
7
|
+
|
1
8
|
## 0.3.1 (April 13, 2012)
|
2
9
|
|
3
10
|
Features:
|
4
11
|
|
5
|
-
|
12
|
+
- When limit is omitted, no limits will be enforced (`check` always returns true)
|
6
13
|
|
7
14
|
Bugfixes:
|
8
15
|
|
9
|
-
|
10
|
-
|
16
|
+
- Fixed bug when action was allowed `limit + 1` times
|
17
|
+
- When limit is 0, `check` should always return `false`
|
11
18
|
|
12
19
|
## 0.3.0 (April 13, 2012)
|
13
20
|
|
14
21
|
Features:
|
15
22
|
|
16
|
-
|
23
|
+
- Added ability to retrieve custom value based on number of actions performed in a period of time
|
17
24
|
|
18
25
|
## 0.2.0 (April 12, 2012)
|
19
26
|
|
20
27
|
Bugfixes:
|
21
28
|
|
22
|
-
|
29
|
+
- Fixed bug when occurrences where increased for larger periods, when smaller ones did not pass
|
23
30
|
|
24
31
|
## 0.1.0 (April 12, 2012)
|
25
32
|
|
26
33
|
Features:
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
35
|
+
- Allows to throttle actions occurred in period of time
|
36
|
+
- Allows to specify limits inline or in external configuration file
|
37
|
+
- 100% test coverage
|
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
throttling (0.4.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.2)
|
10
|
+
diff-lcs (1.5.0)
|
11
|
+
json (2.6.3)
|
12
|
+
language_server-protocol (3.17.0.3)
|
13
|
+
lint_roller (1.0.0)
|
14
|
+
parallel (1.23.0)
|
15
|
+
parser (3.2.2.3)
|
16
|
+
ast (~> 2.4.1)
|
17
|
+
racc
|
18
|
+
racc (1.7.1)
|
19
|
+
rainbow (3.1.1)
|
20
|
+
rake (13.0.6)
|
21
|
+
regexp_parser (2.8.1)
|
22
|
+
rexml (3.2.5)
|
23
|
+
rspec (3.12.0)
|
24
|
+
rspec-core (~> 3.12.0)
|
25
|
+
rspec-expectations (~> 3.12.0)
|
26
|
+
rspec-mocks (~> 3.12.0)
|
27
|
+
rspec-core (3.12.2)
|
28
|
+
rspec-support (~> 3.12.0)
|
29
|
+
rspec-expectations (3.12.3)
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
31
|
+
rspec-support (~> 3.12.0)
|
32
|
+
rspec-mocks (3.12.5)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.12.0)
|
35
|
+
rspec-support (3.12.1)
|
36
|
+
rubocop (1.52.1)
|
37
|
+
json (~> 2.3)
|
38
|
+
parallel (~> 1.10)
|
39
|
+
parser (>= 3.2.2.3)
|
40
|
+
rainbow (>= 2.2.2, < 4.0)
|
41
|
+
regexp_parser (>= 1.8, < 3.0)
|
42
|
+
rexml (>= 3.2.5, < 4.0)
|
43
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
44
|
+
ruby-progressbar (~> 1.7)
|
45
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
46
|
+
rubocop-ast (1.29.0)
|
47
|
+
parser (>= 3.2.1.0)
|
48
|
+
rubocop-performance (1.18.0)
|
49
|
+
rubocop (>= 1.7.0, < 2.0)
|
50
|
+
rubocop-ast (>= 0.4.0)
|
51
|
+
ruby-progressbar (1.13.0)
|
52
|
+
standard (1.29.0)
|
53
|
+
language_server-protocol (~> 3.17.0.2)
|
54
|
+
lint_roller (~> 1.0)
|
55
|
+
rubocop (~> 1.52.0)
|
56
|
+
standard-custom (~> 1.0.0)
|
57
|
+
standard-performance (~> 1.1.0)
|
58
|
+
standard-custom (1.0.1)
|
59
|
+
lint_roller (~> 1.0)
|
60
|
+
standard-performance (1.1.0)
|
61
|
+
lint_roller (~> 1.0)
|
62
|
+
rubocop-performance (~> 1.18.0)
|
63
|
+
timecop (0.9.6)
|
64
|
+
unicode-display_width (2.4.2)
|
65
|
+
|
66
|
+
PLATFORMS
|
67
|
+
arm64-darwin
|
68
|
+
arm64-linux
|
69
|
+
x86_64-darwin
|
70
|
+
x86_64-linux
|
71
|
+
|
72
|
+
DEPENDENCIES
|
73
|
+
rake (~> 13.0)
|
74
|
+
rspec (~> 3.12.0)
|
75
|
+
standard (~> 1.29.0)
|
76
|
+
throttling!
|
77
|
+
timecop (~> 0.9.6)
|
78
|
+
|
79
|
+
BUNDLED WITH
|
80
|
+
2.3.26
|
data/README.md
CHANGED
@@ -1,74 +1,79 @@
|
|
1
1
|
# Throttling
|
2
2
|
|
3
|
-
[![
|
3
|
+
[![Tests](https://github.com/kpumuk/throttling/actions/workflows/tests.yml/badge.svg)](https://github.com/kpumuk/throttling/actions/workflows/tests.yml)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/throttling.svg)](https://badge.fury.io/rb/throttling)
|
5
|
+
[![Gem Downloads](https://img.shields.io/gem/dt/throttling.svg)](https://badge.fury.io/rb/throttling)
|
6
|
+
[![Changelog](https://img.shields.io/badge/Changelog-latest-blue.svg)](https://github.com/kpumuk/throttling/blob/main/CHANGELOG.md)
|
4
7
|
|
5
8
|
Throttling gem provides basic, but very powerful way to throttle various user actions in your application. Basically you can specify how many times some action could be performed over a specified period(s) of time.
|
6
9
|
|
7
10
|
## Installation
|
8
11
|
|
9
|
-
Add this line to your application's Gemfile
|
12
|
+
Add this line to your application's `Gemfile`:
|
10
13
|
|
11
|
-
|
14
|
+
```ruby
|
15
|
+
gem "throttling"
|
16
|
+
```
|
12
17
|
|
13
|
-
And
|
14
|
-
|
15
|
-
$ bundle
|
16
|
-
|
17
|
-
Or install it yourself as:
|
18
|
-
|
19
|
-
$ gem install throttling
|
18
|
+
And run `bundle install` command.
|
20
19
|
|
21
20
|
## Configuration
|
22
21
|
|
23
|
-
You can configure Throttling parameters by accessing attributes of `Throttling` module. Currently it supports only Memcached through `Rails.cache`.
|
22
|
+
You can configure Throttling parameters by accessing attributes of `Throttling` module. Currently it supports only Memcached and Redis through `Rails.cache`.
|
24
23
|
|
25
|
-
|
26
|
-
|
24
|
+
```ruby
|
25
|
+
Throttling.storage = Rails.cache
|
26
|
+
Throttling.logger = Rails.logger
|
27
|
+
```
|
27
28
|
|
28
29
|
Throttling limits could be stored in a configuration file in `config/throttling.yml`. You can also specify another file to read limits from:
|
29
30
|
|
30
|
-
|
31
|
+
```ruby
|
32
|
+
Throttling.limits_config = "#{Rails.root}/config/throttling.yml"
|
33
|
+
```
|
31
34
|
|
32
35
|
The basic structure of the file is:
|
33
36
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
37
|
+
```yaml
|
38
|
+
user_signup:
|
39
|
+
limit: 20
|
40
|
+
period: 3600
|
41
|
+
|
42
|
+
search_requests:
|
43
|
+
minutely:
|
44
|
+
limit: 300
|
45
|
+
period: 600
|
46
|
+
hourly:
|
47
|
+
limit: 1000
|
48
|
+
period: 3600
|
49
|
+
daily:
|
50
|
+
limit: 10000
|
51
|
+
period: 86400
|
52
|
+
|
53
|
+
request_priority:
|
54
|
+
period: 86400
|
55
|
+
default_value: 25
|
56
|
+
values:
|
57
|
+
high_priority:
|
58
|
+
limit: 5
|
59
|
+
value: 10
|
60
|
+
medium_priority:
|
61
|
+
limit: 15
|
62
|
+
value: 15
|
63
|
+
low_priority:
|
64
|
+
limit: 100
|
65
|
+
value: 20
|
66
|
+
```
|
62
67
|
|
63
68
|
This example covers three different scenarios:
|
64
69
|
|
65
|
-
1. Single period
|
70
|
+
1. **Single period**. In this case only 20 actions will be allowed in a period of
|
66
71
|
one hour (3600 seconds).
|
67
72
|
|
68
|
-
2. Multiple periods
|
73
|
+
2. **Multiple periods**. Action will be allowed to perform 300 times in 10 minutes,
|
69
74
|
1000 times an hour, and 10000 times a day.
|
70
75
|
|
71
|
-
3. This special case covers following scenario: based on the number of actions,
|
76
|
+
3. **Complex scenario**. This special case covers following scenario: based on the number of actions,
|
72
77
|
it returns a value, or default value when largest limit is reached. In this
|
73
78
|
case it will return 10, when there were 5 or less requests (including current one),
|
74
79
|
15 for up to 15 requests, 20 for up to 100 requests, and 25 when there were
|
@@ -76,110 +81,132 @@ This example covers three different scenarios:
|
|
76
81
|
|
77
82
|
You can also specify limits as a Hash:
|
78
83
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
}
|
84
|
+
```ruby
|
85
|
+
Throttling.limits = {
|
86
|
+
user_signup: {
|
87
|
+
limit: 20,
|
88
|
+
period: 3600
|
89
|
+
},
|
90
|
+
search_requests: {
|
91
|
+
minutely: {
|
92
|
+
limit: 20,
|
93
|
+
period: 3600
|
94
|
+
},
|
95
|
+
hourly: {
|
96
|
+
limit: 1000,
|
97
|
+
period: 3600
|
98
|
+
},
|
99
|
+
daily: {
|
100
|
+
limit: 10000,
|
101
|
+
period: 86400
|
98
102
|
}
|
103
|
+
}
|
104
|
+
}
|
105
|
+
```
|
99
106
|
|
100
107
|
You can completely disable throttling by setting `enabled` to `false`:
|
101
108
|
|
102
|
-
|
109
|
+
```ruby
|
110
|
+
Throttling.enabled = false
|
111
|
+
```
|
103
112
|
|
104
113
|
## Usage
|
105
114
|
|
106
115
|
The basic usage of Throttling gem is following:
|
107
116
|
|
108
|
-
|
109
|
-
|
110
|
-
|
117
|
+
```ruby
|
118
|
+
Throttling.for(:user_signup).check(:user_id, current_user.id) do
|
119
|
+
# Do your stuff here
|
120
|
+
end
|
111
121
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
122
|
+
if Throttling.for(:user_signup).check(:user_id, current_user.id)
|
123
|
+
# Action allowed
|
124
|
+
else
|
125
|
+
# Action denied
|
126
|
+
end
|
127
|
+
```
|
117
128
|
|
118
129
|
For convenience, there are some simplified methods:
|
119
130
|
|
120
|
-
|
121
|
-
|
131
|
+
```ruby
|
132
|
+
Throttling.for(:user_signup).check_ip(request.remote_ip)
|
133
|
+
Throttling.for(:user_signup).check_user_id(current_user.id)
|
134
|
+
```
|
122
135
|
|
123
136
|
You can add more helpers like this:
|
124
137
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
138
|
+
```ruby
|
139
|
+
Throttling::Base.class_eval do
|
140
|
+
def check_user_id_and_document_id(user_id, doc_id)
|
141
|
+
check("user_id:doc_id", "#{user_id}:#{doc_id}")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
130
145
|
|
131
146
|
## Use cases
|
132
147
|
|
133
148
|
### Limiting number of sign-ups
|
134
149
|
|
135
|
-
|
136
|
-
|
137
|
-
|
150
|
+
```yaml
|
151
|
+
user_signup:
|
152
|
+
limit: 20
|
153
|
+
period: 3600
|
154
|
+
```
|
138
155
|
|
139
156
|
Limit the number of sign-ups to 20 per hour per IP address:
|
140
157
|
|
141
|
-
|
158
|
+
```ruby
|
159
|
+
Throttling.for("user_signup").check_ip(request.remote_ip)
|
160
|
+
```
|
142
161
|
|
143
162
|
### Limiting number of document uploads
|
144
163
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
164
|
+
```yaml
|
165
|
+
document_uploads:
|
166
|
+
minutely:
|
167
|
+
limit: 5
|
168
|
+
period: 600
|
169
|
+
hourly:
|
170
|
+
limit: 10
|
171
|
+
period: 3600
|
172
|
+
daily:
|
173
|
+
limit: 50
|
174
|
+
period: 86400
|
175
|
+
```
|
155
176
|
|
156
177
|
In this case user will be allowed to upload 5 documents in 10 minutes, 10 documents
|
157
178
|
in an hour, or 50 documents a day:
|
158
179
|
|
159
|
-
|
180
|
+
```ruby
|
181
|
+
Throttling.for("document_uploads").check_user_id(current_user.id)
|
182
|
+
```
|
160
183
|
|
161
184
|
### Prioritizing uploads based on number of uploads
|
162
185
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
186
|
+
```yaml
|
187
|
+
document_priorities:
|
188
|
+
period: 86400
|
189
|
+
default_value: 25
|
190
|
+
values:
|
191
|
+
high_priority:
|
192
|
+
limit: 5
|
193
|
+
value: 10
|
194
|
+
medium_priority:
|
195
|
+
limit: 15
|
196
|
+
value: 15
|
197
|
+
low_priority:
|
198
|
+
limit: 100
|
199
|
+
value: 20
|
200
|
+
```
|
176
201
|
|
177
202
|
All documents could be prioritized based on the number of uploads: if user uploads
|
178
203
|
less than 5 documents a day, they all will have priority 10. Next 10 documents
|
179
204
|
(first five keep their original priority) will receive priority 15. Documents
|
180
205
|
16 to 100 will get priority 20, and everything else will get priority 25.
|
181
206
|
|
182
|
-
|
207
|
+
```ruby
|
208
|
+
Throttling.for("document_priorities").check_user_id(current_user.id)
|
209
|
+
```
|
183
210
|
|
184
211
|
## Contributing
|
185
212
|
|
data/Rakefile
CHANGED
data/lib/throttling/base.rb
CHANGED
@@ -15,7 +15,7 @@ module Throttling
|
|
15
15
|
if @limits[:values]
|
16
16
|
@limits[:values] = @limits[:values].sort_by { |name, params| params && params[:limit] }
|
17
17
|
end
|
18
|
-
@limits = [[
|
18
|
+
@limits = [["global", @limits]]
|
19
19
|
else
|
20
20
|
@limits = @limits.sort_by { |name, params| params && params[:period] }
|
21
21
|
end
|
@@ -35,7 +35,7 @@ module Throttling
|
|
35
35
|
|
36
36
|
limits.each do |period_name, params|
|
37
37
|
period = params[:period].to_i
|
38
|
-
limit
|
38
|
+
limit = params[:limit]&.to_i
|
39
39
|
values = params[:values]
|
40
40
|
|
41
41
|
raise ArgumentError, "Invalid or no 'period' parameter in the limits[#{action}][#{period_name}] config: #{limit.inspect}" if period < 1
|
@@ -44,7 +44,7 @@ module Throttling
|
|
44
44
|
key = hits_store_key(check_type, check_value, period_name, period)
|
45
45
|
|
46
46
|
# Retrieve current value
|
47
|
-
hits = Throttling.storage.fetch(key, :
|
47
|
+
hits = Throttling.storage.fetch(key, expires_in: hits_store_ttl(period), raw: true) { "0" }.to_i
|
48
48
|
|
49
49
|
if values
|
50
50
|
value = params[:default_value] || false
|
@@ -54,16 +54,16 @@ module Throttling
|
|
54
54
|
break
|
55
55
|
end
|
56
56
|
end
|
57
|
-
|
57
|
+
elsif !limit.nil? && hits >= limit
|
58
58
|
# Over limit?
|
59
|
-
return false
|
59
|
+
return false
|
60
60
|
end
|
61
61
|
|
62
62
|
Throttling.storage.increment(key) if auto_increment
|
63
63
|
return value if values
|
64
64
|
end
|
65
65
|
|
66
|
-
|
66
|
+
true
|
67
67
|
end
|
68
68
|
|
69
69
|
private
|
@@ -84,7 +84,7 @@ module Throttling
|
|
84
84
|
# hash.values_at("a", "b") # => ["x", "y"]
|
85
85
|
#
|
86
86
|
def values_at(*indices)
|
87
|
-
indices.collect {|key| self[convert_key(key)]}
|
87
|
+
indices.collect { |key| self[convert_key(key)] }
|
88
88
|
end
|
89
89
|
|
90
90
|
# Returns an exact copy of the hash.
|
@@ -95,7 +95,7 @@ module Throttling
|
|
95
95
|
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
|
96
96
|
# Does not overwrite the existing hash.
|
97
97
|
def merge(hash)
|
98
|
-
|
98
|
+
dup.update(hash)
|
99
99
|
end
|
100
100
|
|
101
101
|
# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
|
@@ -109,9 +109,17 @@ module Throttling
|
|
109
109
|
super(convert_key(key))
|
110
110
|
end
|
111
111
|
|
112
|
-
def stringify_keys
|
113
|
-
|
114
|
-
|
112
|
+
def stringify_keys!
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def symbolize_keys!
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_options!
|
121
|
+
self
|
122
|
+
end
|
115
123
|
|
116
124
|
# Convert to a Hash with String keys.
|
117
125
|
def to_hash
|
@@ -119,31 +127,32 @@ module Throttling
|
|
119
127
|
end
|
120
128
|
|
121
129
|
protected
|
122
|
-
def convert_key(key)
|
123
|
-
key.kind_of?(Symbol) ? key.to_s : key
|
124
|
-
end
|
125
130
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
131
|
+
def convert_key(key)
|
132
|
+
key.is_a?(Symbol) ? key.to_s : key
|
133
|
+
end
|
134
|
+
|
135
|
+
def convert_value(value)
|
136
|
+
case value
|
137
|
+
when Hash
|
138
|
+
value.with_indifferent_access
|
139
|
+
when Array
|
140
|
+
value.collect { |e| e.is_a?(Hash) ? e.with_indifferent_access : e }
|
141
|
+
else
|
142
|
+
value
|
135
143
|
end
|
144
|
+
end
|
136
145
|
end
|
137
146
|
|
138
|
-
module HashIndifferentAccess
|
147
|
+
module HashIndifferentAccess # :nodoc:
|
139
148
|
def with_indifferent_access
|
140
149
|
hash = HashWithIndifferentAccess.new(self)
|
141
|
-
hash.default =
|
150
|
+
hash.default = default
|
142
151
|
hash
|
143
152
|
end
|
144
153
|
end
|
145
154
|
|
146
|
-
class ::Hash
|
155
|
+
class ::Hash # :nodoc:
|
147
156
|
unless respond_to?(:with_indifferent_access)
|
148
157
|
include Throttling::HashIndifferentAccess
|
149
158
|
end
|
data/lib/throttling/version.rb
CHANGED
data/lib/throttling.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "logger"
|
2
|
+
require "yaml"
|
3
3
|
|
4
4
|
# Simple throttling library to limit number of actions in time.
|
5
5
|
module Throttling
|
@@ -8,7 +8,7 @@ module Throttling
|
|
8
8
|
# (if it is a Rails application).
|
9
9
|
def storage
|
10
10
|
@@storage ||= (defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache) || nil
|
11
|
-
raise ArgumentError,
|
11
|
+
raise ArgumentError, "Throttling.storage is not specified" unless @@storage
|
12
12
|
@@storage
|
13
13
|
end
|
14
14
|
|
@@ -25,7 +25,7 @@ module Throttling
|
|
25
25
|
|
26
26
|
# Gets the logger used to output errors or warnings.
|
27
27
|
def logger
|
28
|
-
@@logger ||= (defined?(Rails) && Rails.respond_to(:logger) && Rails.logger) || Logger.new(
|
28
|
+
@@logger ||= (defined?(Rails) && Rails.respond_to(:logger) && Rails.logger) || Logger.new($stdout)
|
29
29
|
end
|
30
30
|
|
31
31
|
# Sets the logger used to output errors or warnings.
|
@@ -52,14 +52,14 @@ module Throttling
|
|
52
52
|
|
53
53
|
# Sets current throttling limits.
|
54
54
|
def limits=(limits)
|
55
|
-
@@limits = limits
|
55
|
+
@@limits = limits&.with_indifferent_access
|
56
56
|
end
|
57
57
|
|
58
58
|
# Get the value indicating whether throttling is enabled.
|
59
59
|
def enabled?
|
60
60
|
!!@@enabled
|
61
61
|
end
|
62
|
-
|
62
|
+
alias_method :enabled, :enabled?
|
63
63
|
|
64
64
|
# Sets the value indicating whether throttling is enabled.
|
65
65
|
def enabled=(enabled)
|
@@ -83,20 +83,20 @@ module Throttling
|
|
83
83
|
|
84
84
|
# Resets all values to their default state (mostly for testing purpose).
|
85
85
|
def reset_defaults!
|
86
|
-
@@enabled
|
87
|
-
@@logger
|
88
|
-
@@storage
|
86
|
+
@@enabled = true
|
87
|
+
@@logger = nil
|
88
|
+
@@storage = nil
|
89
89
|
@@limits_config = nil
|
90
90
|
|
91
91
|
# Internal variables
|
92
|
-
@@instances
|
93
|
-
@@config
|
92
|
+
@@instances = {}
|
93
|
+
@@config = nil
|
94
94
|
end
|
95
95
|
|
96
96
|
private
|
97
97
|
|
98
98
|
def load_config(path)
|
99
|
-
return nil unless File.
|
99
|
+
return nil unless File.exist?(path)
|
100
100
|
YAML.load_file(path).with_indifferent_access
|
101
101
|
end
|
102
102
|
end
|
@@ -104,6 +104,6 @@ module Throttling
|
|
104
104
|
reset_defaults!
|
105
105
|
end
|
106
106
|
|
107
|
-
require
|
108
|
-
require
|
107
|
+
require "throttling/indifferent_access"
|
108
|
+
require "throttling/base"
|
109
109
|
require "throttling/version"
|
data/throttling.gemspec
CHANGED
@@ -1,24 +1,38 @@
|
|
1
|
-
|
2
|
-
require File.expand_path('../lib/throttling/version', __FILE__)
|
1
|
+
require File.expand_path("../lib/throttling/version", __FILE__)
|
3
2
|
|
4
|
-
Gem::Specification.new do |
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
gem.homepage = "https://github.com/kpumuk/throttling"
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "throttling"
|
5
|
+
spec.version = Throttling::VERSION
|
6
|
+
spec.authors = ["Dmytro Shteflyuk", "Oleksiy Kovyrin"]
|
7
|
+
spec.email = ["kpumuk@kpumuk.info"]
|
10
8
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
9
|
+
spec.summary = "Easy throttling for Ruby applications"
|
10
|
+
spec.description = "Throttling gem provides basic, but very powerful way to throttle various user actions in your application"
|
11
|
+
spec.homepage = "https://github.com/kpumuk/throttling"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.platform = Gem::Platform::RUBY
|
14
|
+
spec.required_ruby_version = ">= 2.7.0"
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(\.|(bin|test|spec|features)/)}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
22
|
+
spec.add_development_dependency "rspec", "~> 3.12.0"
|
23
|
+
spec.add_development_dependency "timecop", "~> 0.9.6"
|
24
|
+
# Code style
|
25
|
+
spec.add_development_dependency "standard", "~> 1.29.0"
|
26
|
+
|
27
|
+
spec.cert_chain = ["certs/kpumuk.pem"]
|
28
|
+
spec.signing_key = File.expand_path("~/.ssh/gem-kpumuk.pem") if $PROGRAM_NAME.end_with?("gem")
|
29
|
+
|
30
|
+
spec.metadata = {
|
31
|
+
"bug_tracker_uri" => "https://github.com/kpumuk/throttling/issues/",
|
32
|
+
"changelog_uri" => "https://github.com/kpumuk/throttling/blob/main/CHANGELOG.md",
|
33
|
+
"documentation_uri" => "https://rubydoc.info/github/kpumuk/throttling/",
|
34
|
+
"homepage_uri" => "https://github.com/kpumuk/throttling/",
|
35
|
+
"source_code_uri" => "https://github.com/kpumuk/throttling/",
|
36
|
+
"rubygems_mfa_required" => "true"
|
37
|
+
}
|
24
38
|
end
|
data.tar.gz.sig
ADDED
Binary file
|
metadata
CHANGED
@@ -1,83 +1,94 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throttling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.4.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Dmytro Shteflyuk
|
9
8
|
- Oleksiy Kovyrin
|
10
|
-
autorequire:
|
11
|
-
bindir:
|
12
|
-
cert_chain:
|
13
|
-
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain:
|
12
|
+
- |
|
13
|
+
-----BEGIN CERTIFICATE-----
|
14
|
+
MIIDcDCCAligAwIBAgIBATANBgkqhkiG9w0BAQsFADA/MQ8wDQYDVQQDDAZrcHVt
|
15
|
+
dWsxFjAUBgoJkiaJk/IsZAEZFgZrcHVtdWsxFDASBgoJkiaJk/IsZAEZFgRpbmZv
|
16
|
+
MB4XDTIzMDcwNjEyMjY0MVoXDTI0MDcwNTEyMjY0MVowPzEPMA0GA1UEAwwGa3B1
|
17
|
+
bXVrMRYwFAYKCZImiZPyLGQBGRYGa3B1bXVrMRQwEgYKCZImiZPyLGQBGRYEaW5m
|
18
|
+
bzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALw2YroZc+IT+rs8NuPu
|
19
|
+
c13DelrxrpAgPEu1zuRb3l7WaHRNWA4TyS8Z6Aa1G2O+FdUZNMW1n7IwP/QMJ9Mz
|
20
|
+
ahRBiTmhik5kasJ9s0h1lq5/hZiycm0o5OtGioUzCkvk+UEMpzMHbLmVSZCzYciy
|
21
|
+
NDRDbXB0rLLu1eJk+gKgn6Qf5vj93h1w28BdWdaA7YegtbmipZ+pjmzCQAfPActT
|
22
|
+
6uXHG4dSo7Lz9jiFRI5dUizFbGXcRqkQ2b5AB8FFmfcvbqERvzQKBICnybjsKP0N
|
23
|
+
pJ3vGgO2sh5GvJFOPk1Vlur2nX9ZFznPEP1CEAQ+NFrmKRt355Z5sgOkAojSGnIL
|
24
|
+
/1sCAwEAAaN3MHUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFPa4
|
25
|
+
VFc1YOlV1u/7EGTwMCAk8YE9MB0GA1UdEQQWMBSBEmtwdW11a0BrcHVtdWsuaW5m
|
26
|
+
bzAdBgNVHRIEFjAUgRJrcHVtdWtAa3B1bXVrLmluZm8wDQYJKoZIhvcNAQELBQAD
|
27
|
+
ggEBALwivjTzGErFzBtgPEQe7uO6DMysgm/OIF5jLFkvRiuFbuueHM2cUZZBx2Rd
|
28
|
+
2R5uQS4uiZ5KA+3SkgWQOFOzpFlo7ywS5264ULCcvfix6NQpb2aIDsZuzWGBIDbs
|
29
|
+
ixS+8wYslrzsOgUSn/RjF0sVB0dw4wv6G8YnVrR/Jf2SaO+Q2lYuKEOKfj52I+Yt
|
30
|
+
TdFiOjR+WwXSxs/XdSdFtqK2q/THbZdk+HkAe8guvXYtD/fzO2mBlk2AQ0hV1Pxu
|
31
|
+
ovz1LEphwrX/6v635mteXvl+OKWrNo1Q78sU364BgY5MvJMxFytmUrKMgO6RAiIM
|
32
|
+
N9+lhJiLa7+h0LrvPPDZRhV8ze0=
|
33
|
+
-----END CERTIFICATE-----
|
34
|
+
date: 2023-07-06 00:00:00.000000000 Z
|
14
35
|
dependencies:
|
15
36
|
- !ruby/object:Gem::Dependency
|
16
37
|
name: rake
|
17
|
-
requirement:
|
18
|
-
none: false
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
19
39
|
requirements:
|
20
|
-
- -
|
40
|
+
- - "~>"
|
21
41
|
- !ruby/object:Gem::Version
|
22
|
-
version: '0'
|
42
|
+
version: '13.0'
|
23
43
|
type: :development
|
24
44
|
prerelease: false
|
25
|
-
version_requirements:
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '13.0'
|
26
50
|
- !ruby/object:Gem::Dependency
|
27
51
|
name: rspec
|
28
|
-
requirement:
|
29
|
-
none: false
|
52
|
+
requirement: !ruby/object:Gem::Requirement
|
30
53
|
requirements:
|
31
|
-
- -
|
54
|
+
- - "~>"
|
32
55
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
56
|
+
version: 3.12.0
|
34
57
|
type: :development
|
35
58
|
prerelease: false
|
36
|
-
version_requirements:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 3.12.0
|
37
64
|
- !ruby/object:Gem::Dependency
|
38
65
|
name: timecop
|
39
|
-
requirement:
|
40
|
-
none: false
|
66
|
+
requirement: !ruby/object:Gem::Requirement
|
41
67
|
requirements:
|
42
|
-
- -
|
68
|
+
- - "~>"
|
43
69
|
- !ruby/object:Gem::Version
|
44
|
-
version:
|
70
|
+
version: 0.9.6
|
45
71
|
type: :development
|
46
72
|
prerelease: false
|
47
|
-
version_requirements:
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: guard-rspec
|
50
|
-
requirement: &70097466942820 !ruby/object:Gem::Requirement
|
51
|
-
none: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
74
|
requirements:
|
53
|
-
- -
|
75
|
+
- - "~>"
|
54
76
|
- !ruby/object:Gem::Version
|
55
|
-
version:
|
56
|
-
type: :development
|
57
|
-
prerelease: false
|
58
|
-
version_requirements: *70097466942820
|
77
|
+
version: 0.9.6
|
59
78
|
- !ruby/object:Gem::Dependency
|
60
|
-
name:
|
61
|
-
requirement:
|
62
|
-
none: false
|
79
|
+
name: standard
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
63
81
|
requirements:
|
64
|
-
- -
|
82
|
+
- - "~>"
|
65
83
|
- !ruby/object:Gem::Version
|
66
|
-
version:
|
84
|
+
version: 1.29.0
|
67
85
|
type: :development
|
68
86
|
prerelease: false
|
69
|
-
version_requirements:
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
|
-
name: growl
|
72
|
-
requirement: &70097466941980 !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
87
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
88
|
requirements:
|
75
|
-
- -
|
89
|
+
- - "~>"
|
76
90
|
- !ruby/object:Gem::Version
|
77
|
-
version:
|
78
|
-
type: :development
|
79
|
-
prerelease: false
|
80
|
-
version_requirements: *70097466941980
|
91
|
+
version: 1.29.0
|
81
92
|
description: Throttling gem provides basic, but very powerful way to throttle various
|
82
93
|
user actions in your application
|
83
94
|
email:
|
@@ -86,10 +97,9 @@ executables: []
|
|
86
97
|
extensions: []
|
87
98
|
extra_rdoc_files: []
|
88
99
|
files:
|
89
|
-
- .gitignore
|
90
100
|
- CHANGELOG.md
|
91
101
|
- Gemfile
|
92
|
-
-
|
102
|
+
- Gemfile.lock
|
93
103
|
- LICENSE
|
94
104
|
- README.md
|
95
105
|
- Rakefile
|
@@ -97,38 +107,34 @@ files:
|
|
97
107
|
- lib/throttling/base.rb
|
98
108
|
- lib/throttling/indifferent_access.rb
|
99
109
|
- lib/throttling/version.rb
|
100
|
-
- spec/base_spec.rb
|
101
|
-
- spec/fixtures/throttling.yml
|
102
|
-
- spec/spec_helper.rb
|
103
|
-
- spec/throttling_spec.rb
|
104
110
|
- throttling.gemspec
|
105
111
|
homepage: https://github.com/kpumuk/throttling
|
106
|
-
licenses:
|
107
|
-
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata:
|
115
|
+
bug_tracker_uri: https://github.com/kpumuk/throttling/issues/
|
116
|
+
changelog_uri: https://github.com/kpumuk/throttling/blob/main/CHANGELOG.md
|
117
|
+
documentation_uri: https://rubydoc.info/github/kpumuk/throttling/
|
118
|
+
homepage_uri: https://github.com/kpumuk/throttling/
|
119
|
+
source_code_uri: https://github.com/kpumuk/throttling/
|
120
|
+
rubygems_mfa_required: 'true'
|
121
|
+
post_install_message:
|
108
122
|
rdoc_options: []
|
109
123
|
require_paths:
|
110
124
|
- lib
|
111
125
|
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
-
none: false
|
113
126
|
requirements:
|
114
|
-
- -
|
127
|
+
- - ">="
|
115
128
|
- !ruby/object:Gem::Version
|
116
|
-
version:
|
129
|
+
version: 2.7.0
|
117
130
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
-
none: false
|
119
131
|
requirements:
|
120
|
-
- -
|
132
|
+
- - ">="
|
121
133
|
- !ruby/object:Gem::Version
|
122
134
|
version: '0'
|
123
135
|
requirements: []
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
specification_version: 3
|
136
|
+
rubygems_version: 3.4.10
|
137
|
+
signing_key:
|
138
|
+
specification_version: 4
|
128
139
|
summary: Easy throttling for Ruby applications
|
129
|
-
test_files:
|
130
|
-
- spec/base_spec.rb
|
131
|
-
- spec/fixtures/throttling.yml
|
132
|
-
- spec/spec_helper.rb
|
133
|
-
- spec/throttling_spec.rb
|
134
|
-
- Guardfile
|
140
|
+
test_files: []
|
metadata.gz.sig
ADDED
Binary file
|
data/.gitignore
DELETED
data/Guardfile
DELETED
data/spec/base_spec.rb
DELETED
@@ -1,181 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Throttling do
|
4
|
-
before do
|
5
|
-
Throttling.reset_defaults!
|
6
|
-
@storage = Throttling.storage = TestStorage.new
|
7
|
-
end
|
8
|
-
|
9
|
-
describe 'instance methods' do
|
10
|
-
before do
|
11
|
-
Throttling.limits = { 'foo' => {'limit' => 5, 'period' => 2} }
|
12
|
-
@t = Throttling.for('foo')
|
13
|
-
end
|
14
|
-
|
15
|
-
{ :check_ip => '127.0.0.1', :check_user_id => 123 }.each do |check_method, valid_value|
|
16
|
-
describe check_method do
|
17
|
-
it 'should return true for nil check_values' do
|
18
|
-
@t.send(check_method, nil).should be_true
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'should return true if no limit specified in configs' do
|
22
|
-
Throttling.limits['foo']['limit'] = nil
|
23
|
-
@storage.should_receive(:fetch).and_return(1000)
|
24
|
-
@t.send(check_method, valid_value).should be_true
|
25
|
-
end
|
26
|
-
|
27
|
-
it 'should return false if limit is 0' do
|
28
|
-
Throttling.limits['foo']['limit'] = 0
|
29
|
-
@storage.should_receive(:fetch).and_return(0)
|
30
|
-
@t.send(check_method, valid_value).should be_false
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'should raise an exception if no period specified in configs' do
|
34
|
-
Throttling.limits['foo']['period'] = nil
|
35
|
-
lambda { @t.send(check_method, valid_value) }.should raise_error(ArgumentError)
|
36
|
-
end
|
37
|
-
|
38
|
-
it 'should raise an exception if invalid period specified in configs' do
|
39
|
-
Throttling.limits['foo']['period'] = -1
|
40
|
-
lambda { @t.send(check_method, valid_value) }.should raise_error(ArgumentError)
|
41
|
-
|
42
|
-
Throttling.limits['foo']['period'] = 'foo'
|
43
|
-
lambda { @t.send(check_method, valid_value) }.should raise_error(ArgumentError)
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'should return true if throttling limit is not passed' do
|
47
|
-
@storage.should_receive(:fetch).and_return(1)
|
48
|
-
@t.send(check_method, valid_value).should be_true
|
49
|
-
end
|
50
|
-
|
51
|
-
it 'should return false if throttling limit is passed' do
|
52
|
-
@storage.should_receive(:fetch).and_return(Throttling.limits['foo']['limit'] + 1)
|
53
|
-
@t.send(check_method, valid_value).should be_false
|
54
|
-
end
|
55
|
-
|
56
|
-
context 'around limit' do
|
57
|
-
it 'should increase hit counter when values equals to limit - 1' do
|
58
|
-
@storage.should_receive(:fetch).and_return(Throttling.limits['foo']['limit'] - 1)
|
59
|
-
@storage.should_receive(:increment)
|
60
|
-
@t.send(check_method, valid_value)
|
61
|
-
end
|
62
|
-
|
63
|
-
it 'should not increase hit counter when values equals to limit' do
|
64
|
-
@storage.should_receive(:fetch).and_return(Throttling.limits['foo']['limit'])
|
65
|
-
@storage.should_not_receive(:increment)
|
66
|
-
@t.send(check_method, valid_value)
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'should not increase hit counter when values equals to limit + 1' do
|
70
|
-
@storage.should_receive(:fetch).and_return(Throttling.limits['foo']['limit'] + 1)
|
71
|
-
@storage.should_not_receive(:increment)
|
72
|
-
@t.send(check_method, valid_value)
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'should allow exactly limit actions' do
|
76
|
-
5.times { @t.send(check_method, valid_value).should be_true }
|
77
|
-
@storage.should_not_receive(:increment)
|
78
|
-
@t.send(check_method, valid_value).should be_false
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
describe 'with multi-level limits' do
|
86
|
-
before do
|
87
|
-
Throttling.limits = { 'foo' => { 'two' => { 'limit' => 10, 'period' => 20 }, 'one' => { 'limit' => 5, 'period' => 2 } } }
|
88
|
-
end
|
89
|
-
|
90
|
-
it 'should return false if at least one limit is reached' do
|
91
|
-
@storage.should_receive(:fetch).and_return(1, 100)
|
92
|
-
Throttling.for('foo').check_ip('127.0.0.1').should be_false
|
93
|
-
end
|
94
|
-
|
95
|
-
it 'should return true if none limits reached' do
|
96
|
-
@storage.should_receive(:fetch).and_return(1, 2)
|
97
|
-
Throttling.for('foo').check_ip('127.0.0.1').should be_true
|
98
|
-
end
|
99
|
-
|
100
|
-
it 'should sort limits by period' do
|
101
|
-
@storage.should_receive(:fetch).ordered.with(/\:one\:/, anything).and_return(0)
|
102
|
-
@storage.should_receive(:fetch).ordered.with(/\:two\:/, anything).and_return(0)
|
103
|
-
Throttling.for('foo').check_ip('127.0.0.1').should be_true
|
104
|
-
end
|
105
|
-
|
106
|
-
it 'should return as soon as limit reached' do
|
107
|
-
@storage.should_receive(:fetch).ordered.with(/\:one\:/, anything).and_return(10)
|
108
|
-
@storage.should_not_receive(:fetch).with(/\:two\:/)
|
109
|
-
Throttling.for('foo').check_ip('127.0.0.1').should be_false
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
context 'with values specified' do
|
114
|
-
before do
|
115
|
-
Throttling.limits_config = File.expand_path('../fixtures/throttling.yml', __FILE__)
|
116
|
-
end
|
117
|
-
|
118
|
-
it 'should return value when limit is not reached' do
|
119
|
-
@storage.should_receive(:fetch).and_return(0)
|
120
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 10
|
121
|
-
@storage.should_receive(:fetch).and_return(4)
|
122
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 10
|
123
|
-
|
124
|
-
@storage.should_receive(:fetch).and_return(5)
|
125
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 15
|
126
|
-
@storage.should_receive(:fetch).and_return(14)
|
127
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 15
|
128
|
-
|
129
|
-
@storage.should_receive(:fetch).and_return(15)
|
130
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 20
|
131
|
-
@storage.should_receive(:fetch).and_return(99)
|
132
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 20
|
133
|
-
|
134
|
-
@storage.should_receive(:fetch).and_return(100)
|
135
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 25
|
136
|
-
@storage.should_receive(:fetch).and_return(1000)
|
137
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should == 25
|
138
|
-
end
|
139
|
-
|
140
|
-
it 'should increase hit counter' do
|
141
|
-
@storage.should_receive(:fetch).and_return(4)
|
142
|
-
@storage.should_receive(:increment)
|
143
|
-
Throttling.for('request_priority').check_ip('127.0.0.1')
|
144
|
-
|
145
|
-
@storage.should_receive(:fetch).and_return(1000)
|
146
|
-
@storage.should_receive(:increment)
|
147
|
-
Throttling.for('request_priority').check_ip('127.0.0.1')
|
148
|
-
end
|
149
|
-
|
150
|
-
it 'should return false when highest limit reached' do
|
151
|
-
Throttling.limits['request_priority'].delete('default_value')
|
152
|
-
@storage.should_receive(:fetch).and_return(1000)
|
153
|
-
Throttling.for('request_priority').check_ip('127.0.0.1').should be_false
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
context do
|
158
|
-
before do
|
159
|
-
Throttling.limits = { 'foo' => {'limit' => 5, 'period' => 86400} }
|
160
|
-
@timestamp = 1334261569
|
161
|
-
end
|
162
|
-
|
163
|
-
describe 'key name' do
|
164
|
-
it 'should include type, value, name, and period start' do
|
165
|
-
Timecop.freeze(Time.at(@timestamp)) do
|
166
|
-
Throttling.for('foo').check_ip('127.0.0.1')
|
167
|
-
end
|
168
|
-
@storage.values.keys.first.should == 'throttle:foo:ip:127.0.0.1:global:15442'
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
describe 'key expiration' do
|
173
|
-
it 'should calculate expiration time' do
|
174
|
-
Timecop.freeze(Time.at(@timestamp)) do
|
175
|
-
Throttling.for('foo').check_ip('127.0.0.1')
|
176
|
-
end
|
177
|
-
@storage.values.values.first[:expires_in].should == 13631
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
user_signup:
|
2
|
-
limit: 20
|
3
|
-
period: 3600
|
4
|
-
|
5
|
-
search_requests:
|
6
|
-
minutely:
|
7
|
-
limit: 300
|
8
|
-
period: 600
|
9
|
-
hourly:
|
10
|
-
limit: 1000
|
11
|
-
period: 3600
|
12
|
-
daily:
|
13
|
-
limit: 10000
|
14
|
-
period: 86400
|
15
|
-
|
16
|
-
request_priority:
|
17
|
-
period: 86400
|
18
|
-
default_value: 25
|
19
|
-
values:
|
20
|
-
high_priority:
|
21
|
-
limit: 5
|
22
|
-
value: 10
|
23
|
-
medium_priority:
|
24
|
-
limit: 15
|
25
|
-
value: 15
|
26
|
-
low_priority:
|
27
|
-
limit: 100
|
28
|
-
value: 20
|
data/spec/spec_helper.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
require 'bundler/setup'
|
2
|
-
require 'throttling'
|
3
|
-
require 'timecop'
|
4
|
-
|
5
|
-
class TestStorage
|
6
|
-
attr_reader :values
|
7
|
-
|
8
|
-
def fetch(key, options = {}, &block)
|
9
|
-
@values ||= {}
|
10
|
-
value = @values.fetch(key, &block)
|
11
|
-
value = { :value => value.to_s } unless Hash === value
|
12
|
-
@values[key] = value.merge(options)
|
13
|
-
value[:value]
|
14
|
-
end
|
15
|
-
|
16
|
-
def increment(key)
|
17
|
-
@values ||= {}
|
18
|
-
@values[key] ||= { :value => 0 }
|
19
|
-
@values[key][:value] = (@values[key][:value].to_i + 1).to_s
|
20
|
-
end
|
21
|
-
end
|
data/spec/throttling_spec.rb
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Throttling do
|
4
|
-
after :each do
|
5
|
-
Throttling.reset_defaults!
|
6
|
-
end
|
7
|
-
|
8
|
-
context 'with defaults' do
|
9
|
-
it 'should create logger' do
|
10
|
-
Throttling.logger.should be_a(Logger)
|
11
|
-
end
|
12
|
-
|
13
|
-
it 'should be enabled' do
|
14
|
-
Throttling.enabled?.should be_true
|
15
|
-
Throttling.enabled.should be_true
|
16
|
-
end
|
17
|
-
|
18
|
-
it 'should set config file path' do
|
19
|
-
Throttling.limits_config.should == "#{Dir.pwd}/config/throttling.yml"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
context 'setters' do
|
24
|
-
it 'should allow to enabled and disable client' do
|
25
|
-
Throttling.enabled = false
|
26
|
-
Throttling.should_not be_enabled
|
27
|
-
|
28
|
-
Throttling.enable!
|
29
|
-
Throttling.should be_enabled
|
30
|
-
|
31
|
-
Throttling.disable!
|
32
|
-
Throttling.should_not be_enabled
|
33
|
-
|
34
|
-
Throttling.enabled = true
|
35
|
-
Throttling.should be_enabled
|
36
|
-
end
|
37
|
-
|
38
|
-
it 'should allow to change logger' do
|
39
|
-
mock = Throttling.logger = mock('Logger')
|
40
|
-
Throttling.logger.should be(mock)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'should allow to set limits' do
|
44
|
-
limits = { 'foo' => {'limit' => 5, 'period' => 2} }
|
45
|
-
Throttling.limits = limits
|
46
|
-
Throttling.limits.should == limits
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'should allow to change config file path' do
|
50
|
-
path = File.expand_path('../fixtures/throttling.yml', __FILE__)
|
51
|
-
Throttling.limits_config = path
|
52
|
-
Throttling.limits_config.should == path
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
describe '.for' do
|
57
|
-
it "should return a throttling class instance" do
|
58
|
-
Throttling.limits = { 'foo' => {'limit' => 5, 'period' => 2} }
|
59
|
-
Throttling.for('foo').should be_instance_of(Throttling::Base)
|
60
|
-
end
|
61
|
-
|
62
|
-
it "should raise an exception if no throttling_limits found in config" do
|
63
|
-
Throttling.limits = nil
|
64
|
-
lambda { Throttling.for('foo') }.should raise_error(ArgumentError)
|
65
|
-
end
|
66
|
-
|
67
|
-
it "should raise an exception if no throttling_limits[action] found in config" do
|
68
|
-
Throttling.limits = { 'foo' => nil }
|
69
|
-
lambda { Throttling.for('foo') }.should raise_error(ArgumentError)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe 'limits' do
|
74
|
-
context 'when set using .limits' do
|
75
|
-
it 'should convert Hash to HashWithIndifferentAccess' do
|
76
|
-
Throttling.limits = { 'foo' => {'limit' => 5, 'period' => 2} }
|
77
|
-
Throttling.limits.should have_key(:foo)
|
78
|
-
Throttling.limits.should have_key('foo')
|
79
|
-
Throttling.limits[:foo].should have_key(:limit)
|
80
|
-
Throttling.limits[:foo].should have_key('limit')
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
context 'when set using .limits_config' do
|
85
|
-
before do
|
86
|
-
Throttling.limits_config = File.expand_path('../fixtures/throttling.yml', __FILE__)
|
87
|
-
end
|
88
|
-
|
89
|
-
it 'should load limits from configuration file' do
|
90
|
-
Throttling.limits.should be_kind_of(Hash)
|
91
|
-
Throttling.limits.should have_key('search_requests')
|
92
|
-
Throttling.limits.should have_key('user_signup')
|
93
|
-
end
|
94
|
-
|
95
|
-
it 'should convert Hash to HashWithIndifferentAccess' do
|
96
|
-
Throttling.limits.should have_key(:search_requests)
|
97
|
-
Throttling.limits.should have_key('search_requests')
|
98
|
-
Throttling.limits[:search_requests].should have_key(:daily)
|
99
|
-
Throttling.limits[:search_requests].should have_key('daily')
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|