simple_throttle 1.0.0

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: c5d75f7ea1ae6ba61f7ec9894e4247259d3195f8
4
+ data.tar.gz: b1cb14758b9d5729ab9cae5baa22c8cbd40eb9b1
5
+ SHA512:
6
+ metadata.gz: 78c8fd5675425a14094a04ff7f745a8da481d51741da8605ff3f460d77160b6421b30ae6ebba9bb412c45a28a0df8bc502c4a2dd19bd4cf8f87056ba753dcee3
7
+ data.tar.gz: 3553b9f03f048bb96b83935ff93fede2c034d83e9656d32e314e9c8cacec8bf681715c593663e4ce357ba41c59d075b153ecce581af853755c1479565c1a386d
data/MIT_LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 WHI, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ This gem provides a very simple throttling mechanism backed by redis for limiting access to a resource.
2
+
3
+ ## Usage
4
+
5
+ ```ruby
6
+ # Initialize Redis client
7
+ SimpleThrottle.set_redis(Redis.new)
8
+
9
+ # Add a global throttle (max of 10 requests in 60 seconds)
10
+ SimpleThrottle.add(:things, limit: 10, ttl: 60)
11
+
12
+ # Throttle a resource
13
+ if SimpleThrottle[:things].allowed!
14
+ do_somthing
15
+ else
16
+ raise "Too many requests. Resource available in #{SimpleThrottle[:things].wait_time} seconds"
17
+ end
18
+ ```
19
+
20
+ Calling `allow!` will return true if the throttle limit has not yet been reached and will also start tracking a new call if it returned true. There is no way to release a throttled call (that's why it's called SimpleThrottle).
21
+
22
+ The throttle data is kept in redis as a list of timestamps and will be auto expired if it falls out of use. The thottles time windows are rolling time windows and more calls will be allowed as soon as possible.
23
+
24
+ Redis server 2.6 or greater is required.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc 'Default: run unit tests.'
4
+ task :default => :test
5
+
6
+ desc 'RVM likes to call it tests'
7
+ task :tests => :test
8
+
9
+ begin
10
+ require 'rspec'
11
+ require 'rspec/core/rake_task'
12
+ desc 'Run the unit tests'
13
+ RSpec::Core::RakeTask.new(:test)
14
+ rescue LoadError
15
+ task :test do
16
+ STDERR.puts "You must have rspec 2.0 installed to run the tests"
17
+ end
18
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,150 @@
1
+ require 'redis'
2
+
3
+ # Create a simple throttle that can be used to limit the number of request for a resouce
4
+ # per time period. These objects are thread safe.
5
+ class SimpleThrottle
6
+
7
+ class << self
8
+ # Add a new throttle that can be referenced later with the [] method.
9
+ def add(name, limit:, ttl:)
10
+ @throttles ||= {}
11
+ @throttles[name.to_s] = new(name, limit: limit, ttl: ttl)
12
+ end
13
+
14
+ # Returns a globally defined throttle with the specfied name.
15
+ def [](name)
16
+ if defined?(@throttles) && @throttles
17
+ @throttles[name.to_s]
18
+ else
19
+ nil
20
+ end
21
+ end
22
+
23
+ # Set the Redis instance to use for maintaining the throttle. This can either be set
24
+ # with a hard coded value or by the value yielded by a block. If the block form is used
25
+ # it will be invoked at runtime to get the instance. Use this method if your Redis instance
26
+ # isn't constant (for example if you're in a forking environment and re-initialize connections
27
+ # on fork)
28
+ def set_redis(client = nil, &block)
29
+ @redis_client = (client || block)
30
+ end
31
+
32
+ # Return the Redis instance where the throttles are stored.
33
+ def redis
34
+ if @redis_client.is_a?(Proc)
35
+ @redis_client.call
36
+ else
37
+ @redis_client
38
+ end
39
+ end
40
+ end
41
+
42
+ attr_reader :name, :limit, :ttl
43
+
44
+ # Create a new throttle with the given name. The ttl argument specifies the time
45
+ # range that is being used for measuring in seconds while the limit specifies how
46
+ # many calls are allowed in that range.
47
+ def initialize(name, limit:, ttl:)
48
+ @name = name.to_s
49
+ @name.freeze unless @name.frozen?
50
+ @limit = limit
51
+ @ttl = ttl
52
+ @script_sha_1 = nil
53
+ end
54
+
55
+ # Returns true if the limit for the throttle has not been reached yet. This method
56
+ # will also track the throttled resource as having been invoked on each call.
57
+ def allowed!
58
+ size = current_size(true)
59
+ if size < limit
60
+ true
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+ # Reset a throttle back to zero.
67
+ def reset!
68
+ self.class.redis.del(redis_key)
69
+ end
70
+
71
+ # Peek at the current number for throttled calls being tracked.
72
+ def peek
73
+ current_size(false)
74
+ end
75
+
76
+ # Returns when the next resource call should be allowed. Note that this doesn't guarantee that
77
+ # calling allow! will return true if the wait time is zero since other processes or threads can
78
+ # claim the resource.
79
+ def wait_time
80
+ if peek < limit
81
+ 0.0
82
+ else
83
+ first = self.class.redis.lindex(redis_key, 0).to_f / 1000.0
84
+ delta = Time.now.to_f - first
85
+ delta = 0.0 if delta < 0
86
+ delta
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # Evaluate and execute a Lua script on the redis server that returns the number calls currently being tracked.
93
+ # If push is set to true then a new item will be added to the list.
94
+ def current_size(push)
95
+ redis = self.class.redis
96
+ @script_sha_1 ||= redis.script(:load, lua_script)
97
+ begin
98
+ push_arg = (push ? 1 : 0)
99
+ time_ms = (Time.now.to_f * 1000).round
100
+ ttl_ms = ttl * 1000
101
+ redis.evalsha(@script_sha_1, [], [redis_key, limit, ttl_ms, time_ms, push_arg])
102
+ rescue Redis::CommandError => e
103
+ if e.message.include?('NOSCRIPT'.freeze)
104
+ @script_sha_1 = redis.script(:load, lua_script)
105
+ retry
106
+ else
107
+ raise e
108
+ end
109
+ end
110
+ end
111
+
112
+ def redis_key
113
+ "simple_throttle.#{name}"
114
+ end
115
+
116
+ # Server side Lua script that maintains the throttle in redis. The throttle is stored as a list
117
+ # of timestamps in milliseconds. When the script is invoked it will scan the oldest entries
118
+ # removing any that should be expired from the list. If the list is below the specified limit
119
+ # then the current entry will be added. The list is marked to expire with the oldest entry so
120
+ # there's no need to cleanup the lists.
121
+ def lua_script
122
+ <<-LUA
123
+ local list_key = ARGV[1]
124
+ local limit = tonumber(ARGV[2])
125
+ local ttl = tonumber(ARGV[3])
126
+ local now = ARGV[4]
127
+ local push = tonumber(ARGV[5])
128
+
129
+ local size = redis.call('llen', list_key)
130
+ if size >= limit then
131
+ local expired = tonumber(now) - ttl
132
+ while size > 0 do
133
+ local t = redis.call('lpop', list_key)
134
+ if tonumber(t) > expired then
135
+ redis.call('lpush', list_key, t)
136
+ break
137
+ end
138
+ size = size - 1
139
+ end
140
+ end
141
+
142
+ if push > 0 and size < limit then
143
+ redis.call('rpush', list_key, now)
144
+ redis.call('pexpire', list_key, ttl)
145
+ end
146
+
147
+ return size
148
+ LUA
149
+ end
150
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "simple_throttle"
7
+ spec.version = File.read(File.expand_path("../VERSION", __FILE__)).chomp
8
+ spec.authors = ["We Heart It", "Brian Durand"]
9
+ spec.email = ["dev@weheartit.com", "bbdurand@gmail.com"]
10
+ spec.summary = "Simple redis backed throttling mechanism to limit access to a resource"
11
+ spec.description = "Simple redis backed throttling mechanism to limit access to a resource."
12
+ spec.homepage = "https://github.com/weheartit/sidekiq_fast_enq"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency('redis')
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec"
25
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleThrottle do
4
+
5
+ it "should tell if a call is allowed" do
6
+ throttle = SimpleThrottle.new("test_simple_throttle", limit: 3, ttl: 1)
7
+ throttle.reset!
8
+ other_throttle = SimpleThrottle.new("test_simple_throttle_2", limit: 3, ttl: 1)
9
+ other_throttle.reset!
10
+
11
+ expect(throttle.peek).to eq 0
12
+ expect(throttle.allowed!).to eq true
13
+ expect(throttle.peek).to eq 1
14
+ expect(throttle.allowed!).to eq true
15
+ expect(throttle.peek).to eq 2
16
+ expect(throttle.allowed!).to eq true
17
+ expect(throttle.peek).to eq 3
18
+ expect(throttle.allowed!).to eq false
19
+ expect(throttle.peek).to eq 3
20
+ expect(throttle.allowed!).to eq false
21
+ wait_time = throttle.wait_time
22
+ expect(wait_time).to be > 0.0
23
+ expect(wait_time).to be <= 1.0
24
+
25
+ expect(other_throttle.allowed!).to eq true
26
+ expect(other_throttle.peek).to eq 1
27
+ expect(other_throttle.wait_time).to eq 0.0
28
+
29
+ sleep(1.1)
30
+
31
+ expect(throttle.allowed!).to eq true
32
+ sleep(0.3)
33
+ expect(throttle.allowed!).to eq true
34
+ sleep(0.3)
35
+ expect(throttle.allowed!).to eq true
36
+ sleep(0.3)
37
+ expect(throttle.allowed!).to eq false
38
+ sleep(0.3)
39
+ expect(throttle.allowed!).to eq true
40
+ end
41
+
42
+ it "should be able to add global throttles" do
43
+ SimpleThrottle.add(:test_1, limit: 4, ttl: 60)
44
+ SimpleThrottle.add(:test_2, limit: 10, ttl: 3600)
45
+ t1 = SimpleThrottle["test_1"]
46
+ expect(t1.name).to eq "test_1"
47
+ expect(t1.limit).to eq 4
48
+ expect(t1.ttl).to eq 60
49
+ t1 = SimpleThrottle[:test_2]
50
+ expect(t1.name).to eq "test_2"
51
+ expect(t1.limit).to eq 10
52
+ expect(t1.ttl).to eq 3600
53
+ end
54
+
55
+ end
@@ -0,0 +1,14 @@
1
+ require File.expand_path('../../lib/simple_throttle', __FILE__)
2
+
3
+ SimpleThrottle.set_redis(Redis.new)
4
+
5
+ RSpec.configure do |config|
6
+ config.run_all_when_everything_filtered = true
7
+ config.filter_run :focus
8
+
9
+ # Run specs in random order to surface order dependencies. If you find an
10
+ # order dependency and want to debug it, you can fix the order by providing
11
+ # the seed, which is printed after each run.
12
+ # --seed 1234
13
+ config.order = 'random'
14
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_throttle
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - We Heart It
8
+ - Brian Durand
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-05-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.3'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.3'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description: Simple redis backed throttling mechanism to limit access to a resource.
71
+ email:
72
+ - dev@weheartit.com
73
+ - bbdurand@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - MIT_LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - VERSION
82
+ - lib/simple_throttle.rb
83
+ - simple_throttle.gemspec
84
+ - spec/simple_throttle_spec.rb
85
+ - spec/spec_helper.rb
86
+ homepage: https://github.com/weheartit/sidekiq_fast_enq
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.4.5
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Simple redis backed throttling mechanism to limit access to a resource
110
+ test_files:
111
+ - spec/simple_throttle_spec.rb
112
+ - spec/spec_helper.rb