chokepoint 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ The MIT License (MIT) Copyright (c) 2011 Viximo, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4
+ associated documentation files (the "Software"), to deal in the Software without restriction,
5
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial
10
+ portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,81 @@
1
+ Chokepoint: Rate Limiter
2
+ ========================
3
+
4
+ This library provides throttling for arbitary blocks. Chokepoint is based on Arto Bendiken's
5
+ rack-throttle library.
6
+
7
+ * Chokepoint <http://github.com/Viximo/chokepoint>
8
+ * Rack::Throttle <http://github.com/bendiken/rack-throttle>
9
+
10
+ Features
11
+ --------
12
+
13
+ * Throttles a block by enforcing a minimum time interval between
14
+ subsequent calls or by enforcing a maximum number of calls in a given
15
+ duration (Daily, Hourly, Minute)
16
+ * Stores rate-limiting counters in any key/value store implementation that
17
+ responds to `#[]`/`#[]=` (like Ruby's hashes) or to `#get`/`#set` (like
18
+ memcached or Redis).
19
+ * Compatible with the gdbm binding included in Ruby's standard library.
20
+ * Compatible with the memcached, memcache-client, memcache and
21
+ redis gems.
22
+ * Compatible with Heroku's memcached add-on.
23
+ * Compatible with Ruby 1.8.7 & 1.9
24
+
25
+ Examples
26
+ --------
27
+
28
+ ### Enforcing a minimum 3-second interval between calls
29
+
30
+ Chokepoint::Interval('my block', :min => 3.0).throttle do
31
+ ...
32
+ end
33
+
34
+ ### Allowing a maximum of 60 requests per minute
35
+
36
+ Chokepoint::Minute('my block', :max => 60).throttle do
37
+ ...
38
+ end
39
+
40
+ ### Storing the rate-limiting counters on a Memcached server
41
+
42
+ require 'memcached'
43
+
44
+ Checkpoint::Interval.new('my block', :cache => Memcached.new, :key_prefix => :throttle) do
45
+ ...
46
+ end
47
+
48
+ Throttling Strategies
49
+ ---------------------
50
+
51
+ Chokepoint supports four built-in throttling strategies:
52
+
53
+ * `Chokepoint::Interval`: Throttles the application by enforcing a
54
+ minimum interval (by default, 1 second) between calls.
55
+ * `Chokepoint::Minute`: Throttles the application by defining a
56
+ maximum number of allowed calls per minute (by default, 60).
57
+ * `Chokepoint::Hourly`: Throttles the application by defining a
58
+ maximum number of allowed calls per hour (by default, 3,600).
59
+ * `Chokepoint::Daily`: Throttles the application by defining a
60
+ maximum number of allowed calls per day (by default, 86,400).
61
+
62
+ You can fully customize the implementation details of any of these strategies
63
+ by simply subclassing one of the aforementioned default implementations.
64
+ And, of course, should your application-specific requirements be
65
+ significantly more complex than what we've provided for, you can also define
66
+ entirely new kinds of throttling strategies by subclassing the
67
+ `Chokepoint::Limiter` base class directly.
68
+
69
+ Authors
70
+ -------
71
+
72
+ * [Matt Griffin](mailto:matt@griffinonline.org)
73
+
74
+ Based on [rack-throttle] (Public Domain) work by
75
+ [Arto Bendiken](mailto:arto.bendiken@gmail.com) and [Brendon Murphy](mailto:disposable.20.xternal@spamourmet.com>)
76
+
77
+
78
+ License
79
+ -------
80
+
81
+ Distribution is allowed under the MIT License.
@@ -0,0 +1,11 @@
1
+ module Chokepoint
2
+ autoload :Limiter, 'chokepoint/limiter'
3
+ autoload :Daily, 'chokepoint/daily'
4
+ autoload :Hourly, 'chokepoint/hourly'
5
+ autoload :Minute, 'chokepoint/minute'
6
+ autoload :Interval, 'chokepoint/interval'
7
+ autoload :TimeWindow, 'chokepoint/time_window'
8
+ autoload :VERSION, 'chokepoint/version'
9
+
10
+ class RateLimitExceeded < RuntimeError; end
11
+ end
@@ -0,0 +1,44 @@
1
+ module Chokepoint
2
+ ##
3
+ # This rate limiter strategy throttles the block by defining a
4
+ # maximum number of allowed calls per day (by default, 86,400
5
+ # requests per 24 hours, which works out to an average of 1 request per
6
+ # second).
7
+ #
8
+ # Note that this strategy doesn't use a sliding time window, but rather
9
+ # tracks requests per calendar day. This means that the throttling counter
10
+ # is reset at midnight (according to the local timezone) every
11
+ # night.
12
+ #
13
+ # @example Allowing up to 86,400 requests per day
14
+ # Chokepoint::Daily.new('activity').throttle do ... end
15
+ #
16
+ # @example Allowing up to 1,000 requests per day
17
+ # Chokepoint::Daily.new('activity', :max => 1000).throttle do ... end
18
+ #
19
+ class Daily < TimeWindow
20
+ ##
21
+ # @param [String] name
22
+ # @param [Hash{Symbol => Object}] options
23
+ # @option options [Integer] :max (86400)
24
+ def initialize(name, options = {})
25
+ super
26
+ end
27
+
28
+ ##
29
+ def max_per_day
30
+ @max_per_hour ||= options[:max] || 86_400
31
+ end
32
+
33
+ alias_method :max_per_window, :max_per_day
34
+
35
+ protected
36
+
37
+ ##
38
+ # @param [Object] context
39
+ # @return [String]
40
+ def cache_key(context)
41
+ [super, Time.now.strftime('%Y-%m-%d')].join(':')
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ module Chokepoint
2
+ ##
3
+ # This rate limiter strategy throttles the block by defining a
4
+ # maximum number of allowed calls per hour (by default, 3,600
5
+ # requests per 60 minutes, which works out to an average of 1 request per
6
+ # second).
7
+ #
8
+ # Note that this strategy doesn't use a sliding time window, but rather
9
+ # tracks calls per distinct hour. This means that the throttling
10
+ # counter is reset every hour on the hour (according to the local
11
+ # timezone).
12
+ #
13
+ # @example Allowing up to 3,600 requests per hour
14
+ # Chokepoint::Hourly.new('activity').throttle do ... end
15
+ #
16
+ # @example Allowing up to 100 requests per hour
17
+ # Chokepoint::Hourly.new('activity', :max => 100).throttle do ... end
18
+ #
19
+ class Hourly < TimeWindow
20
+ ##
21
+ # @param [String] name
22
+ # @param [Hash{Symbol => Object}] options
23
+ # @option options [Integer] :max (3600)
24
+ def initialize(name, options = {})
25
+ super
26
+ end
27
+
28
+ ##
29
+ def max_per_hour
30
+ @max_per_hour ||= options[:max] || 3_600
31
+ end
32
+
33
+ alias_method :max_per_window, :max_per_hour
34
+
35
+ protected
36
+
37
+ ##
38
+ # @param [Object] context
39
+ # @return [String]
40
+ def cache_key(context)
41
+ [super, Time.now.strftime('%Y-%m-%dT%H')].join(':')
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,52 @@
1
+ module Chokepoint
2
+ ##
3
+ # This rate limiter strategy throttles the block by enforcing a
4
+ # minimum interval (by default, 1 second) between subsequent allowed calls.
5
+ #
6
+ # @example Allowing up to two requests per second
7
+ # Chokepoint::Interval.new('activity', :min => 0.5) # 500 ms interval
8
+ #
9
+ # @example Allowing a request every two seconds
10
+ # Chokepoint::Interval.new('activity', :min => 2) # 2000 ms interval
11
+ #
12
+ class Interval < Limiter
13
+ ##
14
+ # @param [String] name
15
+ # @param [Hash{Symbol => Object}] options
16
+ # @option options [Float] :min (1.0)
17
+ def initialize(name, options = {})
18
+ super
19
+ end
20
+
21
+ ##
22
+ # Returns `true` if sufficient time (equal to or more than
23
+ # {#minimum_interval}) has passed since the last call.
24
+ #
25
+ # @param [Object] context
26
+ # @return [Boolean]
27
+ def allowed?(context)
28
+ time_now = Time.now.to_f
29
+ last_call_time = cache_get(key = cache_key(context)) rescue nil
30
+ allowed = !last_call_time || (dt = time_now - last_call_time.to_f) >= minimum_interval
31
+ begin
32
+ cache_set(key, time_now)
33
+ allowed
34
+ rescue => e
35
+ # If an error occurred while trying to update the timestamp stored
36
+ # in the cache, we will fall back to allowing the request through.
37
+ # This prevents the application blowing up merely due to a
38
+ # backend cache server (Memcached, Redis, etc.) being offline.
39
+ allowed = true
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Returns the required minimal interval (in terms of seconds) that must
45
+ # elapse between two subsequent calls.
46
+ #
47
+ # @return [Float]
48
+ def minimum_interval
49
+ @min ||= (@options[:min] || 1.0).to_f
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,155 @@
1
+ module Chokepoint
2
+ ##
3
+ # This is the base class for rate limiter implementations.
4
+ #
5
+ # @example Defining a rate limiter subclass
6
+ # class MyLimiter < Limiter
7
+ # def allowed?(request)
8
+ # # TODO: custom logic goes here
9
+ # end
10
+ # end
11
+ #
12
+ class Limiter
13
+ attr_reader :name
14
+ attr_reader :options
15
+
16
+ ##
17
+ # @param [String] name
18
+ # @param [Hash{Symbol => Object}] options
19
+ # @option options [String] :cache (Hash.new)
20
+ # @option options [String] :key_prefix (nil)
21
+ # @option options [Integer] :code (403)
22
+ # @option options [String] :message ("Rate Limit Exceeded")
23
+ def initialize(name, options = {})
24
+ @name, @options = name, options
25
+ end
26
+
27
+ def throttle(context = nil)
28
+ allowed?(context) ? yield : rate_limit_exceeded(context)
29
+ end
30
+
31
+ ##
32
+ # Returns `false` if the rate limit has been exceeded for the given
33
+ # `request`, or `true` otherwise.
34
+ #
35
+ # Override this method in subclasses that implement custom rate limiter
36
+ # strategies.
37
+ #
38
+ # @param [Object] context
39
+ # @return [Boolean]
40
+ def allowed?(context = nil)
41
+ case
42
+ when whitelisted?(context) then true
43
+ when blacklisted?(context) then false
44
+ else true # override in subclasses
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Returns `true` if the originator of the given `request` is whitelisted
50
+ # (not subject to further rate limits).
51
+ #
52
+ # The default implementation always returns `false`. Override this
53
+ # method in a subclass to implement custom whitelisting logic.
54
+ #
55
+ # @param [Object] context
56
+ # @return [Boolean]
57
+ # @abstract
58
+ def whitelisted?(context)
59
+ false
60
+ end
61
+
62
+ ##
63
+ # Returns `true` if the originator of the given `request` is blacklisted
64
+ # (not honoring rate limits, and thus permanently forbidden access
65
+ # without the need to maintain further rate limit counters).
66
+ #
67
+ # The default implementation always returns `false`. Override this
68
+ # method in a subclass to implement custom blacklisting logic.
69
+ #
70
+ # @param [Object] context
71
+ # @return [Boolean]
72
+ # @abstract
73
+ def blacklisted?(context)
74
+ false
75
+ end
76
+
77
+ protected
78
+
79
+ ##
80
+ # @return [Hash]
81
+ def cache
82
+ case cache = (options[:cache] ||= {})
83
+ when Proc then cache.call
84
+ else cache
85
+ end
86
+ end
87
+
88
+ ##
89
+ # @param [String] key
90
+ def cache_has?(key)
91
+ case
92
+ when cache.respond_to?(:has_key?)
93
+ cache.has_key?(key)
94
+ when cache.respond_to?(:get)
95
+ cache.get(key) rescue false
96
+ else false
97
+ end
98
+ end
99
+
100
+ ##
101
+ # @param [String] key
102
+ # @return [Object]
103
+ def cache_get(key, default = nil)
104
+ case
105
+ when cache.respond_to?(:[])
106
+ cache[key] || default
107
+ when cache.respond_to?(:get)
108
+ cache.get(key) || default
109
+ end
110
+ end
111
+
112
+ ##
113
+ # @param [String] key
114
+ # @param [Object] value
115
+ # @return [void]
116
+ def cache_set(key, value)
117
+ case
118
+ when cache.respond_to?(:[]=)
119
+ begin
120
+ cache[key] = value
121
+ rescue TypeError => e
122
+ # GDBM throws a "TypeError: can't convert Float into String"
123
+ # exception when trying to store a Float. On the other hand, we
124
+ # don't want to unnecessarily coerce the value to a String for
125
+ # any stores that do support other data types (e.g. in-memory
126
+ # hash objects). So, this is a compromise.
127
+ cache[key] = value.to_s
128
+ end
129
+ when cache.respond_to?(:set)
130
+ cache.set(key, value)
131
+ end
132
+ end
133
+
134
+ ##
135
+ # @param [Object] context
136
+ # @return [String]
137
+ def cache_key(context)
138
+ case
139
+ when options.has_key?(:key_prefix)
140
+ [options[:key_prefix], name].join(':')
141
+ else name
142
+ end
143
+ end
144
+
145
+
146
+ ##
147
+ # Raise a RateLimitExceeded error
148
+ #
149
+ # @param [Object] context
150
+ # @return [void]
151
+ def rate_limit_exceeded(context)
152
+ raise RateLimitExceeded, "Rate Limit Exceeded for #{name}"
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,43 @@
1
+ module Chokepoint
2
+ ##
3
+ # This rate limiter strategy throttles the block by defining a
4
+ # maximum number of allowed calls per minute (by default, 60
5
+ # requests per minute, which works out to an average of 1 request per
6
+ # second).
7
+ #
8
+ # Note that this strategy doesn't use a sliding time window, but rather
9
+ # tracks calls per distinct minute. This means that the throttling
10
+ # counter is reset every minute.
11
+ #
12
+ # @example Allowing up to 60 requests/minute
13
+ # Chokepoint::Minute.new('activity').throttle do ... end
14
+ #
15
+ # @example Allowing up to 100 requests per minute
16
+ # Chokepoint::Minute.new('activity', :max => 100).throttle do ... end
17
+ #
18
+ class Minute < TimeWindow
19
+ ##
20
+ # @param [String] name
21
+ # @param [Hash{Symbol => Object}] options
22
+ # @option options [Integer] :max (60)
23
+ def initialize(name, options = {})
24
+ super
25
+ end
26
+
27
+ ##
28
+ def max_per_minute
29
+ @max_per_hour ||= options[:max] || 60
30
+ end
31
+
32
+ alias_method :max_per_window, :max_per_minute
33
+
34
+ protected
35
+
36
+ ##
37
+ # @param [Object] context
38
+ # @return [String]
39
+ def cache_key(context)
40
+ [super, Time.now.strftime('%Y-%m-%dT%H:%M')].join(':')
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ module Chokepoint
2
+ ##
3
+ class TimeWindow < Limiter
4
+ ##
5
+ # Returns `true` if fewer than the maximum number of requests permitted
6
+ # for the current window of time have been made.
7
+ #
8
+ # @param [Object] context
9
+ # @return [Boolean]
10
+ def allowed?(context)
11
+ count = cache_get(key = cache_key(context)).to_i + 1 rescue 1
12
+ allowed = count <= max_per_window.to_i
13
+ begin
14
+ cache_set(key, count)
15
+ allowed
16
+ rescue => e
17
+ allowed = true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module Chokepoint
2
+
3
+ module VERSION
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ PATCH = 0
7
+
8
+ def self.to_s
9
+ [MAJOR, MINOR, PATCH].join('.')
10
+ end
11
+ end
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chokepoint
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Matt Griffin
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-02 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 2
32
+ - 0
33
+ version: "2.0"
34
+ type: :development
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: timecop
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :development
49
+ version_requirements: *id002
50
+ description: Wrap arbitrary blocks with a chokpoint to throttle access. Chokepoints can be extended with various throttling behaviors as needed.
51
+ email: matt@griffinonline.org
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - lib/chokepoint.rb
60
+ - lib/chokepoint/daily.rb
61
+ - lib/chokepoint/hourly.rb
62
+ - lib/chokepoint/interval.rb
63
+ - lib/chokepoint/limiter.rb
64
+ - lib/chokepoint/minute.rb
65
+ - lib/chokepoint/time_window.rb
66
+ - lib/chokepoint/version.rb
67
+ - README.md
68
+ - LICENSE
69
+ has_rdoc: true
70
+ homepage: https://github.com/Viximo/chokepoint
71
+ licenses:
72
+ - MIT
73
+ post_install_message:
74
+ rdoc_options: []
75
+
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ hash: 3
84
+ segments:
85
+ - 0
86
+ version: "0"
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ requirements: []
97
+
98
+ rubyforge_project:
99
+ rubygems_version: 1.5.3
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Create chokepoints that throttle access to resources
103
+ test_files: []
104
+