chokepoint 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/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
+