throttled_object 1.0.0 → 1.1.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/.rvmrc +1 -0
- data/README.md +2 -1
- data/lib/throttled_object.rb +4 -2
- data/lib/throttled_object/lock.rb +33 -4
- data/lib/throttled_object/proxy.rb +11 -4
- data/lib/throttled_object/version.rb +1 -1
- data/spec/lock_spec.rb +32 -3
- data/spec/proxy_spec.rb +11 -2
- data/spec/spec_helper.rb +3 -1
- data/throttled_object.gemspec +1 -1
- metadata +20 -13
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@throttled_object --create
|
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# throttled_object
|
2
2
|
|
3
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
|
4
|
+
to a given object. It provides an interface to interact with an object, either sleeping or raising
|
5
|
+
an exception when it's not available.
|
5
6
|
|
6
7
|
The ideal use for this is for access to APIs with a blocking interface (e.g. Run the code in a thread,
|
7
8
|
sleep until it's available).
|
data/lib/throttled_object.rb
CHANGED
@@ -5,8 +5,10 @@ module ThrottledObject
|
|
5
5
|
require 'throttled_object/proxy'
|
6
6
|
|
7
7
|
def self.make(object, options = {}, *args)
|
8
|
-
|
9
|
-
|
8
|
+
object_options = args.last.is_a?(Hash) ? args.pop : {}
|
9
|
+
object_options[:lock] = Lock.new(options)
|
10
|
+
args << object_options
|
11
|
+
Proxy.new object, *args
|
10
12
|
end
|
11
13
|
|
12
14
|
end
|
@@ -3,7 +3,20 @@ require 'redis'
|
|
3
3
|
module ThrottledObject
|
4
4
|
class Lock
|
5
5
|
|
6
|
-
class
|
6
|
+
class Error < StandardError; end
|
7
|
+
class Unavailable < Error; end
|
8
|
+
class WaitForLock < Error
|
9
|
+
|
10
|
+
attr_reader :available_at, :wait_for
|
11
|
+
|
12
|
+
def initialize(time, *args)
|
13
|
+
super *args
|
14
|
+
@available_at = Time.now + time
|
15
|
+
@wait_for = time
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
7
20
|
|
8
21
|
KEY_PREFIX = "throttled_object:key:"
|
9
22
|
|
@@ -26,12 +39,13 @@ module ThrottledObject
|
|
26
39
|
# 2. Occassionally, we need to abort after a short period.
|
27
40
|
#
|
28
41
|
# 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 -
|
42
|
+
# loop and attempt to aggressively obtain the lock. We loop until we've obtained a lock -
|
30
43
|
# To obtain the lock, we increment the current periods counter and check if it's <= the max count.
|
31
44
|
# If it is, we have a lock. If not, we sleep until the lock should be 'fresh' again.
|
32
45
|
#
|
33
46
|
# If we're the first one to obtain a lock, we update some book keeping data.
|
34
47
|
def lock(max_time = nil)
|
48
|
+
raise 'lock must be called with a block' unless block_given?
|
35
49
|
started_at = current_period
|
36
50
|
has_lock = false
|
37
51
|
until has_lock
|
@@ -58,13 +72,28 @@ module ThrottledObject
|
|
58
72
|
obtained_at = [redis.get("#{current_key}:obtained_at").to_i, lockable_time].max
|
59
73
|
next_period = (lockable_time + period)
|
60
74
|
wait_for = (next_period - current_period).to_f / 1000
|
61
|
-
|
75
|
+
yield wait_for
|
62
76
|
end
|
63
77
|
end
|
64
78
|
end
|
65
79
|
|
80
|
+
def wait_for_lock(*args)
|
81
|
+
lock(*args) { |time| sleep time }
|
82
|
+
end
|
83
|
+
|
84
|
+
def lock!(*args)
|
85
|
+
lock(*args) do |time|
|
86
|
+
raise WaitForLock.new(time, "Lock unavailable, please wait #{time} seconds and attempt again.")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
66
90
|
def synchronize(*args, &blk)
|
67
|
-
|
91
|
+
wait_for_lock *args
|
92
|
+
yield if block_given?
|
93
|
+
end
|
94
|
+
|
95
|
+
def synchronize!(*args, &blk)
|
96
|
+
lock! *args
|
68
97
|
yield if block_given?
|
69
98
|
end
|
70
99
|
|
@@ -5,17 +5,24 @@ module ThrottledObject
|
|
5
5
|
|
6
6
|
attr_accessor :lock, :throttled_methods
|
7
7
|
|
8
|
-
def initialize(object,
|
8
|
+
def initialize(object, options = {})
|
9
9
|
super object
|
10
|
-
@lock = lock
|
10
|
+
@lock = options.fetch(:lock)
|
11
|
+
@blocking = options.fetch :blocking, true
|
12
|
+
@lock_method = @blocking ? :synchronize : :synchronize!
|
13
|
+
throttled_methods = options.fetch :methods, nil
|
11
14
|
@throttled_methods = throttled_methods && throttled_methods.map(&:to_sym)
|
12
15
|
end
|
13
16
|
|
14
17
|
private
|
15
18
|
|
19
|
+
def lock_method?(m)
|
20
|
+
throttled_methods.nil? || throttled_methods.include?(m.to_sym)
|
21
|
+
end
|
22
|
+
|
16
23
|
def method_missing(m, *args, &block)
|
17
|
-
if
|
18
|
-
@lock.
|
24
|
+
if lock_method?(m)
|
25
|
+
@lock.send(@lock_method) { super }
|
19
26
|
else
|
20
27
|
super
|
21
28
|
end
|
data/spec/lock_spec.rb
CHANGED
@@ -10,15 +10,44 @@ describe ThrottledObject::Lock do
|
|
10
10
|
end
|
11
11
|
|
12
12
|
it 'should throttle access under the limit' do
|
13
|
-
expect_to_take(0.0..0.99) { 4.times { lock.
|
13
|
+
expect_to_take(0.0..0.99) { 4.times { lock.wait_for_lock } }
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'should throttle access equal to the limit' do
|
17
|
-
expect_to_take(0.0..0.99) { 5.times { lock.
|
17
|
+
expect_to_take(0.0..0.99) { 5.times { lock.wait_for_lock } }
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'correctly throttle access over the limit' do
|
21
|
-
expect_to_take(1.0..1.99) { 6.times { lock.
|
21
|
+
expect_to_take(1.0..1.99) { 6.times { lock.wait_for_lock } }
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should allow you to have an exceptional version' do
|
25
|
+
started_at = Time.now.to_f
|
26
|
+
ended_at = nil
|
27
|
+
5.times { lock.lock! }
|
28
|
+
begin
|
29
|
+
lock.lock!
|
30
|
+
raise 'Should not reach this point'
|
31
|
+
rescue => e
|
32
|
+
ended_at = Time.now.to_f
|
33
|
+
e.should be_a ThrottledObject::Lock::WaitForLock
|
34
|
+
time_range = (started_at + 1.0)..(started_at + 2.0)
|
35
|
+
time_range.should include e.available_at.to_f
|
36
|
+
end
|
37
|
+
ended_at.should_not be_nil
|
38
|
+
(0.0..0.99).should include (ended_at - started_at)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should use lock! for synchronize!' do
|
42
|
+
dont_allow(lock).wait_for_lock
|
43
|
+
mock.proxy(lock).lock! { |v| v }
|
44
|
+
lock.synchronize! { true }
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should use wait_for_lock for synchronize' do
|
48
|
+
dont_allow(lock).lock!
|
49
|
+
mock.proxy(lock).wait_for_lock { |v| v }
|
50
|
+
lock.synchronize { true }
|
22
51
|
end
|
23
52
|
|
24
53
|
def expect_to_take(range, &block)
|
data/spec/proxy_spec.rb
CHANGED
@@ -17,7 +17,7 @@ describe ThrottledObject::Proxy do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
it 'should let you control which methods invoke it' do
|
20
|
-
proxy = ThrottledObject::Proxy.new target, lock, [:hello]
|
20
|
+
proxy = ThrottledObject::Proxy.new target, lock: lock, methods: [:hello]
|
21
21
|
proxy.one.should == 1
|
22
22
|
lock.value.should == 0
|
23
23
|
proxy.other.should == nil
|
@@ -29,7 +29,7 @@ describe ThrottledObject::Proxy do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'should default to requiring all are throttled' do
|
32
|
-
proxy = ThrottledObject::Proxy.new target, lock
|
32
|
+
proxy = ThrottledObject::Proxy.new target, lock: lock
|
33
33
|
expect do
|
34
34
|
proxy.one.should == 1
|
35
35
|
end.to change(lock, :value).by(1)
|
@@ -41,4 +41,13 @@ describe ThrottledObject::Proxy do
|
|
41
41
|
end.to change(lock, :value).by(1)
|
42
42
|
end
|
43
43
|
|
44
|
+
it 'should let you make a blocking one' do
|
45
|
+
def lock.synchronize!; yield if block_given?; end
|
46
|
+
def lock.synchronize; raise 'Lock using synchronize, not synchronize!.'; end
|
47
|
+
proxy = ThrottledObject::Proxy.new target, lock: lock, blocking: false
|
48
|
+
proxy.one.should == 1
|
49
|
+
proxy = ThrottledObject::Proxy.new target, lock: lock, blocking: true
|
50
|
+
expect { proxy.one }.to raise_error
|
51
|
+
end
|
52
|
+
|
44
53
|
end
|
data/spec/spec_helper.rb
CHANGED
data/throttled_object.gemspec
CHANGED
@@ -16,8 +16,8 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.version = ThrottledObject::VERSION
|
17
17
|
|
18
18
|
gem.add_dependency 'redis', '~> 3.0'
|
19
|
-
gem.add_development_dependency 'timecop'
|
20
19
|
gem.add_development_dependency 'rspec', '~> 2.0'
|
21
20
|
gem.add_development_dependency 'rake'
|
21
|
+
gem.add_development_dependency 'rr'
|
22
22
|
|
23
23
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throttled_object
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-08-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -28,39 +28,39 @@ dependencies:
|
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '3.0'
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
|
-
name:
|
31
|
+
name: rspec
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
33
33
|
none: false
|
34
34
|
requirements:
|
35
|
-
- -
|
35
|
+
- - ~>
|
36
36
|
- !ruby/object:Gem::Version
|
37
|
-
version: '0'
|
37
|
+
version: '2.0'
|
38
38
|
type: :development
|
39
39
|
prerelease: false
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
41
41
|
none: false
|
42
42
|
requirements:
|
43
|
-
- -
|
43
|
+
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version: '0'
|
45
|
+
version: '2.0'
|
46
46
|
- !ruby/object:Gem::Dependency
|
47
|
-
name:
|
47
|
+
name: rake
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
49
49
|
none: false
|
50
50
|
requirements:
|
51
|
-
- -
|
51
|
+
- - ! '>='
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '
|
53
|
+
version: '0'
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
none: false
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0'
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
|
-
name:
|
63
|
+
name: rr
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
none: false
|
66
66
|
requirements:
|
@@ -84,6 +84,7 @@ extra_rdoc_files: []
|
|
84
84
|
files:
|
85
85
|
- .gitignore
|
86
86
|
- .rspec
|
87
|
+
- .rvmrc
|
87
88
|
- Gemfile
|
88
89
|
- LICENSE
|
89
90
|
- README.md
|
@@ -109,12 +110,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
110
|
- - ! '>='
|
110
111
|
- !ruby/object:Gem::Version
|
111
112
|
version: '0'
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
hash: -4367176850969236044
|
112
116
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
117
|
none: false
|
114
118
|
requirements:
|
115
119
|
- - ! '>='
|
116
120
|
- !ruby/object:Gem::Version
|
117
121
|
version: '0'
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
hash: -4367176850969236044
|
118
125
|
requirements: []
|
119
126
|
rubyforge_project:
|
120
127
|
rubygems_version: 1.8.24
|