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 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: []