throttled_object 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 if it's unavailable.
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).
@@ -5,8 +5,10 @@ module ThrottledObject
5
5
  require 'throttled_object/proxy'
6
6
 
7
7
  def self.make(object, options = {}, *args)
8
- lock = Lock.new(options)
9
- Proxy.new object, lock, *args
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 Unavailable < StandardError; end
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
- sleep wait_for
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
- lock *args
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, lock, throttled_methods = nil)
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 throttled_methods.nil? || throttled_methods.include?(m.to_sym)
18
- @lock.synchronize { super }
24
+ if lock_method?(m)
25
+ @lock.send(@lock_method) { super }
19
26
  else
20
27
  super
21
28
  end
@@ -1,3 +1,3 @@
1
1
  module ThrottledObject
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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.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.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.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)
@@ -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
@@ -1,6 +1,8 @@
1
1
  ENV['REDIS_URL'] ||= "redis://127.0.0.1:6379/#{ENV['REDIS_TEST_DATABASE'] || 9}"
2
2
 
3
3
  require 'throttled_object'
4
+ require 'rr'
4
5
 
5
- RSpec.configure do |config|
6
+ RSpec.configure do |config|
7
+ config.mock_with :rr
6
8
  end
@@ -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.0.0
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-06-18 00:00:00.000000000 Z
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: timecop
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: rspec
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: '2.0'
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: '2.0'
61
+ version: '0'
62
62
  - !ruby/object:Gem::Dependency
63
- name: rake
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