rollie 0.0.1 → 0.1.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: 1a4104ef316b1bf29321667fa9ea4e44dd642f4e
4
- data.tar.gz: c695f97168e7ea2d7f47fe0a118bef23d8f5e24e
3
+ metadata.gz: e7dca59b24d14a1f68bc1e52eb8de74ca2fdb10c
4
+ data.tar.gz: c76e6ef7af25f4b7237c51ab794d2bfbe00eeaf5
5
5
  SHA512:
6
- metadata.gz: 7f9da971ea951921729261a1920e640dfa9b6d1b8fc4580bb87454e6d7657a90583a52b17005a38ee61fdbfaa0dd7d38501e91cea39e9feefd7bbaf73e5c3068
7
- data.tar.gz: a3eb75347541a6092ec9a8bfc085dbb5e0368560abf6dd143892d625b6be6f51e3b7693f208b08a7dcc2a287086d4922ad4d2d7afc6ba90d70e5f1924d0a31fe
6
+ metadata.gz: de6be4c8f9b384cae1755668b217835e92255fc323abbbdf6a257bdc8237942e06c6e6c0714ed2f4a9a33b7598c4d90030f2081fbe66d05d2968dcbad0ef91ae
7
+ data.tar.gz: 379cb65364722e9182b736e61430d6dd8c50daaa45eee19538040f35b1501195b5c13c0c6c405e70df787399f9eb25ab0e300cd782b97d24d40a9289a92d1b0e
@@ -0,0 +1,13 @@
1
+ *.gem
2
+ *.rbc
3
+ Gemfile.lock
4
+ .DS_Store
5
+ dump.rdb
6
+ doc/
7
+ rdoc/
8
+ /spec/reports/
9
+ /.bundle
10
+ tmp/
11
+ .ruby-gemset
12
+ .ruby-version
13
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ services:
3
+ - redis-server
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ * fix for intervals < 1000 ms
6
+ * return actual count (not capped at limit)
7
+ * option to count blocked executions against total count (defaults to false)
8
+ * add ability to fetch current count
9
+
10
+ ## 0.0.1
11
+
12
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+ gemspec :name => "rollie"
3
+
4
+ gem "rake"
5
+
6
+ group :test do
7
+ gem "rspec", "~> 3.4"
8
+ end
data/README.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # Rollie
2
2
 
3
- Rollie is a generic rate limiter backed by Redis for efficient limiting using sliding windows.
3
+ [![Build Status](https://travis-ci.org/zldavis/rollie.svg?branch=master)](https://travis-ci.org/zldavis/rollie)
4
4
 
5
- [![Build Status](https://travis-ci.org/zldavis/rollie.svg?branch=master)](https://travis-ci.org/zldavis/rollie)
5
+ Rollie is a multi-purpose, fast, redis backed rate limiter that can be used to limit requests to external APIs, in Rack
6
+ middleware, etc. Rollie uses a dedicated redis connection pool implemented using `connection_pool` for more efficient
7
+ redis connection management.
8
+
9
+ The key implementation detail is that Rollie utilizes a rolling window to bucket invocations in. Meaning, if you set
10
+ a limit of 100 per 30 seconds, Rollie will start the clock in instant it is first executed with a given key.
11
+
12
+ For example, first execution:
13
+ ```
14
+ rollie = Rollie::RateLimiter.new("api", limit: 10, interval: 30000)
15
+ rollie.within_limit do
16
+ puts Time.now
17
+ end
18
+ # => 2016-12-03 08:31:23.873
19
+ ```
20
+
21
+ This doesn't mean the count is reset back to 0 at `2016-12-03 08:31:53.873`. Its a continuous rolling count, the count
22
+ is checked with every invocation over the last 30 seconds.
23
+
24
+ If you invoke this rate 9 times at `2016-12-03 08:31:53.500`, you will only be able to make one more call until `2016-12-03 08:32:23.500`.
25
+
26
+ ## Install
27
+
28
+ ```
29
+ gem install rollie
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Rollie is simple to use and has only one method, `within_limit`. `within_limit` expects a block and that block will be
35
+ executed only if you are within the limit.
36
+
37
+ Initialize Rollie with a key used to uniquely identify what you are limiting. Use the options to set the limit and
38
+ interval in milliseconds.
39
+ ```
40
+ # limit 30 requests per second.
41
+ twitter_rate = Rollie::RateLimiter.new("twitter_requests", limit: 30, interval: 1000)
42
+ status = twitter_rate.within_limit do
43
+ twitter.do_something
44
+ end
45
+ ```
46
+
47
+ The status will tell you the current state. You can also see the current count and how long until the bucket resets.
48
+ ```
49
+ status.exceeded?
50
+ # => false
51
+ status.count
52
+ # => 1
53
+ status.time_remaining
54
+ # => 987 # milliseconds
55
+ ```
56
+
57
+ Once exceeded:
58
+ ```
59
+ status.exceeded?
60
+ # => true
61
+ status.count
62
+ # => 30
63
+ status.time_remaining
64
+ # => 461 # milliseconds
65
+ ```
66
+
67
+ You can also use a namespace if you want to track multiple entities, for example users.
68
+ ```
69
+ Rollie::RateLimiter.new(user_id, namespace: "user_messages", limit: 100, interval: 30000)
70
+ ```
71
+
72
+ ### Counting blocked actions
73
+
74
+ By default, blocked actions are not counted against the callee. This allows for the block to be executed within the
75
+ rate even when there is a continuous flood of action. If you wish to change this behaviour, for example to require the callee to back off before being allowed to excute again, set this option to true.
76
+
77
+ ```
78
+ request_rate = Rollie::RateLimiter.new(ip, namespace: "ip", limit: 30, interval: 1000, count_blocked: true)
79
+ ```
80
+
81
+ ## Configuration
82
+
83
+ By default Rollie will try to connect to redis using `ENV["REDIS_URL"]` if set or fallback to localhost:6379. You can
84
+ set an alternate redis configuration:
85
+ ```
86
+ Rollie.redis = {
87
+ url: CONFIG[:redis_url],
88
+ pool_size: 5,
89
+ pool_timeout: 1,
90
+ driver: :hiredis
91
+ }
92
+ ```
93
+
94
+ If using rails, create an initializer `config/initializers/rollie.rb` with these settings.
@@ -0,0 +1,28 @@
1
+ require "rake"
2
+ require "rdoc"
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[lib]))
5
+ require "rollie/version"
6
+
7
+ def name
8
+ "rollie"
9
+ end
10
+
11
+ def version
12
+ Rollie::VERSION
13
+ end
14
+
15
+ begin
16
+ require "rspec/core/rake_task"
17
+ RSpec::Core::RakeTask.new(:spec)
18
+ rescue LoadError; end
19
+
20
+ require "rdoc/task"
21
+ Rake::RDocTask.new do |rdoc|
22
+ rdoc.rdoc_dir = "rdoc"
23
+ rdoc.title = "#{name} #{version}"
24
+ rdoc.rdoc_files.include("README*")
25
+ rdoc.rdoc_files.include("lib/**/*.rb")
26
+ end
27
+
28
+ task :default => :spec
@@ -5,6 +5,7 @@ require "rollie/version"
5
5
 
6
6
  module Rollie
7
7
  class << self
8
+
8
9
  def redis
9
10
  raise ArgumentError, "requires a block" unless block_given?
10
11
  redis_pool.with do |conn|
@@ -12,17 +13,25 @@ module Rollie
12
13
  end
13
14
  end
14
15
 
15
- def redis=(hash)
16
- @redis_pool = if hash.is_a?(ConnectionPool)
17
- hash
16
+ # Configures the redis connection pool. Options can be a hash of redis connection pool options or a pre-configured
17
+ # ConnectionPool instance.
18
+ #
19
+ # @option options [String] :url The redis connection URL
20
+ # @option options [String] :driver The redis driver
21
+ # @option options [Integer] :pool_size Size of the connection pool
22
+ # @option options [Integer] :pool_timeout Pool timeout in seconds
23
+ # @option options [String] :namespace Optional namespace for redis keys
24
+ def redis=(options)
25
+ @redis_pool = if options.is_a?(ConnectionPool)
26
+ options
18
27
  else
19
- Rollie::RedisPool.create(hash)
28
+ Rollie::RedisPool.create(options)
20
29
  end
21
30
  end
22
31
 
23
32
  def redis_pool
24
33
  @redis_pool ||= Rollie::RedisPool.create
25
34
  end
26
- end
27
35
 
36
+ end
28
37
  end
@@ -2,12 +2,25 @@ module Rollie
2
2
 
3
3
  class RateLimiter
4
4
 
5
+ # Create a new RateLimiter instance.
6
+ #
7
+ # @param [String] key A unique name to track this rate limit against.
8
+ # @option options [Integer] :limit The limit
9
+ # @option options [Integer] :interval The interval in milliseconds for this rate limit
10
+ # @option options [String] :namespace Optional namespace for this rate limit
11
+ # @option options [Boolean] :count_blocked if true, all calls to within_limit will count towards total execution count, even if blocked.
12
+ #
13
+ # @return [RateLimiter] RateLimiter instance
5
14
  def initialize(key, options = {})
6
15
  @key = "#{options[:namespace]}#{key}"
7
16
  @limit = options[:limit] || 25
8
17
  @interval = (options[:interval] || 1000) * 1000
18
+ @count_blocked = options.key?(:count_blocked) ? options[:count_blocked] : false
9
19
  end
10
20
 
21
+ # Executes a block as long as the current rate is within the limit.
22
+ #
23
+ # @return [Status] The current status for this RateLimiter.
11
24
  def within_limit
12
25
  raise ArgumentError, "requires a block" unless block_given?
13
26
 
@@ -20,6 +33,14 @@ module Rollie
20
33
  end
21
34
  end
22
35
 
36
+ # @return [Integer] The current count of this RateLimiter.
37
+ def count
38
+ Rollie.redis do |conn|
39
+ range = conn.zrange(@key, 0, -1)
40
+ range.length
41
+ end
42
+ end
43
+
23
44
  private
24
45
 
25
46
  def inc(conn)
@@ -29,12 +50,18 @@ module Rollie
29
50
  conn.zremrangebyscore(@key, 0, old)
30
51
  conn.zadd(@key, time, time)
31
52
  conn.zrange(@key, 0, -1)
32
- conn.expire(@key, (@interval / 1000000).ceil)
53
+ conn.expire(@key, (@interval / 1000000.0).ceil)
33
54
  end[2]
34
55
 
35
56
  exceeded = range.length > @limit
57
+ current_count = range.length
36
58
  time_remaining = range.first.to_i - time + @interval
37
- current_count = exceeded ? @limit : range.length
59
+
60
+ if exceeded && !@count_blocked
61
+ conn.zremrangebyscore(@key, time, time)
62
+ current_count -= 1
63
+ end
64
+
38
65
  Rollie::Status.new((time_remaining / 1000).floor, current_count, exceeded)
39
66
  end
40
67
 
@@ -7,7 +7,6 @@ module Rollie
7
7
  class << self
8
8
 
9
9
  def create(options={})
10
- puts "initialized with options: #{options}"
11
10
  pool_size = options[:pool_size] || 5
12
11
  pool_timeout = options[:pool_timeout] || 1
13
12
 
@@ -1,3 +1,3 @@
1
1
  module Rollie
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "0.1.0".freeze
3
3
  end
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+ require "rollie/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "rollie"
6
+ s.version = Rollie::VERSION
7
+ s.license = "MIT"
8
+
9
+ s.summary = "Generic rate limiter backed by Redis for efficient limiting using sliding windows."
10
+ s.description = s.summary
11
+
12
+ s.authors = ["Zach Davis"]
13
+ s.email = "zldavis@gmail.com"
14
+ s.homepage = "https://github.com/zldavis/rollie"
15
+
16
+ s.files = `git ls-files -z`.split("\x0")
17
+ s.test_files = `git ls-files -- test/*`.split("\n")
18
+
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "redis", "~> 3.2", ">= 3.2.1"
22
+ s.add_dependency "redis-namespace", "~> 1.5", ">= 1.5.2"
23
+ s.add_dependency "connection_pool", "~> 2.2", ">= 2.2.0"
24
+ end
@@ -0,0 +1,83 @@
1
+ require "spec_helper"
2
+
3
+ module Rollie
4
+ describe RateLimiter do
5
+
6
+ before do
7
+ @r = RateLimiter.new(SecureRandom.hex(8), count_blocked: true)
8
+ end
9
+
10
+ describe :within_limit do
11
+
12
+ it "should require a block" do
13
+ expect{ @r.within_limit }.to raise_error(ArgumentError)
14
+ end
15
+
16
+ it "should return status" do
17
+ status = @r.within_limit do; end
18
+ expect(status.count).to eq(1)
19
+ expect(status.exceeded?).to be(false)
20
+ expect(status.time_remaining).to eq(1000)
21
+ end
22
+
23
+ it "should execute block only while within limit" do
24
+ count = 0
25
+ status = nil
26
+ 30.times do
27
+ status = @r.within_limit do
28
+ count += 1
29
+ end
30
+ end
31
+ expect(count).to eq(25)
32
+ expect(status.count).to eq(30)
33
+ expect(status.exceeded?).to be(true)
34
+ end
35
+
36
+ it "should block all actions within the window" do
37
+ @r = RateLimiter.new(SecureRandom.hex(8), limit: 10, interval: 100, count_blocked: true)
38
+ count = 0
39
+ 30.times do
40
+ @r.within_limit do
41
+ count += 1
42
+ end
43
+ sleep 0.004
44
+ end
45
+ expect(count).to eq(10)
46
+ end
47
+
48
+ it "should allow blocked actions not to be counted" do
49
+ @r = RateLimiter.new(SecureRandom.hex(8), limit: 10, interval: 100, count_blocked: false)
50
+ count = 0
51
+ 30.times do
52
+ @r.within_limit do
53
+ count += 1
54
+ end
55
+ sleep 0.004
56
+ end
57
+ expect(count).to eq(20)
58
+ end
59
+
60
+ end
61
+
62
+ describe :count do
63
+
64
+ it "should return the current count" do
65
+ 30.times do
66
+ @r.within_limit do; sleep 0.001; end
67
+ end
68
+
69
+ expect(@r.count).to eq(30)
70
+
71
+ @r = RateLimiter.new(SecureRandom.hex(8), limit: 10, count_blocked: false)
72
+
73
+ 30.times do
74
+ @r.within_limit do; sleep 0.001; end
75
+ end
76
+
77
+ expect(@r.count).to eq(10)
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ describe Rollie do
4
+
5
+ describe :redis do
6
+
7
+ it "should require a block" do
8
+ expect{ Rollie.redis }.to raise_error(ArgumentError)
9
+ end
10
+
11
+ it "should return a Redis instance from the pool" do
12
+ Rollie.redis do |conn|
13
+ expect(conn.class).to eq(Redis::Namespace)
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ describe :redis= do
20
+
21
+ it "should allow hash options to initialize connection pool" do
22
+ options = {url: "redis://foo"}
23
+ pool = ConnectionPool.new do; end
24
+ expect(Rollie::RedisPool).to receive(:create).with(options).and_return(pool)
25
+ Rollie.redis = options
26
+ end
27
+
28
+ it "should allow a connection pool" do
29
+ pool = ConnectionPool.new do; end
30
+ Rollie.redis = pool
31
+ expect(Rollie.redis_pool).to eq(pool)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,106 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # The `.rspec` file also contains a few flags that are not defaults but that
16
+ # users commonly want.
17
+ #
18
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19
+
20
+ require "rollie"
21
+
22
+ RSpec.configure do |config|
23
+ # rspec-expectations config goes here. You can use an alternate
24
+ # assertion/expectation library such as wrong or the stdlib/minitest
25
+ # assertions if you prefer.
26
+ config.expect_with :rspec do |expectations|
27
+ # This option will default to `true` in RSpec 4. It makes the `description`
28
+ # and `failure_message` of custom matchers include text for helper methods
29
+ # defined using `chain`, e.g.:
30
+ # be_bigger_than(2).and_smaller_than(4).description
31
+ # # => "be bigger than 2 and smaller than 4"
32
+ # ...rather than:
33
+ # # => "be bigger than 2"
34
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
35
+ end
36
+
37
+ # rspec-mocks config goes here. You can use an alternate test double
38
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
39
+ config.mock_with :rspec do |mocks|
40
+ # Prevents you from mocking or stubbing a method that does not exist on
41
+ # a real object. This is generally recommended, and will default to
42
+ # `true` in RSpec 4.
43
+ mocks.verify_partial_doubles = true
44
+ end
45
+
46
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
47
+ # have no way to turn it off -- the option exists only for backwards
48
+ # compatibility in RSpec 3). It causes shared context metadata to be
49
+ # inherited by the metadata hash of host groups and examples, rather than
50
+ # triggering implicit auto-inclusion in groups with matching metadata.
51
+ config.shared_context_metadata_behavior = :apply_to_host_groups
52
+
53
+ # The settings below are suggested to provide a good initial experience
54
+ # with RSpec, but feel free to customize to your heart's content.
55
+ =begin
56
+ # This allows you to limit a spec run to individual examples or groups
57
+ # you care about by tagging them with `:focus` metadata. When nothing
58
+ # is tagged with `:focus`, all examples get run. RSpec also provides
59
+ # aliases for `it`, `describe`, and `context` that include `:focus`
60
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
61
+ config.filter_run_when_matching :focus
62
+
63
+ # Allows RSpec to persist some state between runs in order to support
64
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
65
+ # you configure your source control system to ignore this file.
66
+ config.example_status_persistence_file_path = "spec/examples.txt"
67
+
68
+ # Limits the available syntax to the non-monkey patched syntax that is
69
+ # recommended. For more details, see:
70
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
71
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
72
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
73
+ config.disable_monkey_patching!
74
+
75
+ # This setting enables warnings. It's recommended, but in some cases may
76
+ # be too noisy due to issues in dependencies.
77
+ config.warnings = true
78
+
79
+ # Many RSpec users commonly either run the entire suite or an individual
80
+ # file, and it's useful to allow more verbose output when running an
81
+ # individual spec file.
82
+ if config.files_to_run.one?
83
+ # Use the documentation formatter for detailed output,
84
+ # unless a formatter has already been configured
85
+ # (e.g. via a command-line flag).
86
+ config.default_formatter = 'doc'
87
+ end
88
+
89
+ # Print the 10 slowest examples and example groups at the
90
+ # end of the spec run, to help surface which specs are running
91
+ # particularly slow.
92
+ config.profile_examples = 10
93
+
94
+ # Run specs in random order to surface order dependencies. If you find an
95
+ # order dependency and want to debug it, you can fix the order by providing
96
+ # the seed, which is printed after each run.
97
+ # --seed 1234
98
+ config.order = :random
99
+
100
+ # Seed global randomization in this process using the `--seed` CLI option.
101
+ # Setting this allows you to use `--seed` to deterministically reproduce
102
+ # test failures related to randomization by passing the same `--seed` value
103
+ # as the one that triggered the failure.
104
+ Kernel.srand config.seed
105
+ =end
106
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Davis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-03 00:00:00.000000000 Z
11
+ date: 2016-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -75,17 +75,25 @@ description: Generic rate limiter backed by Redis for efficient limiting using s
75
75
  email: zldavis@gmail.com
76
76
  executables: []
77
77
  extensions: []
78
- extra_rdoc_files:
79
- - README.md
80
- - LICENSE
78
+ extra_rdoc_files: []
81
79
  files:
80
+ - ".gitignore"
81
+ - ".rspec"
82
+ - ".travis.yml"
83
+ - CHANGELOG.md
84
+ - Gemfile
82
85
  - LICENSE
83
86
  - README.md
87
+ - Rakefile
84
88
  - lib/rollie.rb
85
89
  - lib/rollie/rate_limiter.rb
86
90
  - lib/rollie/redis_pool.rb
87
91
  - lib/rollie/status.rb
88
92
  - lib/rollie/version.rb
93
+ - rollie.gemspec
94
+ - spec/rollie/rate_limiter_spec.rb
95
+ - spec/rollie_spec.rb
96
+ - spec/spec_helper.rb
89
97
  homepage: https://github.com/zldavis/rollie
90
98
  licenses:
91
99
  - MIT