dalli-rate_limiter 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +8 -4
- data/README.md +11 -12
- data/lib/dalli/rate_limiter.rb +85 -13
- data/lib/dalli/rate_limiter/version.rb +1 -1
- metadata +3 -3
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -82,22 +82,21 @@ attributes so it should be safe to share between threads; in this case, you
|
|
82
82
|
will definitely want to use either the default ConnectionPool or your own (as
|
83
83
|
opposed to a single-threaded Dalli::Client instance).
|
84
84
|
|
85
|
-
The main instance method, `#exceeded?` will return
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
85
|
+
The main instance method, `#exceeded?` will return false if the request is free
|
86
|
+
to proceed. If the limit has been exceeded, it will return a positive floating
|
87
|
+
point value that represents the fractional number of seconds that the caller
|
88
|
+
should wait until retrying the request. Assuming no other requests were process
|
89
|
+
during that time, the retried request will be free to proceed at that point.
|
90
|
+
When invoking this method, please be sure to pass in a key that is unique (in
|
91
|
+
combination with the `:key_prefix` option described above) to the thing you are
|
92
|
+
trying to limit. An optional second argument specifies the number of requests
|
93
|
+
to "consume" from the allowance; this defaults to one (1).
|
94
94
|
|
95
95
|
Please note that if the number of requests is greater than the maximum number
|
96
96
|
of requests, the limit will never not be exceeded. Consider a limit of 50
|
97
97
|
requests per minute: no amount of waiting would ever allow for a batch of 51
|
98
|
-
requests! `#exceeded?` returns
|
99
|
-
|
100
|
-
available.
|
98
|
+
requests! `#exceeded?` returns `-1` in this event. To help detect this edge
|
99
|
+
case proactively, a public getter method `#max_requests` is available.
|
101
100
|
|
102
101
|
## Advanced Usage
|
103
102
|
|
data/lib/dalli/rate_limiter.rb
CHANGED
@@ -12,20 +12,74 @@ module Dalli
|
|
12
12
|
0x0d
|
13
13
|
].map(&:chr).join("").freeze
|
14
14
|
|
15
|
+
# Dalli::RateLimiter provides arbitrary Memcached-backed rate limiting for
|
16
|
+
# your Ruby applications.
|
17
|
+
#
|
18
|
+
# @see file:README.md
|
19
|
+
#
|
20
|
+
# @!attribute [r] max_requests
|
21
|
+
# @return [Float] the maximum number of requests in a human-friendly format
|
15
22
|
class RateLimiter
|
16
23
|
LOCK_TTL = 30
|
17
24
|
LOCK_MAX_TRIES = 6
|
18
25
|
|
26
|
+
DEFAULT_OPTIONS = {
|
27
|
+
:key_prefix => "dalli-rate_limiter",
|
28
|
+
:max_requests => 5_000,
|
29
|
+
:period => 8_000,
|
30
|
+
:locking => true
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
private_constant :DEFAULT_OPTIONS
|
34
|
+
|
35
|
+
# Create a new instance of Dalli::RateLimiter.
|
36
|
+
#
|
37
|
+
# @param dalli [nil, ConnectionPool, Dalli::Client] the Dalli::Client (or
|
38
|
+
# ConnectionPool of Dalli::Client) to use as a backing store for this
|
39
|
+
# rate limiter
|
40
|
+
# @param options [Hash{Symbol}] configuration options for this rate limiter
|
41
|
+
#
|
42
|
+
# @option options [String] :key_prefix ("dalli-rate_limiter") a unique
|
43
|
+
# string describing this rate limiter
|
44
|
+
# @option options [Integer, Float] :max_requests (5) maximum number of
|
45
|
+
# requests over the governed interval
|
46
|
+
# @option options [Integer, Float] :period (8) number of seconds over
|
47
|
+
# which to enforce the maximum number of requests
|
48
|
+
# @option options [Boolean] :locking (true) enable or disable locking
|
19
49
|
def initialize(dalli = nil, options = {})
|
20
50
|
@dalli = dalli || ConnectionPool.new { Dalli::Client.new }
|
21
51
|
|
22
|
-
|
23
|
-
|
24
|
-
@
|
25
|
-
@
|
52
|
+
options = normalize_options options
|
53
|
+
|
54
|
+
@key_prefix = options[:key_prefix]
|
55
|
+
@max_requests = options[:max_requests]
|
56
|
+
@period = options[:period]
|
57
|
+
@locking = options[:locking]
|
26
58
|
end
|
27
59
|
|
60
|
+
def max_requests
|
61
|
+
to_fs @max_requests
|
62
|
+
end
|
63
|
+
|
64
|
+
# Determine whether processing a given request would exceed the rate limit.
|
65
|
+
#
|
66
|
+
# @param unique_key [String] a key to use, in combination with the
|
67
|
+
# `:key_prefix` and any `:namespace` defined in the Dalli::Client, to
|
68
|
+
# distinguish the item being limited from similar items
|
69
|
+
# @param to_consume [Integer, Float] the number of requests to consume from
|
70
|
+
# the allowance (used to represent a batch of requests)
|
71
|
+
#
|
72
|
+
# @return [false] if the request can be processed as given without
|
73
|
+
# exceeding the limit (including the case where the number to consume is
|
74
|
+
# zero)
|
75
|
+
# @return [Float] if processing the request as given would exceed
|
76
|
+
# the limit and the caller should wait so many [fractional] seconds
|
77
|
+
# before retrying
|
78
|
+
# @return [-1] if the number to consume exceeds the maximum,
|
79
|
+
# and the request as given would never not exceed the limit
|
28
80
|
def exceeded?(unique_key, to_consume = 1)
|
81
|
+
return false if to_consume == 0
|
82
|
+
|
29
83
|
to_consume = to_ems(to_consume)
|
30
84
|
|
31
85
|
return -1 if to_consume > @max_requests
|
@@ -38,7 +92,7 @@ module Dalli
|
|
38
92
|
# Short-circuit the simple case of seeing the key for the first time.
|
39
93
|
dc.set(timestamp_key, to_ems(Time.now.to_f), @period, :raw => true)
|
40
94
|
|
41
|
-
return
|
95
|
+
return false
|
42
96
|
end
|
43
97
|
|
44
98
|
lock = acquire_lock(dc, unique_key) if @locking
|
@@ -46,8 +100,8 @@ module Dalli
|
|
46
100
|
current_timestamp = to_ems(Time.now.to_f) # obtain timestamp after locking
|
47
101
|
|
48
102
|
previous = dc.get_multi allowance_key, timestamp_key
|
49
|
-
previous_allowance = previous[allowance_key].to_i
|
50
|
-
previous_timestamp = previous[timestamp_key].to_i
|
103
|
+
previous_allowance = previous.key?(allowance_key) ? previous[allowance_key].to_i : @max_requests
|
104
|
+
previous_timestamp = previous.key?(timestamp_key) ? previous[timestamp_key].to_i : current_timestamp
|
51
105
|
|
52
106
|
allowance_delta = (1.0 * (current_timestamp - previous_timestamp) * @max_requests / @period).to_i
|
53
107
|
projected_allowance = previous_allowance + allowance_delta
|
@@ -72,16 +126,30 @@ module Dalli
|
|
72
126
|
|
73
127
|
release_lock(dc, unique_key) if lock
|
74
128
|
|
75
|
-
return
|
129
|
+
return false
|
76
130
|
end
|
77
131
|
end
|
78
132
|
|
79
|
-
def max_requests
|
80
|
-
to_fs(@max_requests)
|
81
|
-
end
|
82
|
-
|
83
133
|
private
|
84
134
|
|
135
|
+
def normalize_options(options)
|
136
|
+
normalized_options = {}
|
137
|
+
|
138
|
+
normalized_options[:key_prefix] = cleanse_key options[:key_prefix] \
|
139
|
+
if options[:key_prefix]
|
140
|
+
|
141
|
+
normalized_options[:max_requests] = to_ems options[:max_requests].to_f \
|
142
|
+
if options[:max_requests]
|
143
|
+
|
144
|
+
normalized_options[:period] = to_ems options[:period].to_f \
|
145
|
+
if options[:period]
|
146
|
+
|
147
|
+
normalized_options[:locking] = !!options[:locking] \
|
148
|
+
if options.key? :locking
|
149
|
+
|
150
|
+
DEFAULT_OPTIONS.dup.merge! normalized_options
|
151
|
+
end
|
152
|
+
|
85
153
|
def acquire_lock(dc, key)
|
86
154
|
lock_key = format_key(key, "mutex")
|
87
155
|
|
@@ -113,7 +181,11 @@ module Dalli
|
|
113
181
|
end
|
114
182
|
|
115
183
|
def format_key(key, attribute)
|
116
|
-
"#{@key_prefix}:#{key
|
184
|
+
"#{@key_prefix}:#{cleanse_key key}:#{attribute}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def cleanse_key(key)
|
188
|
+
key.to_s.delete INVALID_KEY_CHARS
|
117
189
|
end
|
118
190
|
end
|
119
191
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dalli-rate_limiter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-01
|
12
|
+
date: 2016-02-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dalli
|
@@ -165,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
165
|
version: '0'
|
166
166
|
segments:
|
167
167
|
- 0
|
168
|
-
hash:
|
168
|
+
hash: 2846745512546726026
|
169
169
|
requirements: []
|
170
170
|
rubyforge_project:
|
171
171
|
rubygems_version: 1.8.23.2
|