throttled_object 1.0.0
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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/README.md +40 -0
- data/Rakefile +10 -0
- data/lib/throttled_object.rb +12 -0
- data/lib/throttled_object/lock.rb +82 -0
- data/lib/throttled_object/proxy.rb +25 -0
- data/lib/throttled_object/version.rb +3 -0
- data/spec/lock_spec.rb +30 -0
- data/spec/proxy_spec.rb +44 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/throttled_object_spec.rb +13 -0
- data/throttled_object.gemspec +23 -0
- metadata +128 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Filter Squad
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# throttled_object
|
2
|
+
|
3
|
+
`throttled_object` is a Ruby 1.9 library built on top of ruby to provide throttled access
|
4
|
+
to a given object. It provides an interface to interact with an object, sleeping if it's unavailable.
|
5
|
+
|
6
|
+
The ideal use for this is for access to APIs with a blocking interface (e.g. Run the code in a thread,
|
7
|
+
sleep until it's available).
|
8
|
+
|
9
|
+
**PLEASE NOTE:** I don't yet consider this production worthy. There are still bugs in the locking algorithm
|
10
|
+
that need to be worked out for it work efficiently. This will be done in the near future.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'throttled_object'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install throttled_object
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
TODO: Add here.
|
29
|
+
|
30
|
+
## Contributing
|
31
|
+
|
32
|
+
1. Fork it
|
33
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
34
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
35
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
36
|
+
5. Create new Pull Request
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
throttled_object is released under the MIT License (see the [license file](https://github.com/filtersquad/throttled_object/blob/master/LICENSE)) and is copyright Filter Squad, 2012.
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module ThrottledObject
|
4
|
+
class Lock
|
5
|
+
|
6
|
+
class Unavailable < StandardError; end
|
7
|
+
|
8
|
+
KEY_PREFIX = "throttled_object:key:"
|
9
|
+
|
10
|
+
attr_reader :identifier, :amount, :period, :redis
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@identifier = options[:identifier]
|
14
|
+
@amount = options[:amount]
|
15
|
+
@redis = options[:redis] || Redis.current
|
16
|
+
_period = options[:period]
|
17
|
+
raise ArgumentError.new("You must provide an :identifier as a string") unless identifier.is_a?(String)
|
18
|
+
raise ArgumentError.new("You must provide a valid amount of > hits per period") unless amount.is_a?(Numeric) && amount > 0
|
19
|
+
raise ArgumentError.new("You must provide a valid period of > 0 seconds") unless _period.is_a?(Numeric) && _period > 0
|
20
|
+
@period = (_period.to_f * 1000).ceil
|
21
|
+
end
|
22
|
+
|
23
|
+
# The general locking algorithm is pretty simple. It takes into account two things:
|
24
|
+
#
|
25
|
+
# 1. That we may want to block until it's available (the default)
|
26
|
+
# 2. Occassionally, we need to abort after a short period.
|
27
|
+
#
|
28
|
+
# So, the lock method operates in two methods. The first, and default, we will basically
|
29
|
+
# loop and attempt to aggressively obtain the lock. We loop until we've obtained a lock -
|
30
|
+
# To obtain the lock, we increment the current periods counter and check if it's <= the max count.
|
31
|
+
# If it is, we have a lock. If not, we sleep until the lock should be 'fresh' again.
|
32
|
+
#
|
33
|
+
# If we're the first one to obtain a lock, we update some book keeping data.
|
34
|
+
def lock(max_time = nil)
|
35
|
+
started_at = current_period
|
36
|
+
has_lock = false
|
37
|
+
until has_lock
|
38
|
+
now = current_period
|
39
|
+
if max_time && (now - started_at) >= max_time
|
40
|
+
raise Unavailable.new("Unable to obtain a lock after #{now - started_at}ms")
|
41
|
+
end
|
42
|
+
lockable_time = rounded_period now
|
43
|
+
current_key = KEY_PREFIX + lockable_time.to_s
|
44
|
+
count = redis.incr current_key
|
45
|
+
if count <= amount
|
46
|
+
has_lock = true
|
47
|
+
# Now we have a lock, we need to actually set the expiration of
|
48
|
+
# the key. Note, we only ever set this on the first set to avoid
|
49
|
+
# setting @amount times...
|
50
|
+
if count == 1
|
51
|
+
# Expire after 3 periods. This means we only
|
52
|
+
# ever keep a small number in memory.
|
53
|
+
expires_after = ((period * 3).to_f / 1000).ceil
|
54
|
+
redis.expire current_key, expires_after
|
55
|
+
redis.setex "#{current_key}:obtained_at", now, expires_after
|
56
|
+
end
|
57
|
+
else
|
58
|
+
obtained_at = [redis.get("#{current_key}:obtained_at").to_i, lockable_time].max
|
59
|
+
next_period = (lockable_time + period)
|
60
|
+
wait_for = (next_period - current_period).to_f / 1000
|
61
|
+
sleep wait_for
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def synchronize(*args, &blk)
|
67
|
+
lock *args
|
68
|
+
yield if block_given?
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def current_period
|
74
|
+
(Time.now.to_f * 1000).ceil
|
75
|
+
end
|
76
|
+
|
77
|
+
def rounded_period(time)
|
78
|
+
(time.to_f / period).ceil * period
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module ThrottledObject
|
4
|
+
class Proxy < SimpleDelegator
|
5
|
+
|
6
|
+
attr_accessor :lock, :throttled_methods
|
7
|
+
|
8
|
+
def initialize(object, lock, throttled_methods = nil)
|
9
|
+
super object
|
10
|
+
@lock = lock
|
11
|
+
@throttled_methods = throttled_methods && throttled_methods.map(&:to_sym)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def method_missing(m, *args, &block)
|
17
|
+
if throttled_methods.nil? || throttled_methods.include?(m.to_sym)
|
18
|
+
@lock.synchronize { super }
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/spec/lock_spec.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
describe ThrottledObject::Lock do
|
5
|
+
|
6
|
+
let(:lock) { ThrottledObject::Lock.new identifier: "greeter:#{Process.pid}", amount: 5, period: 1 }
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
lock.redis.flushdb
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should throttle access under the limit' do
|
13
|
+
expect_to_take(0.0..0.99) { 4.times { lock.lock } }
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should throttle access equal to the limit' do
|
17
|
+
expect_to_take(0.0..0.99) { 5.times { lock.lock } }
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'correctly throttle access over the limit' do
|
21
|
+
expect_to_take(1.0..1.99) { 6.times { lock.lock } }
|
22
|
+
end
|
23
|
+
|
24
|
+
def expect_to_take(range, &block)
|
25
|
+
start_time = Time.now.to_f
|
26
|
+
yield if block_given?
|
27
|
+
range.should include (Time.now.to_f - start_time)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/spec/proxy_spec.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ThrottledObject::Proxy do
|
4
|
+
|
5
|
+
let(:lock) do
|
6
|
+
counter = Struct.new(:value).new(0)
|
7
|
+
def counter.synchronize; self.value += 1; yield if block_given?; end
|
8
|
+
counter
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:target) do
|
12
|
+
Object.new.tap do |o|
|
13
|
+
def o.hello; "World"; end
|
14
|
+
def o.one; 1; end
|
15
|
+
def o.other; nil; end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should let you control which methods invoke it' do
|
20
|
+
proxy = ThrottledObject::Proxy.new target, lock, [:hello]
|
21
|
+
proxy.one.should == 1
|
22
|
+
lock.value.should == 0
|
23
|
+
proxy.other.should == nil
|
24
|
+
lock.value.should == 0
|
25
|
+
proxy.hello.should == "World"
|
26
|
+
lock.value.should == 1
|
27
|
+
proxy.hello.should == "World"
|
28
|
+
lock.value.should == 2
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should default to requiring all are throttled' do
|
32
|
+
proxy = ThrottledObject::Proxy.new target, lock
|
33
|
+
expect do
|
34
|
+
proxy.one.should == 1
|
35
|
+
end.to change(lock, :value).by(1)
|
36
|
+
expect do
|
37
|
+
proxy.other.should be_nil
|
38
|
+
end.to change(lock, :value).by(1)
|
39
|
+
expect do
|
40
|
+
proxy.hello.should == "World"
|
41
|
+
end.to change(lock, :value).by(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ThrottledObject do
|
4
|
+
|
5
|
+
it 'should give short hand to create an object' do
|
6
|
+
o = Object.new
|
7
|
+
def o.hello; "world"; end
|
8
|
+
object = ThrottledObject.make o, identifier: "test-object", period: 1, amount: 10
|
9
|
+
object.hello.should == "world"
|
10
|
+
object.lock.should be_a ThrottledObject::Lock
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/throttled_object/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Darcy Laycock"]
|
6
|
+
gem.email = ["darcy@filtersquad.com"]
|
7
|
+
gem.description = %q{Distributed Object Locks built on Redis.}
|
8
|
+
gem.summary = %q{Distributed Object Locks built on Redis.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "throttled_object"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = ThrottledObject::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'redis', '~> 3.0'
|
19
|
+
gem.add_development_dependency 'timecop'
|
20
|
+
gem.add_development_dependency 'rspec', '~> 2.0'
|
21
|
+
gem.add_development_dependency 'rake'
|
22
|
+
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: throttled_object
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Darcy Laycock
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: timecop
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Distributed Object Locks built on Redis.
|
79
|
+
email:
|
80
|
+
- darcy@filtersquad.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- .rspec
|
87
|
+
- Gemfile
|
88
|
+
- LICENSE
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- lib/throttled_object.rb
|
92
|
+
- lib/throttled_object/lock.rb
|
93
|
+
- lib/throttled_object/proxy.rb
|
94
|
+
- lib/throttled_object/version.rb
|
95
|
+
- spec/lock_spec.rb
|
96
|
+
- spec/proxy_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
- spec/throttled_object_spec.rb
|
99
|
+
- throttled_object.gemspec
|
100
|
+
homepage: ''
|
101
|
+
licenses: []
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 1.8.24
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Distributed Object Locks built on Redis.
|
124
|
+
test_files:
|
125
|
+
- spec/lock_spec.rb
|
126
|
+
- spec/proxy_spec.rb
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
- spec/throttled_object_spec.rb
|