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.
@@ -2,9 +2,13 @@ language: ruby
2
2
  services:
3
3
  - memcached
4
4
  rvm:
5
- - 1.9
6
- - 2.0
7
- - 2.1
8
- - 2.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1.8
8
+ - 2.2.4
9
9
  - 2.3.0
10
+ - jruby-1.7.23
11
+ - jruby-9.0.5.0
12
+ - rbx-2.5.8
13
+ - rbx-3.14
10
14
  before_install: gem install bundler -v 1.11.2
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 a falsy value if the request
86
- is free to proceed. If the limit has been exceeded, it will return a positive
87
- floating point value that represents the fractional number of seconds that the
88
- caller should wait until retrying the request. Assuming no other requests were
89
- process during that time, the retried request will be free to proceed at that
90
- point. When invoking this method, please be sure to pass in a key that is
91
- unique (in combination with the `:key_prefix` option described above) to the
92
- thing you are trying to limit. An optional second argument specifies the number
93
- of requests to "consume" from the allowance; this defaults to one (1).
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 a negative integer in this event. To help detect
99
- this edge case proactively, a public getter method `#max_requests` is
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
 
@@ -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
- @key_prefix = options[:key_prefix] || "dalli-rate_limiter"
23
- @max_requests = to_ems(options[:max_requests] || 5)
24
- @period = to_ems(options[:period] || 8)
25
- @locking = options.key?(:locking) ? !!options[:locking] : true
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 nil
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 || @max_requests
50
- previous_timestamp = previous[timestamp_key].to_i || current_timestamp
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 nil
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.to_s.delete INVALID_KEY_CHARS}:#{attribute}"
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
@@ -1,5 +1,5 @@
1
1
  module Dalli
2
2
  class RateLimiter
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  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.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-31 00:00:00.000000000 Z
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: 132983352658914105
168
+ hash: 2846745512546726026
169
169
  requirements: []
170
170
  rubyforge_project:
171
171
  rubygems_version: 1.8.23.2