dalli-rate_limiter 0.1.1 → 0.1.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.
- 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
|