ruby_rolling_rate_limiter 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7a4d27689f6f0095e817f2ddc39e345e7a0fdb09
4
+ data.tar.gz: 974182baa28ce3742660d2cc5996c40fbe19c749
5
+ SHA512:
6
+ metadata.gz: 81fb38c9de11936c994ab72b05c41b9b6679250327b9e9e17499af7b06c4f3d78d5fb69dd54894139f510e73cada103a3374b0eb1800471b3e0d214f0fe3d7ca
7
+ data.tar.gz: 1cb014c36c572ce9022021c37adddb1d9500f85c0f0f76bf0e806ec07ca1a2e1bb7501a99859672ad975430b70336c100f417853877dc484e6b90d7f53290eb3
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /builds/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ before_install: gem install bundler -v 1.11.2
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at karl@inventionlabs.com.au. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ruby_rolling_rate_limiter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Karl Kloppenborg
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,76 @@
1
+ # RubyRollingRateLimiter
2
+
3
+ Often Redis is used for rate limiting purposes.
4
+ Usually the rate limit packages available count how many times something happens on a certain second or a certain minute. When the clock ticks to the next minute, rate limit counter is reset back to the zero.
5
+
6
+ This might be problematic if you are looking to limit rates where hits per integration time window is very low.
7
+ If you are looking to limit to the five hits per minute, in one time window you get just one hit and six in another, even though the average over two minutes is 3.5.
8
+
9
+ This package allows you to implement a correct rolling window of threshold that's backed by ATOMIC storage in Redis meaning you can use this implementation across multiple machines and processes.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'ruby_rolling_rate_limiter'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install ruby_rolling_rate_limiter
26
+
27
+ ## Usage
28
+
29
+ To use the rate limiting service use the following example:
30
+
31
+ ```ruby
32
+ require 'ruby_rolling_rate_limiter'
33
+
34
+ $redis = Redis.new
35
+ #
36
+ # Namespace is to group rate limiters, give it any name you want.
37
+ # 60 = rolling_window in seconds.
38
+ # 25 is the max calls in that window.
39
+ # Optional arguments include min_distance (defaults to one second)
40
+ # and also redis object can be passed.
41
+ #
42
+ rate_limiter = RubyRollingRateLimiter.new("MyAwesomeRateLimiter", 60, 25)
43
+
44
+ # This is a unique identifier of the rate limit. It can be used to specify a rate limit per user for example. Give it any unique name.
45
+ rate_limiter.set_call_identifier("karl@karlos.com")
46
+
47
+ #Default call_size is 1 you can change this by using
48
+ # rate_limiter.can_call_proceed?(call_size)
49
+ #
50
+ if rate_limiter.can_call_proceed?
51
+ # Process the task
52
+
53
+ else
54
+ # Get the error
55
+ puts rate_limiter.current_error
56
+ end
57
+ ```
58
+
59
+ Tada!
60
+
61
+ --Karl.
62
+ ## Development
63
+
64
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
65
+
66
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
67
+
68
+ ## Contributing
69
+
70
+ Bug reports and pull requests are welcome on GitHub at https://github.com/logicsaas/ruby_rolling_rate_limiter. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
71
+
72
+
73
+ ## License
74
+
75
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
76
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ruby_rolling_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,96 @@
1
+ require "ruby_rolling_rate_limiter/version"
2
+ require "ruby_rolling_rate_limiter/errors"
3
+ require "redis"
4
+ require 'redis-lock'
5
+ require "date"
6
+
7
+ class RubyRollingRateLimiter
8
+ # Your code goes here...
9
+ attr_reader :current_error
10
+
11
+ def initialize(limiter_identifier, interval_in_seconds, max_calls_per_interval, min_distance_between_calls_in_seconds = 1, redis_connection = $redis)
12
+ @limiter_identifier = limiter_identifier
13
+ @interval_in_seconds = interval_in_seconds
14
+ @max_calls_per_interval = max_calls_per_interval
15
+ @min_distance_between_calls_in_seconds = min_distance_between_calls_in_seconds
16
+ @redis_connection = redis_connection
17
+
18
+ #Check to ensure args are good.
19
+ validate_arguments
20
+
21
+ # Check Redis is there
22
+ check_redis_is_available
23
+
24
+ end
25
+
26
+
27
+ def set_call_identifier(id)
28
+ raise Errors::ArgumentInvalid, "The id must be a string or number with length greater than zero" unless id.length > 0
29
+ @id = id
30
+ end
31
+
32
+ def can_call_proceed?(call_size = 1)
33
+ if call_size > @max_calls_per_interval
34
+ @current_error = {code: 0, result: false, error: "Call size too big. Max calls in rolling window is: #{@max_calls_per_interval}. Increase your max_calls_per_interval or decrease your call_size", retry_in: 0}
35
+ return false
36
+ end
37
+ results = false
38
+ now = DateTime.now.strftime('%s%6N').to_i # Time since EPOC in microseconds.
39
+ interval = @interval_in_seconds * 1000 * 1000 # Inteval in microseconds
40
+
41
+ key = "#{self.class.name}-#{@limiter_identifier}-#{@id}"
42
+
43
+ clear_before = now - interval
44
+ # Begin multi redis
45
+ @redis_connection.lock("#{key}-lock") do |lock|
46
+ @redis_connection.multi
47
+ @redis_connection.zremrangebyscore(key, 0, clear_before.to_s)
48
+ @redis_connection.zrange(key, 0, -1)
49
+ cur = @redis_connection.exec
50
+
51
+ if (cur[1].count <= @max_calls_per_interval) && ((cur[1].count+call_size) <= @max_calls_per_interval) && ((@min_distance_between_calls_in_seconds * 1000 * 1000) && (now - cur[1].last.to_i) > (@min_distance_between_calls_in_seconds * 1000 * 1000))
52
+ @redis_connection.multi
53
+ @redis_connection.zrange(key, 0, -1)
54
+ call_size.times do
55
+ @redis_connection.zadd(key, now.to_s, now.to_s)
56
+ end
57
+ @redis_connection.expire(key, @interval_in_seconds)
58
+ results = @redis_connection.exec
59
+ else
60
+ results = [cur[1]]
61
+ end
62
+ end
63
+ if results
64
+ call_set = results[0]
65
+ too_many_in_interval = call_set.count >= @max_calls_per_interval
66
+ time_since_last_request = (@min_distance_between_calls_in_seconds * 1000 * 1000) && (now - call_set.last.to_i)
67
+
68
+ if too_many_in_interval
69
+ @current_error = {code: 1, result: false, error: "Too many requests", retry_in: (call_set.first.to_i - now + interval) / 1000 / 1000, retry_in_micro: (call_set.first.to_i - now + interval)}
70
+ return false
71
+ elsif (call_set.count+call_size) > @max_calls_per_interval
72
+ @current_error = {code: 2, result: false, error: "Call Size too big for available access, trying to make #{call_size} with only #{call_set.count} calls available in window", retry_in: (call_set.first.to_i - now + interval) / 1000 / 1000, retry_in_micro: (call_set.first.to_i - now + interval)}
73
+ return false
74
+ elsif time_since_last_request < (@min_distance_between_calls_in_seconds * 1000 * 1000)
75
+ @current_error = {code: 3, result: false, error: "Attempting to thrash faster than the minimal distance between calls", retry_in: @min_distance_between_calls_in_seconds, retry_in_micro: (@min_distance_between_calls_in_seconds * 1000 * 1000)}
76
+ return false
77
+ end
78
+ return true
79
+ end
80
+ return false
81
+ end
82
+
83
+ private
84
+ def validate_arguments
85
+ raise Errors::ArgumentInvalid, "limiter_identifier argument must be 1 or more characters long" unless @limiter_identifier.length > 0
86
+ raise Errors::ArgumentInvalid, "interval_in_seconds argument must be an integer, this is specified in seconds" unless @interval_in_seconds.is_a? Integer and @interval_in_seconds > 0
87
+ raise Errors::ArgumentInvalid, "max_calls_per_interval argument must be an integer, this is the amount of calls that can be made during the rolling window." unless @max_calls_per_interval.is_a? Integer and @max_calls_per_interval > 0
88
+ raise Errors::ArgumentInvalid, "min_distance_between_calls_in_seconds argument must be an integer, this is the buffer between each call during the rolling window" unless @min_distance_between_calls_in_seconds.is_a? Integer and @min_distance_between_calls_in_seconds > 0
89
+ end
90
+
91
+ #
92
+ # Checks to ensure redis is present and available,
93
+ def check_redis_is_available
94
+ raise Errors::RedisNotFound, "Unable to find redis connection, either declare a global $redis connection or add the connection to the last argument on #{self.class.name} initializer" unless @redis_connection.is_a? Object and @redis_connection.class == Redis
95
+ end
96
+ end
@@ -0,0 +1,6 @@
1
+ class RubyRollingRateLimiter
2
+ module Errors
3
+ class RedisNotFound < StandardError; end
4
+ class ArgumentInvalid < StandardError; end
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ class RubyRollingRateLimiter
2
+ VERSION = "0.1.1"
3
+ end
@@ -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 'ruby_rolling_rate_limiter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ruby_rolling_rate_limiter"
8
+ spec.version = RubyRollingRateLimiter::VERSION
9
+ spec.authors = ["Karl Kloppenborg"]
10
+ spec.email = ["karl@logicsaas.com"]
11
+
12
+ spec.summary = %q{Ruby Rolling Rate Limiter provides a redis backed rolling window rate limiting service}
13
+ spec.description = %q{Often Redis is used for rate limiting purposes. Usually the rate limit packages available count how many times something happens on a certain second or a certain minute. When the clock ticks to the next minute, rate limit counter is reset back to the zero. This might be problematic if you are looking to limit rates where hits per integration time window is very low. If you are looking to limit to the five hits per minute, in one time window you get just one hit and six in another, even though the average over two minutes is 3.5. This package allows you to implement a correct rolling window of threshold that's backed by ATOMIC storage in Redis meaning you can use this implementation across multiple machines and processes.}
14
+ spec.homepage = "https://github.com/logicsaas/ruby_rolling_rate_limiter"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.11"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "minitest", "~> 5.0"
28
+ spec.add_development_dependency "bump"
29
+
30
+ spec.add_runtime_dependency "redis", "~> 3.2"
31
+ spec.add_runtime_dependency "mlanett-redis-lock"
32
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_rolling_rate_limiter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Karl Kloppenborg
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bump
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redis
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mlanett-redis-lock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Often Redis is used for rate limiting purposes. Usually the rate limit
98
+ packages available count how many times something happens on a certain second or
99
+ a certain minute. When the clock ticks to the next minute, rate limit counter is
100
+ reset back to the zero. This might be problematic if you are looking to limit rates
101
+ where hits per integration time window is very low. If you are looking to limit
102
+ to the five hits per minute, in one time window you get just one hit and six in
103
+ another, even though the average over two minutes is 3.5. This package allows you
104
+ to implement a correct rolling window of threshold that's backed by ATOMIC storage
105
+ in Redis meaning you can use this implementation across multiple machines and processes.
106
+ email:
107
+ - karl@logicsaas.com
108
+ executables: []
109
+ extensions: []
110
+ extra_rdoc_files: []
111
+ files:
112
+ - ".gitignore"
113
+ - ".travis.yml"
114
+ - CODE_OF_CONDUCT.md
115
+ - Gemfile
116
+ - LICENSE.txt
117
+ - README.md
118
+ - Rakefile
119
+ - bin/console
120
+ - bin/setup
121
+ - lib/ruby_rolling_rate_limiter.rb
122
+ - lib/ruby_rolling_rate_limiter/errors.rb
123
+ - lib/ruby_rolling_rate_limiter/version.rb
124
+ - ruby_rolling_rate_limiter.gemspec
125
+ homepage: https://github.com/logicsaas/ruby_rolling_rate_limiter
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.4.4
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Ruby Rolling Rate Limiter provides a redis backed rolling window rate limiting
149
+ service
150
+ test_files: []