dalli-rate_limiter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/mwpastore/dalli-rate_limiter.svg?branch=master)](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: []
|