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 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