throttling 0.3.1 → 0.4.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 +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
|
-
[](https://github.com/kpumuk/throttling/actions/workflows/tests.yml)
|
4
|
+
[](https://badge.fury.io/rb/throttling)
|
5
|
+
[](https://badge.fury.io/rb/throttling)
|
6
|
+
[](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
|