dalli-rate_limiter 0.1.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.
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +10 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +193 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dalli-rate_limiter.gemspec +32 -0
- data/lib/dalli-rate_limiter.rb +1 -0
- data/lib/dalli/rate_limiter.rb +119 -0
- data/lib/dalli/rate_limiter/version.rb +5 -0
- metadata +175 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
inherit_from:
|
2
|
+
- http://relaxed.ruby.style/rubocop.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
Exclude:
|
6
|
+
- "**/spec_helper.rb"
|
7
|
+
- "**/*_spec.rb"
|
8
|
+
- "lib/dalli-rate_limiter.rb"
|
9
|
+
|
10
|
+
# It's more consistent to use the old style everywhere. If anything, I want to
|
11
|
+
# disable the new style!
|
12
|
+
Style/HashSyntax:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
# I find it clearer to continue multiline operations with leading periods.
|
16
|
+
# English reads left-to-right; we don't look at the ends of lines to see if
|
17
|
+
# they continue on the next.
|
18
|
+
Style/MultilineOperationIndentation:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
# I'm not entirely opposed to this but it's going to add a ton of lines to the
|
22
|
+
# code base...
|
23
|
+
Style/AlignParameters:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
# What, and indent this entire file another stop? No way!
|
27
|
+
Style/ClassAndModuleChildren:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
# Umm, no, commented code should have no leading space.
|
31
|
+
Style/LeadingCommentSpace:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
# WHY IS THIS FEATURE DISCOURAGED IF I WANTED TO WRITE JAVA I WOULD WRITE JAVA
|
35
|
+
Style/UnlessElse:
|
36
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Mike Pastore
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
# Dalli::RateLimiter [](https://travis-ci.org/mwpastore/dalli-rate_limiter)
|
2
|
+
|
3
|
+
**Dalli::RateLimiter** provides arbitrary [Memcached][6]-backed rate limiting
|
4
|
+
for your Ruby applications. You may be using an application-level rate limiter
|
5
|
+
such as [Rack::Ratelimit][1], [Rack::Throttle][2], or [Rack::Attack][3], or
|
6
|
+
something higher up in your stack (like an Nginx zone or HAproxy stick-table).
|
7
|
+
This is not intended to be a replacement for any of those functions. Your
|
8
|
+
application may not even be a web service and yet you find yourself needing to
|
9
|
+
throttle certain types of operations.
|
10
|
+
|
11
|
+
This library allows you to impose specific rate limits on specific functions at
|
12
|
+
whatever granularity you desire. For example, you have a function in your Ruby
|
13
|
+
web application that allows users to change their username, but you want to
|
14
|
+
limit these requests to two per hour per user. Or your command-line Ruby
|
15
|
+
application makes API calls over HTTP, but you must adhere to a strict rate
|
16
|
+
limit imposed by the provider for a certain endpoint. It wouldn't make sense to
|
17
|
+
apply these limits at the application level—it would be much easier to
|
18
|
+
tightly integrate a check within your business logic.
|
19
|
+
|
20
|
+
**Dalli::RateLimiter** leverages the excellent [Dalli][4] and
|
21
|
+
[ConnectionPool][5] gems for fast and efficient Memcached access and
|
22
|
+
thread-safe connection pooling. It uses an allowance counter and floating
|
23
|
+
timestamp to implement a sliding window for each unique key, enforcing a limit
|
24
|
+
of _m_ requests over a period of _n_ seconds. It supports arbitrary unit
|
25
|
+
quantities of consumption for operations that logically count as more than one
|
26
|
+
request (i.e. batched requests). A simple mutex locking scheme (enabled by
|
27
|
+
default) is used to mitigate race conditions and ensure that the limit is
|
28
|
+
enforced under most cirumstances (see [Caveats](#caveats) below). Math
|
29
|
+
operations are performed with three decimal places of precision but the results
|
30
|
+
are stored in Memcached as integers.
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
Add this line to your application's Gemfile:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
gem 'dalli-rate_limiter', '~> 0.1.0'
|
38
|
+
```
|
39
|
+
|
40
|
+
And then execute:
|
41
|
+
|
42
|
+
$ bundle install
|
43
|
+
|
44
|
+
Or install it yourself as:
|
45
|
+
|
46
|
+
$ gem install dalli-rate_limiter
|
47
|
+
|
48
|
+
## Basic Usage
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
lim = Dalli::RateLimiter.new
|
52
|
+
|
53
|
+
if lim.exceeded? "foo"
|
54
|
+
fail "Sorry, can't foo right now. Try again later!"
|
55
|
+
else
|
56
|
+
# ..
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
**Dalli::RateLimiter** will, by default, create a ConnectionPool with the
|
61
|
+
default options, using a block that yields Dalli::Client instances with the
|
62
|
+
default options. If `MEMCACHE_SERVERS` is set in your environment, or if your
|
63
|
+
Memcached instance is running on localhost, port 11211, this is the quickest
|
64
|
+
way to get started. Alternatively, you can pass in your own single-threaded
|
65
|
+
Dalli::Client instance—or your own multi-threaded ConnectionPool instance
|
66
|
+
(wrapping Dalli::Client)—as the first argument to customize the
|
67
|
+
connection settings. Pass in `nil` to force the default behavior.
|
68
|
+
|
69
|
+
The library itself defaults to five (5) requests per eight (8) seconds, but
|
70
|
+
these can easily be changed with the `:max_requests` and `:period` options.
|
71
|
+
Locking can be disabled by setting the `:locking` option to `false` (see
|
72
|
+
[Caveats](#caveats) below). A `:key_prefix` option can be specified as well;
|
73
|
+
note that this will be used in combination with any `:namespace` option defined
|
74
|
+
in the Dalli::Client.
|
75
|
+
|
76
|
+
The **Dalli::RateLimiter** instance itself is not stateful, so it can be
|
77
|
+
instantiated as needed (e.g. in a function definition) or in a more global
|
78
|
+
scope (e.g. in a Rails initializer). It does not mutate any of its own
|
79
|
+
attributes so it should be safe to share between threads; in this case, you
|
80
|
+
will definitely want to use either the default ConnectionPool or your own (as
|
81
|
+
opposed to a single-threaded Dalli::Client instance).
|
82
|
+
|
83
|
+
The main instance method, `#exceeded?` will return a falsy value if the request
|
84
|
+
is free to proceed. If the limit has been exceeded, it will return a floating
|
85
|
+
point value that represents the fractional number of seconds that the caller
|
86
|
+
should wait until retrying the request. Assuming no other requests were process
|
87
|
+
during that time, the retried request will be free to proceed at that point.
|
88
|
+
When invoking this method, please be sure to pass in a key that is unique (in
|
89
|
+
combination with the `:key_prefix` option described above) to the thing you are
|
90
|
+
trying to limit. An optional second argument specifies the number of requests
|
91
|
+
to "consume" from the allowance; this defaults to one (1). Please note that if
|
92
|
+
the number of requests is greater than the maximum number of requests, it will
|
93
|
+
never not be limited. Consider a limit of 50 requests per minute: no amount of
|
94
|
+
waiting would allow for a batch of 51 requests! To help check for this, a
|
95
|
+
public getter method `#max_requests` is available.
|
96
|
+
|
97
|
+
## Advanced Usage
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
dalli = ConnectionPool.new(:size => 5, :timeout => 3) {
|
101
|
+
Dalli::Client.new(nil, :namespace => "myapp")
|
102
|
+
}
|
103
|
+
|
104
|
+
lim1 = Dalli::RateLimiter.new dalli,
|
105
|
+
:key_prefix => "username-throttle", :max_requests => 2, :period => 3_600
|
106
|
+
|
107
|
+
lim2 = Dalli::RateLimiter.new dalli,
|
108
|
+
:key_prefix => "widgets-throttle", :max_requests => 10, :period => 60
|
109
|
+
|
110
|
+
def change_username(user_id, new_username)
|
111
|
+
if lim1.exceeded? user_id
|
112
|
+
halt 422, "Sorry! Only two username changes allowed per hour."
|
113
|
+
end
|
114
|
+
|
115
|
+
# ..
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_widgets(foo_id, some_widgets)
|
119
|
+
if some_widgets.length > lim2.max_requests
|
120
|
+
halt 400, "Too many widgets!"
|
121
|
+
end
|
122
|
+
|
123
|
+
if time = lim2.exceeded? foo_id, some_widgets.length
|
124
|
+
halt 422, "Sorry! Unable to process request. " \
|
125
|
+
"Please wait at least #{time} seconds before trying again."
|
126
|
+
end
|
127
|
+
|
128
|
+
# ..
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
## Compatibility
|
133
|
+
|
134
|
+
**Dalli::RateLimiter** is compatible with Ruby 1.9.3 and greater and has been
|
135
|
+
tested with frozen string literals under Ruby 2.3.0.
|
136
|
+
|
137
|
+
You might consider installing the [kgio][7] gem to [give Dalli a 10-20%
|
138
|
+
performance boost][8].
|
139
|
+
|
140
|
+
## Caveats
|
141
|
+
|
142
|
+
A rate-limiting system is only as good as its backing store, and it should be
|
143
|
+
noted that a Memcached ring can lose members or indeed its entire working set
|
144
|
+
at the drop of a hat. Mission-critical use cases, where operations absolutely,
|
145
|
+
positively have to be idempotent, should probably seek solutions elsewhere.
|
146
|
+
|
147
|
+
The limiting algorithm seems to work well but it is far from battle-tested. I
|
148
|
+
tried to use atomic operations where possible to mitigate race conditions, but
|
149
|
+
still had to implement a locking scheme, which might slow down operations and
|
150
|
+
lead to timeouts and exceptions if a lock can't be acquired for some reason.
|
151
|
+
Locking can be disabled but this will increase the chances that a determined
|
152
|
+
attacker figures out a way to defeat the limit.
|
153
|
+
|
154
|
+
I will likely be revisiting the algorithm in the future, but at the moment it
|
155
|
+
is in the unfortunate state of "good enough".
|
156
|
+
|
157
|
+
As noted above, this is not a replacement for an application-level rate limit,
|
158
|
+
and if your application faces the web, you should probably definitely have
|
159
|
+
something else in your stack to handle e.g. a casual DoS.
|
160
|
+
|
161
|
+
Make sure your ConnectionPool has enough slots for these operations. I aim for
|
162
|
+
one slot per thread plus one or two for overhead in my applications.
|
163
|
+
|
164
|
+
## Development
|
165
|
+
|
166
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
167
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
168
|
+
prompt that will allow you to experiment.
|
169
|
+
|
170
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
171
|
+
release a new version, update the version number in `version.rb`, and then run
|
172
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
173
|
+
git commits and tags, and push the `.gem` file to
|
174
|
+
[rubygems.org](https://rubygems.org).
|
175
|
+
|
176
|
+
## Contributing
|
177
|
+
|
178
|
+
Bug reports and pull requests are welcome on GitHub at
|
179
|
+
https://github.com/mwpastore/dalli-rate_limiter.
|
180
|
+
|
181
|
+
## License
|
182
|
+
|
183
|
+
The gem is available as open source under the terms of the [MIT
|
184
|
+
License](http://opensource.org/licenses/MIT).
|
185
|
+
|
186
|
+
[1]: https://github.com/jeremy/rack-ratelimit "Rate::Ratelimit"
|
187
|
+
[2]: https://github.com/bendiken/rack-throttle "Rack::Throttle"
|
188
|
+
[3]: https://github.com/kickstarter/rack-attack "Rack::Attack"
|
189
|
+
[4]: https://github.com/petergoldstein/dalli "Dalli"
|
190
|
+
[5]: https://github.com/mperham/connection_pool "ConnectionPool"
|
191
|
+
[6]: http://memcached.org "Memcached"
|
192
|
+
[7]: http://bogomips.org/kgio "kgio"
|
193
|
+
[8]: https://github.com/petergoldstein/dalli/blob/master/Performance.md "Dalli Performance"
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "dalli/rate_limiter"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "dalli/rate_limiter/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dalli-rate_limiter"
|
8
|
+
spec.version = Dalli::RateLimiter::VERSION
|
9
|
+
spec.authors = ["Mike Pastore"]
|
10
|
+
spec.email = ["mike@oobak.org"]
|
11
|
+
|
12
|
+
spec.summary = "Arbitrary Memcached-backed rate limiting for Ruby"
|
13
|
+
spec.description = spec.summary
|
14
|
+
spec.homepage = "https://github.com/mwpastore/dalli-rate_limiter"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^#{spec.bindir}/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.required_ruby_version = '>= 1.9.3'
|
23
|
+
|
24
|
+
spec.add_runtime_dependency "dalli", "~> 2.7.5"
|
25
|
+
spec.add_runtime_dependency "connection_pool", "~> 2.2.0"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.11.0"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.5.0"
|
29
|
+
spec.add_development_dependency "rubocop", "~> 0.35.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.4.0"
|
31
|
+
spec.add_development_dependency "rspec-given", "~> 3.8.0"
|
32
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "dalli/rate_limiter"
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dalli"
|
4
|
+
require "connection_pool"
|
5
|
+
|
6
|
+
require "dalli/rate_limiter/version"
|
7
|
+
|
8
|
+
module Dalli
|
9
|
+
INVALID_KEY_CHARS = [
|
10
|
+
0x00, 0x20,
|
11
|
+
0x09, 0x0a,
|
12
|
+
0x0d
|
13
|
+
].map(&:chr).join("").freeze
|
14
|
+
|
15
|
+
class RateLimiter
|
16
|
+
LOCK_TTL = 30
|
17
|
+
LOCK_MAX_TRIES = 6
|
18
|
+
|
19
|
+
def initialize(dalli = nil, options = {})
|
20
|
+
@dalli = dalli || ConnectionPool.new { Dalli::Client.new }
|
21
|
+
|
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
|
26
|
+
end
|
27
|
+
|
28
|
+
def exceeded?(unique_key, to_consume = 1)
|
29
|
+
to_consume = to_ems(to_consume)
|
30
|
+
|
31
|
+
timestamp_key = format_key(unique_key, "timestamp")
|
32
|
+
allowance_key = format_key(unique_key, "allowance")
|
33
|
+
|
34
|
+
@dalli.with do |dc|
|
35
|
+
if to_consume <= @max_requests
|
36
|
+
if dc.add(allowance_key, @max_requests - to_consume, @period, :raw => true)
|
37
|
+
# Short-circuit the simple case of seeing the key for the first time.
|
38
|
+
dc.set(timestamp_key, to_ems(Time.now.to_f), @period, :raw => true)
|
39
|
+
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
lock = acquire_lock(dc, unique_key) if @locking
|
45
|
+
|
46
|
+
current_timestamp = to_ems(Time.now.to_f) # obtain timestamp after locking
|
47
|
+
|
48
|
+
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
|
51
|
+
|
52
|
+
allowance_delta = (1.0 * (current_timestamp - previous_timestamp) * @max_requests / @period).to_i
|
53
|
+
projected_allowance = previous_allowance + allowance_delta
|
54
|
+
if projected_allowance > @max_requests
|
55
|
+
projected_allowance = @max_requests
|
56
|
+
allowance_delta = @max_requests - previous_allowance
|
57
|
+
end
|
58
|
+
|
59
|
+
if to_consume > projected_allowance
|
60
|
+
release_lock(dc, unique_key) if lock
|
61
|
+
|
62
|
+
# Tell the caller how long (in seconds) to wait before retrying the request.
|
63
|
+
return to_fs((1.0 * (to_consume - projected_allowance) * @period / @max_requests).to_i)
|
64
|
+
end
|
65
|
+
|
66
|
+
allowance_delta -= to_consume
|
67
|
+
|
68
|
+
dc.set(timestamp_key, current_timestamp, @period, :raw => true)
|
69
|
+
dc.add(allowance_key, previous_allowance, 0, :raw => true) # ensure baseline exists
|
70
|
+
dc.send(allowance_delta < 0 ? :decr : :incr, allowance_key, allowance_delta.abs)
|
71
|
+
dc.touch(allowance_key, @period)
|
72
|
+
|
73
|
+
release_lock(dc, unique_key) if lock
|
74
|
+
|
75
|
+
return nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def max_requests
|
80
|
+
to_fs(@max_requests)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def acquire_lock(dc, key)
|
86
|
+
lock_key = format_key(key, "mutex")
|
87
|
+
|
88
|
+
(1..LOCK_MAX_TRIES).each do |tries|
|
89
|
+
if lock = dc.add(lock_key, true, LOCK_TTL)
|
90
|
+
return lock
|
91
|
+
else
|
92
|
+
sleep rand(2**tries)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
raise DalliError, "Unable to lock key for update"
|
97
|
+
end
|
98
|
+
|
99
|
+
def release_lock(dc, key)
|
100
|
+
lock_key = format_key(key, "mutex")
|
101
|
+
|
102
|
+
dc.delete(lock_key)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Convert fractional units to encoded milliunits
|
106
|
+
def to_ems(fs)
|
107
|
+
(fs * 1_000).to_i
|
108
|
+
end
|
109
|
+
|
110
|
+
# Convert encoded milliunits to fractional units
|
111
|
+
def to_fs(ems)
|
112
|
+
1.0 * ems / 1_000
|
113
|
+
end
|
114
|
+
|
115
|
+
def format_key(key, attribute)
|
116
|
+
"#{@key_prefix}:#{key.to_s.delete INVALID_KEY_CHARS}:#{attribute}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dalli-rate_limiter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Pastore
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-01-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: dalli
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.7.5
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.7.5
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: connection_pool
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.2.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.2.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: bundler
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.11.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.11.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 10.5.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 10.5.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rubocop
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.35.0
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.35.0
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rspec
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 3.4.0
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 3.4.0
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rspec-given
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 3.8.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 3.8.0
|
126
|
+
description: Arbitrary Memcached-backed rate limiting for Ruby
|
127
|
+
email:
|
128
|
+
- mike@oobak.org
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- .rspec
|
135
|
+
- .rubocop.yml
|
136
|
+
- .travis.yml
|
137
|
+
- Gemfile
|
138
|
+
- LICENSE.txt
|
139
|
+
- README.md
|
140
|
+
- Rakefile
|
141
|
+
- bin/console
|
142
|
+
- bin/setup
|
143
|
+
- dalli-rate_limiter.gemspec
|
144
|
+
- lib/dalli-rate_limiter.rb
|
145
|
+
- lib/dalli/rate_limiter.rb
|
146
|
+
- lib/dalli/rate_limiter/version.rb
|
147
|
+
homepage: https://github.com/mwpastore/dalli-rate_limiter
|
148
|
+
licenses:
|
149
|
+
- MIT
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
none: false
|
156
|
+
requirements:
|
157
|
+
- - ! '>='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 1.9.3
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
segments:
|
167
|
+
- 0
|
168
|
+
hash: 717744935584309687
|
169
|
+
requirements: []
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 1.8.23.2
|
172
|
+
signing_key:
|
173
|
+
specification_version: 3
|
174
|
+
summary: Arbitrary Memcached-backed rate limiting for Ruby
|
175
|
+
test_files: []
|