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 ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .rbenv-vars
11
+ .ruby-version
12
+ .rubocop-http*yml
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
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
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ services:
3
+ - memcached
4
+ rvm:
5
+ - 1.9
6
+ - 2.0
7
+ - 2.1
8
+ - 2.2
9
+ - 2.3.0
10
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ RuboCop::RakeTask.new(:rubocop)
7
+
8
+ task :default => :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,5 @@
1
+ module Dalli
2
+ class RateLimiter
3
+ VERSION = "0.1.0"
4
+ end
5
+ 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: []