ihasa 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 46f87a6594af9e83c6ff6d56f7d897d376325993
4
+ data.tar.gz: 13437815218c853f71cae4a93b0af469bfaee9bb
5
+ SHA512:
6
+ metadata.gz: dd880e51552ae7a23d07b7f6ea93e1cd333e3ee82bdeed6ea207c343deb71e026c9ddb4caa2c24e68e3993f879e37444117523927c887d85f78a2cc7e9e42c5b
7
+ data.tar.gz: 9316733742bf6224bc8bf137ab804970530a69e6be3bb66e19361812fdfa21dd5afe3a7de69f2c8b3a91e4e44c3f04ff685675677805e1869311329a3706321d
@@ -0,0 +1,44 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ast (2.2.0)
5
+ byebug (8.2.2)
6
+ diff-lcs (1.2.5)
7
+ parser (2.3.1.0)
8
+ ast (~> 2.2)
9
+ powerpack (0.1.1)
10
+ rainbow (2.1.0)
11
+ redis (3.3.0)
12
+ rspec (3.4.0)
13
+ rspec-core (~> 3.4.0)
14
+ rspec-expectations (~> 3.4.0)
15
+ rspec-mocks (~> 3.4.0)
16
+ rspec-core (3.4.4)
17
+ rspec-support (~> 3.4.0)
18
+ rspec-expectations (3.4.0)
19
+ diff-lcs (>= 1.2.0, < 2.0)
20
+ rspec-support (~> 3.4.0)
21
+ rspec-mocks (3.4.1)
22
+ diff-lcs (>= 1.2.0, < 2.0)
23
+ rspec-support (~> 3.4.0)
24
+ rspec-support (3.4.1)
25
+ rubocop (0.40.0)
26
+ parser (>= 2.3.1.0, < 3.0)
27
+ powerpack (~> 0.1)
28
+ rainbow (>= 1.99.1, < 3.0)
29
+ ruby-progressbar (~> 1.7)
30
+ unicode-display_width (~> 1.0, >= 1.0.1)
31
+ ruby-progressbar (1.8.1)
32
+ unicode-display_width (1.0.5)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ byebug
39
+ redis
40
+ rspec
41
+ rubocop
42
+
43
+ BUNDLED WITH
44
+ 1.11.2
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012-14 Bozhidar Batsov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,139 @@
1
+ No more pounding on your APIs !
2
+
3
+ Ihasa is a ruby implementation of the [token bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) backed-up by Redis.
4
+
5
+ It provides a way to share your rate/burst limit across multiple servers, as well as a simple interface.
6
+
7
+ *Why use Ihasa?*
8
+ 1. It's easy to use ([go check the usage section](#usage)
9
+ 2. It supports rate AND burst
10
+ 3. It does not reset all rate limit consumption each new second/minute/hour
11
+ 4. It has [namespaces](namespaces)
12
+
13
+
14
+ - [Installation](#installation)
15
+ - [Usage](#usage)
16
+ - [Advanced](#advanced)
17
+ - [Example](#example)
18
+
19
+ ## Installation
20
+
21
+ Installation is standard:
22
+
23
+ ```
24
+ $ gem install ihasa
25
+ ```
26
+
27
+ You can as well include it in you `Gemfile`:
28
+
29
+ ```
30
+ gem 'rubocop', require: false
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ Be sure to require Ihasa:
36
+
37
+ ```ruby
38
+ require 'ihasa'
39
+ ```
40
+
41
+ To create a new bucket accepting 5 requests per second with an allowed burst of 10
42
+ requests per second (the default values), use the `Ihasa.bucket` method:
43
+
44
+ ```ruby
45
+ bucket = Ihasa.bucket
46
+ ```
47
+
48
+ Please note that the default redis connection is built from the `REDIS_URL`
49
+ environment variable, or use the default constructor of redis-rb
50
+ (`redis://localhost:6379`).
51
+
52
+ Now, you can use your token bucket to check if an incoming request can be handled
53
+ or must be turned down:
54
+
55
+ ```ruby
56
+ def process(request)
57
+ if @bucket.accept?
58
+ # Do very interesting things with the request
59
+ # ...
60
+ else
61
+ puts "Could not process request #{request}. Rate limit violated."
62
+ end
63
+ end
64
+ ```
65
+
66
+ Please note that there is also a `Ihasa::Bucket#accept?!` method that raise a
67
+ `Ihasa::Bucket::EmptyBucket` errors if the limit is violated.
68
+
69
+ ### Advanced
70
+
71
+ Here is some details on the available configuration options of the Ihasa::Bucket
72
+ class.
73
+
74
+ #### Configuring rate and burst limit
75
+
76
+ Two configuration options exists to configure the rate and burst limits:
77
+
78
+ ```ruby
79
+ bucket = Ihasa.bucket(rate: 20, burst: 100)
80
+ ```
81
+
82
+ #### Namespaces
83
+
84
+ You can have as many bucket as you want on the same redis instance, as long as you
85
+ configure different namespace for each of them.
86
+
87
+ Here is an example of using two different buckets for reading and writing to data:
88
+
89
+ ```ruby
90
+ class Controller < ActionController::Base
91
+ def self.read_bucket
92
+ @read_bucket ||= Ihasa.bucket(prefix: 'read')
93
+ end
94
+
95
+ def self.write_bucket
96
+ @write_bucket ||= Ihasa.bucket(prefix: 'write')
97
+ end
98
+
99
+ def read(request)
100
+ return head 403 unless self.class.read_bucket.accept?
101
+ # ... Standard rendering ...
102
+ end
103
+
104
+ def write(request)
105
+ return head 403 unless self.class.write_bucket.accept?
106
+ # ... Standard rendering ...
107
+ end
108
+ ```
109
+
110
+ #### Redis
111
+
112
+ By default, all new buckets use the redis instance hosted at localhost:6379. There is
113
+ however two way to configure the used redis instance:
114
+ 1. Override the `REDIS_URL` env variable. All new buckets will use that instance
115
+ 2. Override the redis url on a bucket creation basis like follow:
116
+
117
+ ```ruby
118
+ Ihasa.bucket(redis: Redis.new(url: 'redis://fancy_host:6379'))
119
+ ```
120
+
121
+ ## Example
122
+
123
+ This is an example of a rack middleware accepting 20 requests per seconds, and
124
+ tolerating burst up to 100 requests per second:
125
+
126
+ ```ruby
127
+ class RateLimiter
128
+ BUCKET = Ihasa.bucket(rate: 20, burst: 100)
129
+
130
+ def initialize(app)
131
+ @app = app
132
+ end
133
+
134
+ def call(env)
135
+ return @app.call(env) if BUCKET.accept?
136
+ [403, {'Content-Type' => 'text/plain'}, ["Request limit violated\n"]]
137
+ end
138
+ end
139
+ ```
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
4
+ require 'ihasa/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'ihasa'
8
+ s.version = Ihasa::Version::STRING
9
+ s.platform = Gem::Platform::RUBY
10
+ s.required_ruby_version = '>= 1.9.3'
11
+ s.authors = ['Alexandre Ignjatovic']
12
+ s.description = <<-EOF
13
+ A Redis-backed rate limiter written in ruby and
14
+ using the token bucket algorithm.
15
+ Its light and efficient implementation takes
16
+ advantage of the Lua capabilities of Redis.
17
+ EOF
18
+
19
+ s.email = 'alexandre.ignjatovic@gmail.com'
20
+ s.files = `git ls-files`.split($RS).reject do |file|
21
+ file =~ %r{^(?:
22
+ spec/.*
23
+ |Gemfile
24
+ |Rakefile
25
+ |\.rspec
26
+ |\.gitignore
27
+ |\.rubocop.yml
28
+ |\.rubocop_todo.yml
29
+ |.*\.eps
30
+ )$}x
31
+ end
32
+ s.test_files = []
33
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
34
+ s.extra_rdoc_files = ['LICENSE.txt', 'README.md']
35
+ s.homepage = 'http://github.com/bankair/ihasa'
36
+ s.licenses = ['MIT']
37
+ s.require_paths = ['lib']
38
+ s.rubygems_version = '1.8.23'
39
+
40
+ s.summary = 'Redis-backed rate limiter (token bucket) written in Ruby and Lua'
41
+
42
+ s.add_runtime_dependency('redis', '~> 3')
43
+ s.add_development_dependency('rspec', '~> 3.4')
44
+ end
@@ -0,0 +1,19 @@
1
+ require 'redis'
2
+ require 'ihasa/version'
3
+ require 'ihasa/bucket'
4
+
5
+ module Ihasa
6
+ module_function
7
+ def default_redis
8
+ @redis ||= if ENV['REDIS_URL']
9
+ Redis.new url: ENV['REDIS_URL']
10
+ else
11
+ Redis.new
12
+ end
13
+ end
14
+
15
+ DEFAULT_REDIS_PREFIX = 'IHAB'
16
+ def bucket(rate: 5, burst: 10, prefix: DEFAULT_REDIS_PREFIX, redis: default_redis)
17
+ @implementation = Bucket.new(rate, burst, prefix, redis)
18
+ end
19
+ end
@@ -0,0 +1,123 @@
1
+ module Ihasa
2
+ NOK = 0
3
+ OK = 1
4
+ # Bucket class. That bucket fills up to burst, by rate per
5
+ # second. Each accept? or accept?! call decrement it from 1.
6
+ class Bucket
7
+ attr_reader :redis
8
+ def initialize(rate, burst, prefix, redis)
9
+ @prefix = prefix
10
+ @keys = {}
11
+ @keys[:rate] = "#{prefix}:RATE"
12
+ @keys[:allowance] = "#{prefix}:ALLOWANCE"
13
+ @keys[:burst] = "#{prefix}:BURST"
14
+ @keys[:last] = "#{prefix}:LAST"
15
+ @redis = redis
16
+ @rate = Float rate
17
+ @burst = Float burst
18
+ redis_init
19
+ end
20
+
21
+ def accept?
22
+ result = redis_eval(statement) == OK
23
+ return yield if result && block_given?
24
+ result
25
+ end
26
+
27
+ class EmptyBucket < RuntimeError; end
28
+
29
+ def accept!
30
+ result = (block_given? ? accept?(&Proc.new) : accept?)
31
+ raise EmptyBucket, "Bucket #{@prefix} throttle limit" unless result
32
+ result
33
+ end
34
+
35
+ protected
36
+
37
+ def redis_init
38
+ redis_eval <<-LUA
39
+ #{INTRO_STATEMENT}
40
+ #{redis_set rate, @rate}
41
+ #{redis_set burst, @burst}
42
+ #{redis_set allowance, @burst}
43
+ #{redis_set last, 'now'}
44
+ LUA
45
+ end
46
+
47
+ require 'forwardable'
48
+ extend Forwardable
49
+
50
+ def_delegator :@keys, :keys
51
+ def_delegator :@keys, :values, :redis_keys
52
+
53
+ def index(key)
54
+ keys.index(key) + 1
55
+ end
56
+
57
+ # Please note that the replicate_commands is mandatory when using a
58
+ # non deterministic command before writing shit to the redis instance.
59
+ INTRO_STATEMENT = <<-LUA.freeze
60
+ redis.replicate_commands()
61
+ local now = redis.call('TIME')
62
+ now = now[1] + now[2] * 10 ^ -6
63
+ LUA
64
+
65
+ def redis_eval(statement)
66
+ redis.eval(statement, redis_keys)
67
+ end
68
+
69
+ ELAPSED_STATEMENT = 'local elapsed = now - last'.freeze
70
+
71
+ def local_statements
72
+ results = %i(rate burst last allowance).map do |key|
73
+ "local #{key} = tonumber(#{redis_get(redis_key(key))})"
74
+ end
75
+ results << ELAPSED_STATEMENT
76
+ results.join "\n"
77
+ end
78
+
79
+ ALLOWANCE_UPDATE_STATEMENT = <<-LUA.freeze
80
+ allowance = allowance + (elapsed * rate)
81
+ if allowance > burst then
82
+ allowance = burst
83
+ end
84
+ LUA
85
+
86
+ def statement
87
+ @statement ||= <<-LUA
88
+ #{INTRO_STATEMENT}
89
+ #{local_statements}
90
+ #{ALLOWANCE_UPDATE_STATEMENT}
91
+ local result = #{NOK}
92
+ if allowance >= 1.0 then
93
+ #{redis_set(last, 'now')}
94
+ allowance = allowance - 1.0
95
+ result = #{OK}
96
+ end
97
+ #{redis_set(allowance, 'allowance')}
98
+ return result
99
+ LUA
100
+ end
101
+
102
+ def redis_exists(key)
103
+ "redis.call('EXISTS', #{key})"
104
+ end
105
+
106
+ def redis_key(key)
107
+ "KEYS[#{index key}]"
108
+ end
109
+
110
+ def redis_get(key)
111
+ "tonumber(redis.call('GET', #{key}))"
112
+ end
113
+
114
+ def redis_set(key, value)
115
+ "redis.call('SET', #{key}, tostring(#{value}))"
116
+ end
117
+
118
+ def method_missing(sym, *args, &block)
119
+ super unless @keys.key?(sym)
120
+ redis_key sym
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module Ihasa
4
+ # This module holds the Ihasa version information.
5
+ module Version
6
+ STRING = '0.0.1'
7
+
8
+ module_function
9
+
10
+ def version(debug = false)
11
+ STRING
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ihasa
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexandre Ignjatovic
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.4'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.4'
41
+ description: |2
42
+ A Redis-backed rate limiter written in ruby and
43
+ using the token bucket algorithm.
44
+ Its light and efficient implementation takes
45
+ advantage of the Lua capabilities of Redis.
46
+ email: alexandre.ignjatovic@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files:
50
+ - LICENSE.txt
51
+ - README.md
52
+ files:
53
+ - Gemfile.lock
54
+ - LICENSE.txt
55
+ - README.md
56
+ - ihasa.gemspec
57
+ - lib/ihasa.rb
58
+ - lib/ihasa/bucket.rb
59
+ - lib/ihasa/version.rb
60
+ homepage: http://github.com/bankair/ihasa
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 1.9.3
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.5.1
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Redis-backed rate limiter (token bucket) written in Ruby and Lua
84
+ test_files: []