prop 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c5753d3ad85cbb0e81257570a59b12362e7da611
4
- data.tar.gz: 7ff4f5c528bfe42bc8b15a0a7e9193e6e13238bb
3
+ metadata.gz: 2601b63983949b53ff9f31664203316614466b74
4
+ data.tar.gz: dc5deeebea017087d28a378e83783e3d7a5001ae
5
5
  SHA512:
6
- metadata.gz: b1b0d1f4239241457b30e60bd80db2301d47b38c1f6de67cbacce29f16de80d777d91ba21f1043f5f73f6f532fbdc487076d3fc97f8f5ffa20d4da3299a8a380
7
- data.tar.gz: 160b608c60ed35e72426f2c053d4a8d77643cd1506dfe9a742819b23005373ac5550a4bb7f9bcab67f406ac70d04594f085027e17fc6da45990cce44bf76d6a8
6
+ metadata.gz: fcd418b8e8bd9c96bb84ad902db628ce7c792a3ff3324b52c682ed989ad951595aa35564f26386f742ee07f73c3af42b490cc1731614ae255b4cc2d2988608eb
7
+ data.tar.gz: 43d95cc4198eacf0c3fe20182788085c778a7d20d67ffeb97c1875f3f8488275d2ea2e0aa8e997e82ffffb2aab56a7c37bbab619fb8ca930d7b3c72b94a76150
data/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
 
2
- # Prop [![Build Status](https://secure.travis-ci.org/zendesk/prop.png)](http://travis-ci.org/zendesk/prop)
2
+ # Prop [![Build Status](https://travis-ci.org/zendesk/prop.png)](https://travis-ci.org/zendesk/prop)
3
3
 
4
4
  Prop is a simple gem for rate limiting requests of any kind. It allows you to configure hooks for registering certain actions, such that you can define thresholds, register usage and finally act on exceptions once thresholds get exceeded.
5
5
 
6
- Prop uses an interval to define a window of time using simple div arithmetic. This means that it's a worst-case throttle that will allow up to two times the specified requests within the specified interval.
6
+ Prop supports two limiting strategies:
7
+
8
+ * Basic strategy (default): Prop will use an interval to define a window of time using simple div arithmetic. This means that it's a worst-case throttle that will allow up to two times the specified requests within the specified interval.
9
+ * Leaky bucket strategy: Prop also supports the [Leaky Bucket](https://en.wikipedia.org/wiki/Leaky_bucket) algorithm, which is similar to the basic strategy but also supports bursts up to a specified threshold.
7
10
 
8
11
  To get going with Prop, you first define the read and write operations. These define how you write a registered request and how to read the number of requests for a given action. For example, do something like the below in a Rails initializer:
9
12
 
@@ -34,7 +37,7 @@ end
34
37
  Once the read and write operations are defined, you can optionally define thresholds. If, for example, you want to have a threshold on accepted emails per hour from a given user, you could define a threshold and interval (in seconds) for this like so:
35
38
 
36
39
  ```ruby
37
- Prop.configure(:mails_per_hour, :threshold => 100, :interval => 1.hour, :description => "Mail rate limit exceeded")
40
+ Prop.configure(:mails_per_hour, threshold: 100, interval: 1.hour, description: "Mail rate limit exceeded")
38
41
  ```
39
42
 
40
43
  The `:mails_per_hour` in the above is called the "handle". You can now put the throttle to work with these values, by passing the handle to the respective methods in Prop:
@@ -82,7 +85,7 @@ If the throttle! method gets called more than "threshold" times within "interval
82
85
  ```ruby
83
86
  rescue_from Prop::RateLimited do |e|
84
87
  if e.handle == :authorization_attempt
85
- render :status => :forbidden, :message => I18n.t(e.description)
88
+ render status: :forbidden, message: I18n.t(e.description)
86
89
  elsif ...
87
90
 
88
91
  end
@@ -142,8 +145,8 @@ Prop.throttle!(:mails_per_hour, nil)
142
145
  The default (and smallest possible) increment is 1, you can set that to any integer value using :increment which is handy for building time based throttles:
143
146
 
144
147
  ```ruby
145
- Prop.configure(:execute_time, :threshold => 10, :interval => 1.minute)
146
- Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { execute }).to_i)
148
+ Prop.configure(:execute_time, threshold: 10, interval: 1.minute)
149
+ Prop.throttle!(:execute_time, account.id, increment: (Benchmark.realtime { execute }).to_i)
147
150
  ```
148
151
 
149
152
  ## Optional configuration
@@ -151,9 +154,9 @@ Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { ex
151
154
  You can add optional configuration to a prop and retrieve it using `Prop.configurations[:foo]`:
152
155
 
153
156
  ```ruby
154
- Prop.configure(:api_query, :threshold => 10, :interval => 1.minute, :category => :api)
155
- Prop.configure(:api_insert, :threshold => 50, :interval => 1.minute, :category => :api)
156
- Prop.configure(:password_failure, :threshold => 5, :interval => 1.minute, :category => :auth)
157
+ Prop.configure(:api_query, threshold: 10, interval: 1.minute, category: :api)
158
+ Prop.configure(:api_insert, threshold: 50, interval: 1.minute, category: :api)
159
+ Prop.configure(:password_failure, threshold: 5, interval: 1.minute, category: :auth)
157
160
  ```
158
161
 
159
162
  ```
@@ -173,9 +176,20 @@ rescue Prop::RateLimited => e
173
176
  end
174
177
  ```
175
178
 
179
+ ## Using Leaky Bucket Algorithm
180
+
181
+ You can add two additional configurations: `:strategy` and `:burst_rate` to use the [leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket). Prop will handle the details after configured, and you don't have to specify `:strategy` again when using `throttle`, `throttle!` or any other methods.
182
+
183
+ ```ruby
184
+ Prop.configure(:api_request, strategy: :leaky_bucket, burst_rate: 20, threshold: 5, interval: 1.minute)
185
+ ```
186
+
187
+ * `:threshold` value here would be the "leak rate" of leaky bucket algorithm.
188
+
189
+
176
190
  ## License
177
191
 
178
- Copyright 2013 Zendesk
192
+ Copyright 2015 Zendesk
179
193
 
180
194
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
181
195
  You may obtain a copy of the License at
data/lib/prop.rb CHANGED
@@ -2,7 +2,7 @@ require "prop/limiter"
2
2
  require "forwardable"
3
3
 
4
4
  module Prop
5
- VERSION = "1.1.0"
5
+ VERSION = "1.2.0"
6
6
 
7
7
  # Short hand for accessing Prop::Limiter methods
8
8
  class << self
@@ -0,0 +1,59 @@
1
+ require 'prop/limiter'
2
+ require 'prop/options'
3
+ require 'prop/key'
4
+
5
+ module Prop
6
+ class IntervalStrategy
7
+ class << self
8
+ def counter(cache_key, options)
9
+ Prop::Limiter.reader.call(cache_key).to_i
10
+ end
11
+
12
+ def increment(cache_key, options, counter)
13
+ increment = options.fetch(:increment, 1)
14
+ Prop::Limiter.writer.call(cache_key, counter + increment)
15
+ end
16
+
17
+ def reset(cache_key)
18
+ Prop::Limiter.writer.call(cache_key, 0)
19
+ end
20
+
21
+ def at_threshold?(counter, options)
22
+ counter >= options.fetch(:threshold)
23
+ end
24
+
25
+ # Builds the expiring cache key
26
+ def build(options)
27
+ key = options.fetch(:key)
28
+ handle = options.fetch(:handle)
29
+ interval = options.fetch(:interval)
30
+
31
+ window = (Time.now.to_i / interval)
32
+ cache_key = Prop::Key.normalize([ handle, key, window ])
33
+
34
+ "prop/#{Digest::MD5.hexdigest(cache_key)}"
35
+ end
36
+
37
+ def threshold_reached(options)
38
+ threshold = options.fetch(:threshold)
39
+
40
+ "#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s exceeded for key '#{options[:key].inspect}', hash #{options[:cache_key]}"
41
+ end
42
+
43
+ def validate_options!(options)
44
+ validate_positive_integer(options[:threshold], :threshold)
45
+ validate_positive_integer(options[:interval], :interval)
46
+
47
+ if options.key?(:increment)
48
+ raise ArgumentError.new(":increment must be zero or a positive Integer") if !options[:increment].is_a?(Fixnum) || options[:increment] < 0
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def validate_positive_integer(option, key)
55
+ raise ArgumentError.new("#{key.inspect} must be a positive Integer") if !option.is_a?(Fixnum) || option <= 0
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/prop/key.rb CHANGED
@@ -3,18 +3,6 @@ require "digest/md5"
3
3
  module Prop
4
4
  class Key
5
5
 
6
- # Builds the expiring cache key
7
- def self.build(options)
8
- key = options.fetch(:key)
9
- handle = options.fetch(:handle)
10
- interval = options.fetch(:interval)
11
-
12
- window = (Time.now.to_i / interval)
13
- cache_key = normalize([ handle, key, window ])
14
-
15
- "prop/#{Digest::MD5.hexdigest(cache_key)}"
16
- end
17
-
18
6
  # Simple key expansion only supports arrays and primitives
19
7
  def self.normalize(key)
20
8
  if key.is_a?(Array)
@@ -0,0 +1,68 @@
1
+ require 'prop/limiter'
2
+ require 'prop/options'
3
+ require 'prop/key'
4
+ require 'prop/interval_strategy'
5
+
6
+ module Prop
7
+ class LeakyBucketStrategy
8
+ class << self
9
+ def update_bucket(cache_key, interval, leak_rate)
10
+ bucket = Prop::Limiter.reader.call(cache_key) || default_bucket
11
+ now = Time.now.to_i
12
+ leak_amount = (now - bucket[:last_updated]) / interval * leak_rate
13
+
14
+ bucket[:bucket] = [bucket[:bucket] - leak_amount, 0].max
15
+ bucket[:last_updated] = now
16
+
17
+ Prop::Limiter.writer.call(cache_key, bucket)
18
+ bucket
19
+ end
20
+
21
+ def counter(cache_key, options)
22
+ update_bucket(cache_key, options[:interval], options[:threshold]).merge(burst_rate: options[:burst_rate])
23
+ end
24
+
25
+ def increment(cache_key, options, counter)
26
+ increment = options.fetch(:increment, 1)
27
+ bucket = { :bucket => counter[:bucket].to_i + increment, :last_updated => Time.now.to_i }
28
+ Prop::Limiter.writer.call(cache_key, bucket)
29
+ end
30
+
31
+ def reset(cache_key)
32
+ Prop::Limiter.writer.call(cache_key, default_bucket)
33
+ end
34
+
35
+ def at_threshold?(counter, options)
36
+ counter[:bucket].to_i >= options.fetch(:burst_rate)
37
+ end
38
+
39
+ def build(options)
40
+ key = options.fetch(:key)
41
+ handle = options.fetch(:handle)
42
+
43
+ cache_key = Prop::Key.normalize([ handle, key ])
44
+
45
+ "prop/leaky_bucket/#{Digest::MD5.hexdigest(cache_key)}"
46
+ end
47
+
48
+ def default_bucket
49
+ { :bucket => 0, :last_updated => 0 }
50
+ end
51
+
52
+ def threshold_reached(options)
53
+ burst_rate = options.fetch(:burst_rate)
54
+ threshold = options.fetch(:threshold)
55
+
56
+ "#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s and burst rate #{burst_rate} tries exceeded for key '#{options[:key].inspect}', hash #{options[:cache_key]}"
57
+ end
58
+
59
+ def validate_options!(options)
60
+ Prop::IntervalStrategy.validate_options!(options)
61
+
62
+ if !options[:burst_rate].is_a?(Fixnum) || options[:burst_rate] < options[:threshold]
63
+ raise ArgumentError.new(":burst_rate must be an Integer and larger than :threshold")
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/prop/limiter.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'prop/rate_limited'
2
2
  require 'prop/key'
3
3
  require 'prop/options'
4
+ require 'prop/interval_strategy'
5
+ require 'prop/leaky_bucket_strategy'
4
6
 
5
7
  module Prop
6
8
  class Limiter
@@ -54,18 +56,17 @@ module Prop
54
56
  # Returns true if the threshold for this handle has been reached, else returns false
55
57
  def throttle(handle, key = nil, options = {})
56
58
  options, cache_key = prepare(handle, key, options)
57
- counter = reader.call(cache_key).to_i
59
+ counter = @strategy.counter(cache_key, options)
58
60
 
59
61
  unless disabled?
60
- if at_threshold?(counter, options[:threshold])
62
+ if @strategy.at_threshold?(counter, options)
61
63
  unless before_throttle_callback.nil?
62
64
  before_throttle_callback.call(handle, key, options[:threshold], options[:interval])
63
65
  end
64
66
 
65
67
  true
66
68
  else
67
- increment = options.key?(:increment) ? options[:increment].to_i : 1
68
- writer.call(cache_key, counter + increment)
69
+ @strategy.increment(cache_key, options, counter)
69
70
 
70
71
  yield if block_given?
71
72
 
@@ -90,7 +91,7 @@ module Prop
90
91
  raise Prop::RateLimited.new(options.merge(:cache_key => cache_key, :handle => handle))
91
92
  end
92
93
 
93
- block_given? ? yield : reader.call(cache_key).to_i
94
+ block_given? ? yield : @strategy.counter(cache_key, options)
94
95
  end
95
96
 
96
97
  # Public: Allows to query whether the given handle/key combination is currently throttled
@@ -101,7 +102,8 @@ module Prop
101
102
  # Returns true if a call to `throttle!` with same parameters would raise, otherwise false
102
103
  def throttled?(handle, key = nil, options = {})
103
104
  options, cache_key = prepare(handle, key, options)
104
- reader.call(cache_key).to_i >= options[:threshold]
105
+ counter = @strategy.counter(cache_key, options)
106
+ @strategy.at_threshold?(counter, options)
105
107
  end
106
108
 
107
109
  # Public: Resets a specific throttle
@@ -112,7 +114,7 @@ module Prop
112
114
  # Returns nothing
113
115
  def reset(handle, key = nil, options = {})
114
116
  options, cache_key = prepare(handle, key, options)
115
- writer.call(cache_key, 0)
117
+ @strategy.reset(cache_key)
116
118
  end
117
119
 
118
120
  # Public: Counts the number of times the given handle/key combination has been hit in the current window
@@ -123,7 +125,7 @@ module Prop
123
125
  # Returns a count of hits in the current window
124
126
  def count(handle, key = nil, options = {})
125
127
  options, cache_key = prepare(handle, key, options)
126
- reader.call(cache_key).to_i
128
+ @strategy.counter(cache_key, options)
127
129
  end
128
130
  alias :query :count
129
131
 
@@ -134,10 +136,6 @@ module Prop
134
136
 
135
137
  private
136
138
 
137
- def at_threshold?(mark, threshold)
138
- mark >= threshold
139
- end
140
-
141
139
  def disabled?
142
140
  !!@disabled
143
141
  end
@@ -147,7 +145,10 @@ module Prop
147
145
 
148
146
  defaults = handles[handle]
149
147
  options = Prop::Options.build(:key => key, :params => params, :defaults => defaults)
150
- cache_key = Prop::Key.build(:key => key, :handle => handle, :interval => options[:interval])
148
+
149
+ @strategy = options.fetch(:strategy)
150
+
151
+ cache_key = @strategy.build(:key => key, :handle => handle, :interval => options[:interval])
151
152
 
152
153
  [ options, cache_key ]
153
154
  end
data/lib/prop/options.rb CHANGED
@@ -10,15 +10,22 @@ module Prop
10
10
  defaults = options.fetch(:defaults)
11
11
  result = defaults.merge(params)
12
12
 
13
- result[:key] = Prop::Key.normalize(key)
14
- result[:threshold] = result[:threshold].to_i
15
- result[:interval] = result[:interval].to_i
13
+ result[:key] = Prop::Key.normalize(key)
16
14
 
17
- raise RuntimeError.new("Invalid threshold setting") unless result[:threshold] > 0
18
- raise RuntimeError.new("Invalid interval setting") unless result[:interval] > 0
15
+ result[:strategy] = if leaky_bucket.include?(result[:strategy])
16
+ Prop::LeakyBucketStrategy
17
+ elsif result[:strategy] == nil
18
+ Prop::IntervalStrategy
19
+ else
20
+ result[:strategy] # allowing any new/unknown strategy to be used
21
+ end
19
22
 
23
+ result[:strategy].validate_options!(result)
20
24
  result
21
25
  end
22
26
 
27
+ def self.leaky_bucket
28
+ [:leaky_bucket, "leaky_bucket"]
29
+ end
23
30
  end
24
- end
31
+ end
@@ -6,9 +6,9 @@ module Prop
6
6
  handle = options.fetch(:handle)
7
7
  cache_key = options.fetch(:cache_key)
8
8
  interval = options.fetch(:interval).to_i
9
- threshold = options.fetch(:threshold).to_i
9
+ strategy = options.fetch(:strategy)
10
10
 
11
- super("#{handle} threshold of #{threshold} tries per #{interval}s exceeded for key '#{options[:key].inspect}', hash #{cache_key}")
11
+ super(strategy.threshold_reached(options))
12
12
 
13
13
  self.description = options[:description]
14
14
  self.handle = handle
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prop
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Morten Primdahl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-31 00:00:00.000000000 Z
11
+ date: 2015-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -66,35 +66,23 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- description: Gem for implementing rate limits.
69
+ description:
70
70
  email: primdahl@me.com
71
71
  executables: []
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
74
  files:
75
- - ".document"
76
- - ".gemtest"
77
- - ".gitignore"
78
- - ".travis.yml"
79
- - Gemfile
80
75
  - LICENSE
81
76
  - README.md
82
- - Rakefile
83
77
  - lib/prop.rb
78
+ - lib/prop/interval_strategy.rb
84
79
  - lib/prop/key.rb
80
+ - lib/prop/leaky_bucket_strategy.rb
85
81
  - lib/prop/limiter.rb
86
82
  - lib/prop/middleware.rb
87
83
  - lib/prop/options.rb
88
84
  - lib/prop/rate_limited.rb
89
- - prop.gemspec
90
- - test/helper.rb
91
- - test/test_key.rb
92
- - test/test_limiter.rb
93
- - test/test_middleware.rb
94
- - test/test_options.rb
95
- - test/test_prop.rb
96
- - test/test_rate_limited.rb
97
- homepage: http://github.com/zendesk/prop
85
+ homepage: https://github.com/zendesk/prop
98
86
  licenses:
99
87
  - Apache License Version 2.0
100
88
  metadata: {}
@@ -113,15 +101,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
101
  - !ruby/object:Gem::Version
114
102
  version: '0'
115
103
  requirements: []
116
- rubyforge_project: prop
117
- rubygems_version: 2.2.2
104
+ rubyforge_project:
105
+ rubygems_version: 2.4.7
118
106
  signing_key:
119
107
  specification_version: 4
120
108
  summary: Gem for implementing rate limits.
121
- test_files:
122
- - test/test_key.rb
123
- - test/test_limiter.rb
124
- - test/test_middleware.rb
125
- - test/test_options.rb
126
- - test/test_prop.rb
127
- - test/test_rate_limited.rb
109
+ test_files: []
data/.document DELETED
@@ -1,5 +0,0 @@
1
- README.rdoc
2
- lib/**/*.rb
3
- bin/*
4
- features/**/*.feature
5
- LICENSE
data/.gemtest DELETED
File without changes
data/.gitignore DELETED
@@ -1,22 +0,0 @@
1
- ## MAC OS
2
- .DS_Store
3
-
4
- ## TEXTMATE
5
- *.tmproj
6
- tmtags
7
-
8
- ## EMACS
9
- *~
10
- \#*
11
- .\#*
12
-
13
- ## VIM
14
- *.swp
15
-
16
- ## PROJECT::GENERAL
17
- coverage
18
- rdoc
19
- pkg
20
-
21
- ## PROJECT::SPECIFIC
22
- Gemfile.lock
data/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- rvm:
2
- - 2.0.0
3
- - 1.8.7
4
- - 1.9.3
5
- - jruby
6
- - ree
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- require 'bundler/gem_tasks'
2
- require 'rake/testtask'
3
-
4
- Rake::TestTask.new do |test|
5
- test.libs << 'lib' << 'test'
6
- test.pattern = 'test/**/test_*.rb'
7
- test.verbose = true
8
- end
9
-
10
- task :default do
11
- sh "bundle exec rake test"
12
- end
data/prop.gemspec DELETED
@@ -1,26 +0,0 @@
1
- Gem::Specification.new "prop", "1.1.0" do |s|
2
- s.name = 'prop'
3
- s.version = '1.1.0'
4
- s.date = '2014-12-31'
5
- s.rubyforge_project = 'prop'
6
- s.license = "Apache License Version 2.0"
7
-
8
- s.summary = "Gem for implementing rate limits."
9
- s.description = "Gem for implementing rate limits."
10
-
11
- s.authors = ["Morten Primdahl"]
12
- s.email = 'primdahl@me.com'
13
- s.homepage = 'http://github.com/zendesk/prop'
14
-
15
- ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
16
- ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
17
- s.require_paths = %w[lib]
18
-
19
- s.add_development_dependency('rake')
20
- s.add_development_dependency('bundler')
21
- s.add_development_dependency('minitest')
22
- s.add_development_dependency('mocha')
23
-
24
- s.files = `git ls-files`.split("\n")
25
- s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
26
- end
data/test/helper.rb DELETED
@@ -1,11 +0,0 @@
1
- require 'rubygems'
2
-
3
- require "minitest/autorun"
4
- require 'mocha/setup'
5
-
6
- require 'time'
7
-
8
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
- $LOAD_PATH.unshift(File.dirname(__FILE__))
10
-
11
- require 'prop'
data/test/test_key.rb DELETED
@@ -1,24 +0,0 @@
1
- require 'helper'
2
-
3
- describe Prop::Key do
4
- describe "#build" do
5
- it "return a hexdigested key" do
6
- assert_match /prop\/[a-f0-9]+/, Prop::Key.build(:handle => :hello, :key => [ "foo", 2, :bar ], :interval => 60)
7
- end
8
- end
9
-
10
- describe "#normalize" do
11
- it "turn a Fixnum into a String" do
12
- assert_equal "3", Prop::Key.normalize(3)
13
- end
14
-
15
- it "return a String" do
16
- assert_equal "S", Prop::Key.normalize("S")
17
- end
18
-
19
- it "flatten and join an Array" do
20
- assert_equal "1/B/3", Prop::Key.normalize([ 1, "B", "3" ])
21
- end
22
- end
23
- end
24
-
data/test/test_limiter.rb DELETED
@@ -1,153 +0,0 @@
1
- require 'helper'
2
-
3
- describe Prop::Limiter do
4
- before do
5
- @store = {}
6
-
7
- Prop::Limiter.read { |key| @store[key] }
8
- Prop::Limiter.write { |key, value| @store[key] = value }
9
- Prop::Limiter.configure(:something, :threshold => 10, :interval => 10)
10
-
11
- @start = Time.now
12
- Time.stubs(:now).returns(@start)
13
-
14
- Prop.reset(:something)
15
- end
16
-
17
- describe "#throttle" do
18
- describe "when disabled" do
19
- before { Prop::Limiter.stubs(:disabled?).returns(true) }
20
-
21
- it "returns nil" do
22
- assert_nil Prop.throttle(:something)
23
- end
24
- end
25
-
26
- describe "when not disabled" do
27
- before { Prop::Limiter.stubs(:disabled?).returns(false) }
28
-
29
- describe "and the threshold has been reached" do
30
- before { Prop::Limiter.stubs(:at_threshold?).returns(true) }
31
-
32
- it "returns true" do
33
- assert Prop.throttle(:something)
34
- end
35
-
36
- it "does not increment the throttle count" do
37
- Prop.throttle(:something)
38
-
39
- assert_equal 0, Prop.count(:something)
40
- end
41
-
42
- describe "when given a block" do
43
- before { @test_block_executed = false }
44
-
45
- it "does not execute the block" do
46
- Prop.throttle(:something) { @test_block_executed = true }
47
-
48
- refute @test_block_executed
49
- end
50
- end
51
-
52
- describe "when a before_throttle callback has been specified" do
53
- before do
54
- Prop.before_throttle do |handle, key, threshold, interval|
55
- @handle = handle
56
- @key = key
57
- @threshold = threshold
58
- @interval = interval
59
- end
60
-
61
- Prop.throttle(:something, [:extra])
62
- end
63
-
64
- it "invokes callback with expected parameters" do
65
- assert_equal @handle, :something
66
- assert_equal @key, [:extra]
67
- assert_equal @threshold, 10
68
- assert_equal @interval, 10
69
- end
70
- end
71
- end
72
-
73
- describe "and the threshold has not been reached" do
74
- before { Prop::Limiter.stubs(:at_threshold?).returns(false) }
75
-
76
- it "returns false" do
77
- refute Prop.throttle(:something)
78
- end
79
-
80
- it "increments the throttle count by one" do
81
- Prop.throttle(:something)
82
-
83
- assert_equal 1, Prop.count(:something)
84
- end
85
-
86
- it "increments the throttle count by the specified number when provided" do
87
- Prop.throttle(:something, nil, :increment => 5)
88
-
89
- assert_equal 5, Prop.count(:something)
90
- end
91
-
92
- describe "when given a block" do
93
- before { @test_block_executed = false }
94
-
95
- it "executes the block" do
96
- Prop.throttle(:something) { @test_block_executed = true }
97
-
98
- assert @test_block_executed
99
- end
100
- end
101
- end
102
- end
103
- end
104
-
105
- describe "#throttle!" do
106
- it "throttles the given handle/key combination" do
107
- Prop::Limiter.expects(:throttle).with(
108
- :something,
109
- :key,
110
- {
111
- :threshold => 10,
112
- :interval => 10,
113
- :key => 'key',
114
- :options => true
115
- }
116
- )
117
-
118
- Prop.throttle!(:something, :key, :options => true)
119
- end
120
-
121
- describe "when the threshold has been reached" do
122
- before { Prop::Limiter.stubs(:throttle).returns(true) }
123
-
124
- it "raises a rate-limited exception" do
125
- assert_raises(Prop::RateLimited) { Prop.throttle!(:something) }
126
- end
127
-
128
- describe "when given a block" do
129
- before { @test_block_executed = false }
130
-
131
- it "does not executes the block" do
132
- begin
133
- Prop.throttle!(:something) { @test_block_executed = true }
134
- rescue Prop::RateLimited
135
- refute @test_block_executed
136
- end
137
- end
138
- end
139
- end
140
-
141
- describe "when the threshold has not been reached" do
142
- it "returns the counter value" do
143
- assert_equal Prop.count(:something) + 1, Prop.throttle!(:something)
144
- end
145
-
146
- describe "when given a block" do
147
- it "returns the return value of the block" do
148
- assert_equal 'block_value', Prop.throttle!(:something) { 'block_value' }
149
- end
150
- end
151
- end
152
- end
153
- end
@@ -1,46 +0,0 @@
1
- require 'helper'
2
-
3
- require 'prop/middleware'
4
- require 'prop/rate_limited'
5
-
6
-
7
- describe Prop::Middleware do
8
- before do
9
- @app = stub()
10
- @env = {}
11
- @middleware = Prop::Middleware.new(@app)
12
- end
13
-
14
- describe "when the app call completes" do
15
- before do
16
- @app.expects(:call).with(@env).returns("response")
17
- end
18
-
19
- it "return the response" do
20
- assert_equal "response", @middleware.call(@env)
21
- end
22
- end
23
-
24
- describe "when the app call results in a raised throttle" do
25
- before do
26
- @app.expects(:call).with(@env).raises(Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!"))
27
- end
28
-
29
- it "return the rate limited message" do
30
- response = @middleware.call(@env)
31
-
32
- assert_equal 429, response[0]
33
- assert_equal ["Boom!"], response[2]
34
- end
35
-
36
- describe "with a custom error handler" do
37
- before do
38
- @middleware = Prop::Middleware.new(@app, :error_handler => Proc.new { |env, error| "Oops" })
39
- end
40
-
41
- it "allow setting a custom error handler" do
42
- assert_equal "Oops", @middleware.call(@env)
43
- end
44
- end
45
- end
46
- end
data/test/test_options.rb DELETED
@@ -1,44 +0,0 @@
1
- require 'helper'
2
-
3
- describe Prop::Options do
4
- describe "#build" do
5
- before do
6
- @args = { :key => "hello", :params => { :foo => "bif" }, :defaults => { :foo => "bar", :baz => "moo", :threshold => 10, :interval => 5 }}
7
- end
8
-
9
- describe "when given valid input" do
10
- before do
11
- @options = Prop::Options.build(@args)
12
- end
13
-
14
- it "support defaults" do
15
- assert_equal "moo", @options[:baz]
16
- end
17
-
18
- it "override defaults" do
19
- assert_equal "bif", @options[:foo]
20
- end
21
- end
22
-
23
- describe "when given invalid input" do
24
- it "raise when not given an interval" do
25
- @args[:defaults].delete(:interval)
26
- assert_raises(RuntimeError) { Prop::Options.build(@args) }
27
- end
28
-
29
- it "raise when not given a threshold" do
30
- @args[:defaults].delete(:threshold)
31
- assert_raises(RuntimeError) { Prop::Options.build(@args) }
32
- end
33
-
34
- it "raise when not given a key" do
35
- @args.delete(:key)
36
- begin
37
- Prop::Options.build(@args)
38
- fail "it puke when not given a valid key"
39
- rescue
40
- end
41
- end
42
- end
43
- end
44
- end
data/test/test_prop.rb DELETED
@@ -1,205 +0,0 @@
1
- require 'helper'
2
-
3
- # Integration level tests
4
- describe Prop do
5
- before do
6
- store = {}
7
- Prop.read { |key| store[key] }
8
- Prop.write { |key, value| store[key] = value }
9
-
10
- @start = Time.now
11
- Time.stubs(:now).returns(@start)
12
- end
13
-
14
- describe "#defaults" do
15
- it "raise errors on invalid configuation" do
16
- assert_raises(RuntimeError) do
17
- Prop.configure :hello_there, :threshold => 20, :interval => 'hello'
18
- end
19
-
20
- assert_raises(RuntimeError) do
21
- Prop.configure :hello_there, :threshold => 'wibble', :interval => 100
22
- end
23
- end
24
-
25
- it "result in a default handle" do
26
- Prop.configure :hello_there, :threshold => 4, :interval => 10
27
- 4.times do |i|
28
- assert_equal (i + 1), Prop.throttle!(:hello_there, 'some key')
29
- end
30
-
31
- assert_raises(Prop::RateLimited) { Prop.throttle!(:hello_there, 'some key') }
32
- assert_equal 5, Prop.throttle!(:hello_there, 'some key', :threshold => 20)
33
- end
34
-
35
- it "create a handle accepts various cache key types" do
36
- Prop.configure :hello_there, :threshold => 4, :interval => 10
37
- assert_equal 1, Prop.throttle!(:hello_there, 5)
38
- assert_equal 2, Prop.throttle!(:hello_there, 5)
39
- assert_equal 1, Prop.throttle!(:hello_there, '6')
40
- assert_equal 2, Prop.throttle!(:hello_there, '6')
41
- assert_equal 1, Prop.throttle!(:hello_there, [ 5, '6' ])
42
- assert_equal 2, Prop.throttle!(:hello_there, [ 5, '6' ])
43
- end
44
- end
45
-
46
- describe "#disable" do
47
- before do
48
- Prop.configure :hello, :threshold => 10, :interval => 10
49
- end
50
-
51
- it "not increase the throttle" do
52
- assert_equal 1, Prop.throttle!(:hello)
53
- assert_equal 2, Prop.throttle!(:hello)
54
- Prop.disabled do
55
- assert_equal 2, Prop.throttle!(:hello)
56
- assert_equal 2, Prop.throttle!(:hello)
57
- assert Prop::Limiter.send(:disabled?)
58
- end
59
- assert !Prop::Limiter.send(:disabled?)
60
- assert_equal 3, Prop.throttle!(:hello)
61
- end
62
- end
63
-
64
- describe "#reset" do
65
- before do
66
- Prop.configure :hello, :threshold => 10, :interval => 10
67
-
68
- 5.times do |i|
69
- assert_equal (i + 1), Prop.throttle!(:hello)
70
- end
71
- end
72
-
73
- it "set the correct counter to 0" do
74
- Prop.throttle!(:hello, 'wibble')
75
- Prop.throttle!(:hello, 'wibble')
76
-
77
- Prop.reset(:hello)
78
- assert_equal 1, Prop.throttle!(:hello)
79
-
80
- assert_equal 3, Prop.throttle!(:hello, 'wibble')
81
- Prop.reset(:hello, 'wibble')
82
- assert_equal 1, Prop.throttle!(:hello, 'wibble')
83
- end
84
- end
85
-
86
- describe "#throttled?" do
87
- it "return true once the threshold has been reached" do
88
- Prop.configure(:hello, :threshold => 2, :interval => 10)
89
- Prop.throttle!(:hello)
90
- assert !Prop.throttled?(:hello)
91
- Prop.throttle!(:hello)
92
- assert Prop.throttled?(:hello)
93
- end
94
- end
95
-
96
- describe "#count" do
97
- before do
98
- Prop.configure(:hello, :threshold => 20, :interval => 20)
99
- Prop.throttle!(:hello)
100
- Prop.throttle!(:hello)
101
- end
102
-
103
- it "be aliased by #count" do
104
- assert_equal Prop.count(:hello), 2
105
- end
106
-
107
- it "return the number of hits on a throttle" do
108
- assert_equal Prop.query(:hello), 2
109
- end
110
- end
111
-
112
- describe "#throttle!" do
113
- it "increment counter correctly" do
114
- Prop.configure(:hello, :threshold => 20, :interval => 20)
115
- 3.times do |i|
116
- assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
117
- end
118
- end
119
-
120
- it "reset counter when time window is passed" do
121
- Prop.configure(:hello, :threshold => 20, :interval => 20)
122
- 3.times do |i|
123
- assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
124
- end
125
-
126
- Time.stubs(:now).returns(@start + 20)
127
-
128
- 3.times do |i|
129
- assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
130
- end
131
- end
132
-
133
- it "not increment the counter beyond the threshold" do
134
- Prop.configure(:hello, :threshold => 5, :interval => 1)
135
- 10.times do |i|
136
- Prop.throttle!(:hello) rescue nil
137
- end
138
-
139
- assert_equal 5, Prop.query(:hello)
140
- end
141
-
142
- it "support custom increments" do
143
- Prop.configure(:hello, :threshold => 100, :interval => 10)
144
-
145
- Prop.throttle!(:hello)
146
- Prop.throttle!(:hello)
147
-
148
- assert_equal 2, Prop.query(:hello)
149
-
150
- Prop.throttle!(:hello, nil, :increment => 48)
151
-
152
- assert_equal 50, Prop.query(:hello)
153
- end
154
-
155
- it "raise Prop::RateLimited when the threshold is exceeded" do
156
- Prop.configure(:hello, :threshold => 5, :interval => 10, :description => "Boom!")
157
-
158
- 5.times do |i|
159
- Prop.throttle!(:hello, nil)
160
- end
161
- assert_raises(Prop::RateLimited) do
162
- Prop.throttle!(:hello, nil)
163
- end
164
-
165
- begin
166
- Prop.throttle!(:hello, nil)
167
- fail
168
- rescue Prop::RateLimited => e
169
- assert_equal :hello, e.handle
170
- assert_match "5 tries per 10s exceeded for key", e.message
171
- assert_equal "Boom!", e.description
172
- assert e.retry_after
173
- end
174
- end
175
-
176
- it "raise a RuntimeError when a handle has not been configured" do
177
- assert_raises(RuntimeError) do
178
- Prop.throttle!(:no_such_handle, nil, :threshold => 5, :interval => 10)
179
- end
180
- end
181
- end
182
-
183
- describe 'different handles with the same interval' do
184
- before do
185
- Prop.configure(:api_requests, :threshold => 100, :interval => 30)
186
- Prop.configure(:login_attempts, :threshold => 10, :interval => 30)
187
- end
188
-
189
- it 'be counted separately' do
190
- user_id = 42
191
- Prop.throttle!(:api_requests, user_id)
192
- assert_equal(1, Prop.count(:api_requests, user_id))
193
- assert_equal(0, Prop.count(:login_attempts, user_id))
194
- end
195
- end
196
-
197
- describe "#configurations" do
198
- it "returns the configuration" do
199
- Prop.configure(:something, :threshold => 100, :interval => 30)
200
- config = Prop.configurations[:something]
201
- assert_equal 100, config[:threshold]
202
- assert_equal 30, config[:interval]
203
- end
204
- end
205
- end
@@ -1,39 +0,0 @@
1
- require 'helper'
2
-
3
- describe Prop::RateLimited do
4
- before do
5
- time = Time.at(1333685680)
6
- Time.stubs(:now).returns(time)
7
-
8
- Prop.configure :foo, :threshold => 10, :interval => 60, :category => :api
9
-
10
- @error = Prop::RateLimited.new(
11
- :handle => :foo,
12
- :threshold => 10,
13
- :interval => 60,
14
- :cache_key => "wibble",
15
- :description => "Boom!"
16
- )
17
- end
18
-
19
- describe "#initialize" do
20
- it "returns an error instance" do
21
- assert @error.is_a?(StandardError)
22
- assert @error.is_a?(Prop::RateLimited)
23
-
24
- assert_equal :foo, @error.handle
25
- assert_equal "wibble", @error.cache_key
26
- assert_equal "Boom!", @error.description
27
- assert_equal "foo threshold of 10 tries per 60s exceeded for key 'nil', hash wibble", @error.message
28
- assert_equal 20, @error.retry_after
29
- end
30
- end
31
-
32
- describe "#config" do
33
- it "returns the original configuration" do
34
- assert_equal 10, @error.config[:threshold]
35
- assert_equal 60, @error.config[:interval]
36
- assert_equal :api, @error.config[:category]
37
- end
38
- end
39
- end