congestion 0.0.1 → 0.0.2
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/.gitignore +1 -0
- data/.travis.yml +9 -1
- data/Guardfile +6 -0
- data/README.md +31 -6
- data/congestion.gemspec +7 -1
- data/lib/congestion/rate_limiter.rb +104 -0
- data/lib/congestion/redis_pool.rb +32 -0
- data/lib/congestion/version.rb +1 -1
- data/lib/congestion.rb +22 -0
- metadata +93 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16e2c7689805fd59071ed37c3ca3948706b9523d
|
4
|
+
data.tar.gz: 96c12215e6403a4bf5f68f6668f4f5bc7384c8d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d68a8e3a27f4cb38bcb4ae72d2ac45f84171abb4c0e5dc86e0f3b19ad61417d70f8957d11d8ef9204933e48ae6aa0eba8f6851847e1b03ff1fafca19500bb729
|
7
|
+
data.tar.gz: 982bc455e6a47aba8e950c1c96d0dfed4024bbf65fe92d93a0b6a25ab08892c0d8ebe04fa6b524525c908fac2d4aa091c8c1d2dc1456e492c8852ef7a6d83ab4
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Guardfile
ADDED
data/README.md
CHANGED
@@ -42,20 +42,45 @@ limiter.backoff # => the number of seconds before a request will be permit
|
|
42
42
|
|
43
43
|
### Configuration
|
44
44
|
|
45
|
+
A proc provides a Redis connection:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
Congestion.redis = ->{
|
49
|
+
Redis.new url: 'redis://:password@host:port/db'
|
50
|
+
}
|
51
|
+
```
|
52
|
+
|
53
|
+
To pool, your Redis connections:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
require 'congestion/redis_pool'
|
57
|
+
|
58
|
+
Congestion::RedisPool.redis_config = {
|
59
|
+
url: 'redis://:password@host:port/db'
|
60
|
+
}
|
61
|
+
|
62
|
+
Congestion::RedisPool.pool_size = 10 # number of connections to use
|
63
|
+
Congestion::RedisPool.timeout = 10 # seconds before timing out an operation
|
64
|
+
|
65
|
+
Congestion.redis = Congestion::RedisPool.instance
|
66
|
+
```
|
67
|
+
|
45
68
|
Global options can be set with:
|
46
69
|
|
47
70
|
```ruby
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
71
|
+
Congestion.default_options = {
|
72
|
+
namespace: 'congestion' # The Redis key prefix (e.g. 'congestion:some_key')
|
73
|
+
interval: 1, # The timeframe to limit within in seconds
|
74
|
+
max_in_interval: 1, # The number of allowed requests within the interval
|
75
|
+
min_delay: 0.0, # The minimum amount of time in seconds between requests
|
76
|
+
track_rejected: false # True if rejected request count towards the limit
|
77
|
+
}
|
53
78
|
```
|
54
79
|
|
55
80
|
Per-request options can be set as well:
|
56
81
|
|
57
82
|
```ruby
|
58
|
-
|
83
|
+
Congestion.request 'some_key', interval: 60, min_delay: 1
|
59
84
|
```
|
60
85
|
|
61
86
|
## Development
|
data/congestion.gemspec
CHANGED
@@ -19,8 +19,14 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
20
|
spec.require_paths = ['lib']
|
21
21
|
|
22
|
-
spec.
|
22
|
+
spec.add_runtime_dependency 'redis', '>= 3.1'
|
23
|
+
spec.add_runtime_dependency 'connection_pool', '>= 2.0'
|
24
|
+
spec.add_development_dependency 'bundler', '>= 1.5'
|
23
25
|
spec.add_development_dependency 'rake', '~> 10.0'
|
24
26
|
spec.add_development_dependency 'rspec', '~> 3.2'
|
27
|
+
spec.add_development_dependency 'rspec-its', '~> 1.2'
|
28
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.5'
|
29
|
+
spec.add_development_dependency 'timecop', '~> 0.7'
|
25
30
|
spec.add_development_dependency 'pry'
|
31
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
26
32
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Congestion
|
2
|
+
class RateLimiter
|
3
|
+
attr_accessor :redis, :key, :options
|
4
|
+
|
5
|
+
def initialize(redis, key, opts = { })
|
6
|
+
self.redis = redis
|
7
|
+
self.key = "#{ opts[:namespace] }:#{ key }"
|
8
|
+
self.options = opts
|
9
|
+
self.options[:interval] *= 1_000
|
10
|
+
self.options[:min_delay] *= 1_000
|
11
|
+
allowed?
|
12
|
+
end
|
13
|
+
|
14
|
+
def total_requests
|
15
|
+
get_requests[1]
|
16
|
+
end
|
17
|
+
|
18
|
+
def first_request
|
19
|
+
first = get_requests[2].first
|
20
|
+
first ? first.to_i : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def last_request
|
24
|
+
last = get_requests[3].first
|
25
|
+
last ? last.to_i : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def allowed?
|
29
|
+
add_request unless rejected?
|
30
|
+
!rejected?
|
31
|
+
end
|
32
|
+
|
33
|
+
def rejected?
|
34
|
+
too_many? || too_frequent?
|
35
|
+
end
|
36
|
+
|
37
|
+
def too_many?
|
38
|
+
total_requests > options[:max_in_interval]
|
39
|
+
end
|
40
|
+
|
41
|
+
def too_frequent?
|
42
|
+
last_request && time_since_last_request < options[:min_delay]
|
43
|
+
end
|
44
|
+
|
45
|
+
def backoff
|
46
|
+
if too_many? && too_frequent?
|
47
|
+
[quantity_backoff, frequency_backoff].max
|
48
|
+
elsif too_many?
|
49
|
+
quantity_backoff
|
50
|
+
elsif too_frequent?
|
51
|
+
frequency_backoff
|
52
|
+
else
|
53
|
+
0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def current_time
|
60
|
+
@current_time ||= (Time.now.utc.to_f * 1_000).round
|
61
|
+
end
|
62
|
+
|
63
|
+
def time_since_last_request
|
64
|
+
current_time - last_request
|
65
|
+
end
|
66
|
+
|
67
|
+
def time_since_first_request
|
68
|
+
current_time - first_request
|
69
|
+
end
|
70
|
+
|
71
|
+
def expired_at
|
72
|
+
current_time - options[:interval]
|
73
|
+
end
|
74
|
+
|
75
|
+
def quantity_backoff
|
76
|
+
millis = options[:interval] - time_since_first_request
|
77
|
+
(millis / 1_000.0).ceil
|
78
|
+
end
|
79
|
+
|
80
|
+
def frequency_backoff
|
81
|
+
millis = options[:min_delay] - time_since_last_request
|
82
|
+
(millis / 1_000.0).ceil
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_request
|
86
|
+
unless options[:track_rejected]
|
87
|
+
redis.zadd key, current_time, current_time
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_requests
|
92
|
+
@requests ||= redis.multi do |t|
|
93
|
+
t.zremrangebyscore key, 0, expired_at # [0] - clear old requests
|
94
|
+
t.zcount key, '-inf', '+inf' # [1] - number of requests
|
95
|
+
t.zrange key, 0, 0 # [2] - first request
|
96
|
+
t.zrange key, -1, -1 # [3] - last request
|
97
|
+
t.pexpire key, options[:interval] # [4] - expire request key
|
98
|
+
|
99
|
+
# [5] - Add the request if tracking rejected
|
100
|
+
t.zadd(key, current_time, current_time) if options[:track_rejected]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'connection_pool'
|
3
|
+
|
4
|
+
module Congestion
|
5
|
+
class RedisPool
|
6
|
+
class << self
|
7
|
+
attr_accessor :redis_config
|
8
|
+
attr_accessor :pool_size
|
9
|
+
attr_accessor :timeout
|
10
|
+
end
|
11
|
+
|
12
|
+
self.redis_config = { }
|
13
|
+
self.pool_size = 5
|
14
|
+
self.timeout = 5
|
15
|
+
|
16
|
+
attr_accessor :pool
|
17
|
+
|
18
|
+
def self.instance
|
19
|
+
@instance ||= new
|
20
|
+
@redis_pool ||= ->{ @instance.pool.with{ |redis| redis } }
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def initialize
|
25
|
+
pool_config = { size: self.class.pool_size, timeout: self.class.timeout }
|
26
|
+
|
27
|
+
self.pool = ConnectionPool.new(pool_config) do
|
28
|
+
Redis.new self.class.redis_config
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/congestion/version.rb
CHANGED
data/lib/congestion.rb
CHANGED
@@ -1,4 +1,26 @@
|
|
1
|
+
require 'redis'
|
1
2
|
require 'congestion/version'
|
3
|
+
require 'congestion/rate_limiter'
|
2
4
|
|
3
5
|
module Congestion
|
6
|
+
class << self
|
7
|
+
attr_accessor :default_options
|
8
|
+
attr_accessor :redis
|
9
|
+
end
|
10
|
+
|
11
|
+
self.default_options = {
|
12
|
+
namespace: 'congestion', # Redis key namespace
|
13
|
+
interval: 1, # 1 second
|
14
|
+
max_in_interval: 1, # 1 / second
|
15
|
+
min_delay: 0, # none
|
16
|
+
track_rejected: false
|
17
|
+
}
|
18
|
+
|
19
|
+
self.redis = ->{
|
20
|
+
Redis.new
|
21
|
+
}
|
22
|
+
|
23
|
+
def self.request(key, opts = { })
|
24
|
+
RateLimiter.new redis.call, key, default_options.merge(opts)
|
25
|
+
end
|
4
26
|
end
|
metadata
CHANGED
@@ -1,29 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: congestion
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Parrish
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-04-
|
11
|
+
date: 2015-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: connection_pool
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: bundler
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
16
44
|
requirements:
|
17
|
-
- - "
|
45
|
+
- - ">="
|
18
46
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
47
|
+
version: '1.5'
|
20
48
|
type: :development
|
21
49
|
prerelease: false
|
22
50
|
version_requirements: !ruby/object:Gem::Requirement
|
23
51
|
requirements:
|
24
|
-
- - "
|
52
|
+
- - ">="
|
25
53
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
54
|
+
version: '1.5'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: rake
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +80,48 @@ dependencies:
|
|
52
80
|
- - "~>"
|
53
81
|
- !ruby/object:Gem::Version
|
54
82
|
version: '3.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-its
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.2'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: timecop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.7'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.7'
|
55
125
|
- !ruby/object:Gem::Dependency
|
56
126
|
name: pry
|
57
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +136,20 @@ dependencies:
|
|
66
136
|
- - ">="
|
67
137
|
- !ruby/object:Gem::Version
|
68
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: codeclimate-test-reporter
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
69
153
|
description: Redis rate limiter that provides both time-based limits and quantity-based
|
70
154
|
limits
|
71
155
|
email:
|
@@ -79,6 +163,7 @@ files:
|
|
79
163
|
- ".travis.yml"
|
80
164
|
- CODE_OF_CONDUCT.md
|
81
165
|
- Gemfile
|
166
|
+
- Guardfile
|
82
167
|
- LICENSE.txt
|
83
168
|
- README.md
|
84
169
|
- Rakefile
|
@@ -86,6 +171,8 @@ files:
|
|
86
171
|
- bin/setup
|
87
172
|
- congestion.gemspec
|
88
173
|
- lib/congestion.rb
|
174
|
+
- lib/congestion/rate_limiter.rb
|
175
|
+
- lib/congestion/redis_pool.rb
|
89
176
|
- lib/congestion/version.rb
|
90
177
|
homepage: https://github.com/parrish/Congestion
|
91
178
|
licenses:
|