zk 1.1.1 → 1.2.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 +1 -0
- data/.gitmodules +3 -0
- data/.travis.yml +5 -1
- data/.yardopts +0 -1
- data/Gemfile +15 -0
- data/README.markdown +46 -76
- data/RELEASES.markdown +18 -0
- data/Rakefile +46 -65
- data/lib/zk.rb +5 -0
- data/lib/zk/client/base.rb +33 -20
- data/lib/zk/client/unixisms.rb +7 -7
- data/lib/zk/core_ext.rb +26 -0
- data/lib/zk/event.rb +5 -5
- data/lib/zk/event_handler.rb +2 -2
- data/lib/zk/event_handler_subscription/actor.rb +1 -13
- data/lib/zk/event_handler_subscription/base.rb +15 -15
- data/lib/zk/exceptions.rb +3 -0
- data/lib/zk/extensions.rb +8 -37
- data/lib/zk/locker.rb +8 -0
- data/lib/zk/locker/exclusive_locker.rb +25 -12
- data/lib/zk/locker/locker_base.rb +124 -21
- data/lib/zk/locker/shared_locker.rb +34 -22
- data/lib/zk/stat.rb +2 -2
- data/lib/zk/subscription.rb +65 -0
- data/lib/zk/version.rb +1 -1
- data/notes/documentation-notes +20 -0
- data/notes/voting-notes.txt +39 -0
- data/spec/event_catcher_spec.rb +5 -2
- data/spec/informal/close-in-event-thread.rb +1 -1
- data/spec/shared/client_examples.rb +1 -1
- data/spec/spec_helper.rb +8 -1
- data/spec/support/event_catcher.rb +6 -6
- data/spec/support/latch.rb +23 -0
- data/spec/support/logging.rb +1 -1
- data/spec/watch_spec.rb +1 -1
- data/spec/zk/client/locking_and_session_death_spec.rb +7 -7
- data/spec/zk/locker_spec.rb +177 -62
- data/spec/zookeeper_spec.rb +2 -2
- data/zk.gemspec +3 -1
- metadata +34 -10
@@ -3,24 +3,10 @@ module ZK
|
|
3
3
|
class SharedLocker < LockerBase
|
4
4
|
include Exceptions
|
5
5
|
|
6
|
+
# (see LockerBase#lock)
|
6
7
|
# obtain a shared lock.
|
7
8
|
#
|
8
|
-
|
9
|
-
# a lock on the resource
|
10
|
-
#
|
11
|
-
# @return [true] if we're already obtained a shared lock, or if we were able to
|
12
|
-
# obtain the lock in non-blocking mode.
|
13
|
-
#
|
14
|
-
# @return [false] if we did not obtain the lock in non-blocking mode
|
15
|
-
#
|
16
|
-
# @return [void] if we obtained the lock in blocking mode.
|
17
|
-
#
|
18
|
-
# @raise [InterruptedSession] raised when blocked waiting for a lock and
|
19
|
-
# the underlying client's session is interrupted.
|
20
|
-
#
|
21
|
-
# @see ZK::Client::Unixisms#block_until_node_deleted more about possible execptions
|
22
|
-
#
|
23
|
-
def lock!(blocking=false)
|
9
|
+
def lock(blocking=false)
|
24
10
|
return true if @locked
|
25
11
|
create_lock_path!(SHARED_LOCK_PREFIX)
|
26
12
|
|
@@ -38,9 +24,35 @@ module ZK
|
|
38
24
|
end
|
39
25
|
end
|
40
26
|
|
27
|
+
# (see LockerBase#assert!)
|
28
|
+
#
|
29
|
+
# checks that we:
|
30
|
+
#
|
31
|
+
# * we have obtained the lock (i.e. {#locked?} is true)
|
32
|
+
# * have a lock path
|
33
|
+
# * our lock path still exists
|
34
|
+
# * there are no exclusive locks with lower numbers than ours
|
35
|
+
#
|
36
|
+
def assert!
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
# (see LockerBase#locked?)
|
41
|
+
def locked?
|
42
|
+
false|@locked
|
43
|
+
end
|
44
|
+
|
45
|
+
# (see LockerBase#acquirable?)
|
46
|
+
def acquirable?
|
47
|
+
return true if locked?
|
48
|
+
!lock_children.any? { |n| n.start_with?(EXCLUSIVE_LOCK_PREFIX) }
|
49
|
+
rescue Exceptions::NoNode
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
41
53
|
# @private
|
42
54
|
def lock_number
|
43
|
-
|
55
|
+
lock_path and digit_from(lock_path)
|
44
56
|
end
|
45
57
|
|
46
58
|
# returns the sequence number of the next lowest write lock node
|
@@ -69,9 +81,9 @@ module ZK
|
|
69
81
|
ary = ordered_lock_children()
|
70
82
|
my_idx = ary.index(lock_basename) # our idx would be 2
|
71
83
|
|
72
|
-
|
73
|
-
|
74
|
-
|
84
|
+
ary[0..my_idx].reverse.find { |n| n.start_with?(EXCLUSIVE_LOCK_PREFIX) }.tap do |rv|
|
85
|
+
raise NoWriteLockFoundException if rv.nil?
|
86
|
+
end
|
75
87
|
end
|
76
88
|
|
77
89
|
# @private
|
@@ -80,13 +92,13 @@ module ZK
|
|
80
92
|
rescue NoWriteLockFoundException
|
81
93
|
true
|
82
94
|
end
|
95
|
+
alias got_lock? got_read_lock?
|
83
96
|
|
84
97
|
protected
|
85
98
|
# TODO: make this generic, can either block or non-block
|
86
|
-
# @private
|
87
99
|
def block_until_read_lock!
|
88
100
|
begin
|
89
|
-
path =
|
101
|
+
path = "#{root_lock_path}/#{next_lowest_write_lock_name}"
|
90
102
|
logger.debug { "SharedLocker#block_until_read_lock! path=#{path.inspect}" }
|
91
103
|
@zk.block_until_node_deleted(path)
|
92
104
|
rescue NoWriteLockFoundException
|
data/lib/zk/stat.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module ZK
|
2
|
-
# Included in
|
2
|
+
# Included in Zookeeper::Stat, extends it with some conveniences for
|
3
3
|
# dealing with Stat objects. Also provides docuemntation here for the meaning
|
4
4
|
# of these values.
|
5
5
|
#
|
@@ -109,7 +109,7 @@ module ZK
|
|
109
109
|
end # Stat
|
110
110
|
end # ZK
|
111
111
|
|
112
|
-
class
|
112
|
+
class Zookeeper::Stat
|
113
113
|
include ZK::Stat
|
114
114
|
end
|
115
115
|
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ZK
|
2
|
+
# Basic pattern for objects that have the concept of a parent (the thing that
|
3
|
+
# granted this subscription), a callback, and that can unregister (so the
|
4
|
+
# callback no longer receives events).
|
5
|
+
#
|
6
|
+
# expects the 'parent' to respond_to? the 'unregister' method, and will
|
7
|
+
# be passed the subscription instance
|
8
|
+
module Subscription
|
9
|
+
class Base
|
10
|
+
include ZK::Logging
|
11
|
+
|
12
|
+
# the object from which we will attempt to #unregister on
|
13
|
+
# XXX: need a better name for this
|
14
|
+
attr_reader :parent
|
15
|
+
|
16
|
+
# the user-supplied callback block, used to create a ThreadedCallback
|
17
|
+
attr_reader :callable
|
18
|
+
|
19
|
+
def initialize(parent, block)
|
20
|
+
raise ArgumentError, "block must repsond_to?(:call)" unless block.respond_to?(:call)
|
21
|
+
@parent = parent
|
22
|
+
@callable = block
|
23
|
+
@mutex = Monitor.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def unregister
|
27
|
+
parent.unregister(self)
|
28
|
+
end
|
29
|
+
alias unsubscribe unregister
|
30
|
+
|
31
|
+
# @private
|
32
|
+
def call(*args)
|
33
|
+
callable.call(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
def synchronize
|
38
|
+
@mutex.synchronize { yield }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ActorStyle
|
43
|
+
extend Concern
|
44
|
+
|
45
|
+
included do
|
46
|
+
alias_method_chain :unsubscribe, :threaded_callback
|
47
|
+
alias_method_chain :callable, :threaded_callback_wrapper
|
48
|
+
end
|
49
|
+
|
50
|
+
def unsubscribe_with_threaded_callback
|
51
|
+
synchronize do
|
52
|
+
@threaded_callback.shutdown
|
53
|
+
unsubscribe_without_threaded_callback
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def callable_with_threaded_callback_wrapper(*args)
|
58
|
+
synchronize do
|
59
|
+
@threaded_callback ||= ThreadedCallback.new(@callable)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
data/lib/zk/version.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
A Day at the Races
|
2
|
+
|
3
|
+
* What are race conditions?
|
4
|
+
* Simple example with changing a node
|
5
|
+
* Simple example with a node being deleted
|
6
|
+
* What pattern do we use in ZK to manage this problem?
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
def block_until_node_deleted(zk, abs_node_path)
|
10
|
+
node_deletion_cb = lambda do |event|
|
11
|
+
if event.node_deleted?
|
12
|
+
queue.enq(:deleted)
|
13
|
+
else
|
14
|
+
queue.enq(:deleted) unless exists?(abs_node_path, :watch => true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
subs << event_handler.register(abs_node_path, &node_deletion_cb)
|
19
|
+
```
|
20
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
overall subject of voting is '/blah'
|
2
|
+
|
3
|
+
all nodes denote presence create('/blah/nodes/id', '', :ephemeral => true)
|
4
|
+
|
5
|
+
coordinator notes children('/blah/nodes/id', watch: true)
|
6
|
+
|
7
|
+
if any presence nodes are deleted, the transaction is aborted
|
8
|
+
|
9
|
+
nodes children('/blah/tx', watch: true)
|
10
|
+
|
11
|
+
coordinator voteseq = create('/blah/tx/vote', 'proposal', sequential: true) # beginning of election
|
12
|
+
|
13
|
+
coordinator children('/blah/tx/voteseq', watch: true) # loop until all nodes checked in
|
14
|
+
|
15
|
+
nodes children('/blah/tx')
|
16
|
+
nodes children('/blah/tx/voteseq', watch: true) # all nodes voted, then commit
|
17
|
+
nodes get('/blah/tx/voteseq', watch: true) # if data == 'abort' then delete id node
|
18
|
+
nodes get('/blah/tx/votes_uuid/result' watch: true) # result will be placed here?
|
19
|
+
nodes create('/blah/tx/voteseq/id', up_or_down, ephemeral: true) # any node votes down, abort
|
20
|
+
|
21
|
+
# coordinator sees all votes have been cast
|
22
|
+
|
23
|
+
# NOTE: is this step necessary? the coordinator confirming?
|
24
|
+
|
25
|
+
coordinator create('/blah/tx/voteseq/result', up_or_down, ephemeral: true)
|
26
|
+
|
27
|
+
# nodes tear down
|
28
|
+
|
29
|
+
node delete('/blah/tx/voteseq/id')
|
30
|
+
|
31
|
+
# when just result node is left
|
32
|
+
|
33
|
+
coordinator rm_rf('/blah/tx/voteseq')
|
34
|
+
|
35
|
+
|
36
|
+
# at many points we need a timer to see if things are taking too long
|
37
|
+
|
38
|
+
|
39
|
+
|
data/spec/event_catcher_spec.rb
CHANGED
@@ -4,19 +4,22 @@ require 'spec_helper'
|
|
4
4
|
describe EventCatcher do
|
5
5
|
subject { EventCatcher.new }
|
6
6
|
|
7
|
+
let!(:latch) { Latch.new }
|
8
|
+
|
7
9
|
describe :wait_for do
|
8
10
|
it %[should wake when an event is delivered] do
|
11
|
+
|
9
12
|
th = Thread.new do
|
10
13
|
subject.synchronize do
|
11
14
|
logger.debug { "about to wait for created" }
|
15
|
+
latch.release
|
12
16
|
subject.wait_for_created
|
13
17
|
logger.debug { "woke up, created must have been delivered" }
|
14
18
|
end
|
15
19
|
true
|
16
20
|
end
|
17
21
|
|
18
|
-
|
19
|
-
Thread.pass until th.status == 'sleep'
|
22
|
+
latch.await
|
20
23
|
|
21
24
|
logger.debug { "th.status: #{th.status}" }
|
22
25
|
|
@@ -116,7 +116,7 @@ shared_examples_for 'client' do
|
|
116
116
|
end
|
117
117
|
|
118
118
|
it %[should return a Stat object] do
|
119
|
-
@zk.stat(@missing_path).should be_kind_of(
|
119
|
+
@zk.stat(@missing_path).should be_kind_of(Zookeeper::Stat)
|
120
120
|
end
|
121
121
|
|
122
122
|
it %[should return a stat that not exists?] do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
|
4
|
-
#
|
4
|
+
# not a constant so we don't pollute global namespace
|
5
|
+
release_ops_path = File.expand_path('../../releaseops/lib', __FILE__)
|
6
|
+
|
7
|
+
if File.exists?(release_ops_path)
|
8
|
+
require File.join(release_ops_path, 'releaseops')
|
9
|
+
ReleaseOps::SimpleCov.maybe_start
|
10
|
+
end
|
11
|
+
|
5
12
|
|
6
13
|
Bundler.require(:development, :test)
|
7
14
|
|
@@ -6,7 +6,7 @@ class EventCatcher
|
|
6
6
|
|
7
7
|
MEMBERS = [:created, :changed, :deleted, :child, :all]
|
8
8
|
|
9
|
-
attr_reader :events
|
9
|
+
attr_reader :events, :mutex
|
10
10
|
|
11
11
|
def initialize(*args)
|
12
12
|
@mutex = Monitor.new
|
@@ -64,15 +64,15 @@ class EventCatcher
|
|
64
64
|
|
65
65
|
# waits for an event group to not be empty (up to timeout sec)
|
66
66
|
def wait_for_#{name}(timeout=5)
|
67
|
-
|
67
|
+
wait_for(:#{name}, timeout)
|
68
68
|
end
|
69
69
|
|
70
|
-
def wait_while_#{name}
|
71
|
-
|
70
|
+
def wait_while_#{name}(&blk)
|
71
|
+
wait_while(:#{name}, &blk)
|
72
72
|
end
|
73
73
|
|
74
|
-
def wait_until_#{name}
|
75
|
-
|
74
|
+
def wait_until_#{name}(&blk)
|
75
|
+
wait_until(:#{name}, &blk)
|
76
76
|
end
|
77
77
|
EOS
|
78
78
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# the much fabled 'latch' that tenderlove and nahi were on about
|
2
|
+
|
3
|
+
class Latch
|
4
|
+
def initialize(count = 1)
|
5
|
+
@count = count
|
6
|
+
@mutex = Monitor.new
|
7
|
+
@cond = @mutex.new_cond
|
8
|
+
end
|
9
|
+
|
10
|
+
def release
|
11
|
+
@mutex.synchronize {
|
12
|
+
@count -= 1 if @count > 0
|
13
|
+
@cond.broadcast if @count.zero?
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def await
|
18
|
+
@mutex.synchronize {
|
19
|
+
@cond.wait_while { @count > 0 }
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
data/spec/support/logging.rb
CHANGED
@@ -21,7 +21,7 @@ module SpecGlobalLogger
|
|
21
21
|
# sets the log level to FATAL for the duration of the block
|
22
22
|
def mute_logger
|
23
23
|
orig_level, ZK.logger.level = ZK.logger.level, Logger::FATAL
|
24
|
-
orig_zk_level, Zookeeper.debug_level = Zookeeper.debug_level,
|
24
|
+
orig_zk_level, Zookeeper.debug_level = Zookeeper.debug_level, Zookeeper::Constants::ZOO_LOG_LEVEL_ERROR
|
25
25
|
yield
|
26
26
|
ensure
|
27
27
|
ZK.logger.level = orig_level
|
data/spec/watch_spec.rb
CHANGED
@@ -378,7 +378,7 @@ describe ZK do
|
|
378
378
|
m.should_receive(:zk=).with(any())
|
379
379
|
m.should_receive(:node_event?).and_return(false)
|
380
380
|
m.should_receive(:state_event?).and_return(true)
|
381
|
-
m.should_receive(:state).and_return(
|
381
|
+
m.should_receive(:state).and_return(Zookeeper::Constants::ZOO_CONNECTED_STATE)
|
382
382
|
end
|
383
383
|
end
|
384
384
|
end # registered listeners
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
shared_examples_for 'session death' do
|
4
4
|
def deliver_session_event_to(event_num, zk)
|
5
5
|
# jeez, Zookeeper callbacks are so frustratingly stupid
|
6
|
-
bogus_event =
|
6
|
+
bogus_event = Zookeeper::Callbacks::WatcherCallback.new
|
7
7
|
bogus_event.initialize_context(:type => -1, :state => event_num, :path => '', :context => 'bogustestevent')
|
8
8
|
# XXX: this is bad because we're in the wrong thread, but we'll fix this after the next Zookeeper release
|
9
9
|
zk.event_handler.process(bogus_event)
|
@@ -51,22 +51,22 @@ end # session death
|
|
51
51
|
shared_examples_for 'locking and session death' do
|
52
52
|
describe 'exceptional conditions' do
|
53
53
|
describe 'ZOO_EXPIRED_SESSION_STATE' do
|
54
|
-
let(:zoo_state) {
|
55
|
-
let(:zoo_error_class) {
|
54
|
+
let(:zoo_state) { Zookeeper::Constants::ZOO_EXPIRED_SESSION_STATE }
|
55
|
+
let(:zoo_error_class) { Zookeeper::Exceptions::SessionExpired }
|
56
56
|
|
57
57
|
it_behaves_like 'session death'
|
58
58
|
end
|
59
59
|
|
60
60
|
describe 'ZOO_CONNECTING_STATE' do
|
61
|
-
let(:zoo_state) {
|
62
|
-
let(:zoo_error_class) {
|
61
|
+
let(:zoo_state) { Zookeeper::Constants::ZOO_CONNECTING_STATE }
|
62
|
+
let(:zoo_error_class) { Zookeeper::Exceptions::NotConnected }
|
63
63
|
|
64
64
|
it_behaves_like 'session death'
|
65
65
|
end
|
66
66
|
|
67
67
|
describe 'ZOO_CLOSED_STATE' do
|
68
|
-
let(:zoo_state) {
|
69
|
-
let(:zoo_error_class) {
|
68
|
+
let(:zoo_state) { Zookeeper::Constants::ZOO_CLOSED_STATE }
|
69
|
+
let(:zoo_error_class) { Zookeeper::Exceptions::ConnectionClosed }
|
70
70
|
|
71
71
|
it_behaves_like 'session death'
|
72
72
|
end
|
data/spec/zk/locker_spec.rb
CHANGED
@@ -23,42 +23,42 @@ describe 'ZK::Client#locker' do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it "should be able to acquire the lock if no one else is locking it" do
|
26
|
-
@zk.locker(@path_to_lock).lock
|
26
|
+
@zk.locker(@path_to_lock).lock.should be_true
|
27
27
|
end
|
28
28
|
|
29
29
|
it "should not be able to acquire the lock if someone else is locking it" do
|
30
|
-
@zk.locker(@path_to_lock).lock
|
31
|
-
@zk2.locker(@path_to_lock).lock
|
30
|
+
@zk.locker(@path_to_lock).lock.should be_true
|
31
|
+
@zk2.locker(@path_to_lock).lock.should be_false
|
32
32
|
end
|
33
33
|
|
34
34
|
it "should be able to acquire the lock after the first one releases it" do
|
35
35
|
lock1 = @zk.locker(@path_to_lock)
|
36
36
|
lock2 = @zk2.locker(@path_to_lock)
|
37
37
|
|
38
|
-
lock1.lock
|
39
|
-
lock2.lock
|
40
|
-
lock1.unlock
|
41
|
-
lock2.lock
|
38
|
+
lock1.lock.should be_true
|
39
|
+
lock2.lock.should be_false
|
40
|
+
lock1.unlock
|
41
|
+
lock2.lock.should be_true
|
42
42
|
end
|
43
43
|
|
44
44
|
it "should be able to acquire the lock if the first locker goes away" do
|
45
45
|
lock1 = @zk.locker(@path_to_lock)
|
46
46
|
lock2 = @zk2.locker(@path_to_lock)
|
47
47
|
|
48
|
-
lock1.lock
|
49
|
-
lock2.lock
|
48
|
+
lock1.lock.should be_true
|
49
|
+
lock2.lock.should be_false
|
50
50
|
@zk.close!
|
51
|
-
lock2.lock
|
51
|
+
lock2.lock.should be_true
|
52
52
|
end
|
53
53
|
|
54
54
|
it "should be able to handle multi part path locks" do
|
55
|
-
@zk.locker("my/multi/part/path").lock
|
55
|
+
@zk.locker("my/multi/part/path").lock.should be_true
|
56
56
|
end
|
57
57
|
|
58
58
|
it "should blocking lock" do
|
59
59
|
array = []
|
60
60
|
first_lock = @zk.locker("mylock")
|
61
|
-
first_lock.lock
|
61
|
+
first_lock.lock.should be_true
|
62
62
|
array << :first_lock
|
63
63
|
|
64
64
|
thread = Thread.new do
|
@@ -69,7 +69,7 @@ describe 'ZK::Client#locker' do
|
|
69
69
|
end
|
70
70
|
|
71
71
|
array.length.should == 1
|
72
|
-
first_lock.unlock
|
72
|
+
first_lock.unlock
|
73
73
|
thread.join(10)
|
74
74
|
array.length.should == 2
|
75
75
|
end
|
@@ -77,26 +77,87 @@ describe 'ZK::Client#locker' do
|
|
77
77
|
end
|
78
78
|
|
79
79
|
shared_examples_for 'SharedLocker' do
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
let(:shared_locker) { ZK::Locker.shared_locker(zk, path) }
|
81
|
+
let(:shared_locker2) { ZK::Locker.shared_locker(zk2, path) }
|
82
|
+
|
83
|
+
describe :assert! do
|
84
|
+
it %[should raise LockAssertionFailedError if its connection is no longer connected?] do
|
85
|
+
zk.close!
|
86
|
+
lambda { shared_locker.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
87
|
+
end
|
88
|
+
|
89
|
+
it %[should raise LockAssertionFailedError if locked? is false] do
|
90
|
+
shared_locker.should_not be_locked
|
91
|
+
lambda { shared_locker.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
92
|
+
end
|
93
|
+
|
94
|
+
it %[should raise LockAssertionFailedError lock_path does not exist] do
|
95
|
+
shared_locker.lock
|
96
|
+
lambda { shared_locker.assert! }.should_not raise_error
|
97
|
+
|
98
|
+
zk.delete(shared_locker.lock_path)
|
99
|
+
lambda { shared_locker.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
100
|
+
end
|
101
|
+
|
102
|
+
it %[should raise LockAssertionFailedError if there is an exclusive lock with a number lower than ours] do
|
103
|
+
# this should *really* never happen
|
104
|
+
shared_locker.lock.should be_true
|
105
|
+
shl_path = shared_locker.lock_path
|
106
|
+
|
107
|
+
shared_locker2.lock.should be_true
|
108
|
+
|
109
|
+
shared_locker.unlock.should be_true
|
110
|
+
shared_locker.should_not be_locked
|
111
|
+
|
112
|
+
zk.exists?(shl_path).should be_false
|
113
|
+
|
114
|
+
shared_locker2.lock_path.should_not == shl_path
|
115
|
+
|
116
|
+
# convert the first shared lock path into a exclusive one
|
117
|
+
|
118
|
+
exl_path = shl_path.sub(%r%/sh(\d+)\Z%, '/ex\1')
|
119
|
+
|
120
|
+
zk.create(exl_path, :ephemeral => true)
|
121
|
+
|
122
|
+
lambda { shared_locker2.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe :acquirable? do
|
127
|
+
describe %[with default options] do
|
128
|
+
it %[should work if the lock root doesn't exist] do
|
129
|
+
zk.rm_rf(ZK::Locker.default_root_lock_node)
|
130
|
+
shared_locker.should be_acquirable
|
131
|
+
end
|
132
|
+
|
133
|
+
it %[should check local state of lockedness] do
|
134
|
+
shared_locker.lock.should be_true
|
135
|
+
shared_locker.should be_acquirable
|
136
|
+
end
|
137
|
+
|
138
|
+
it %[should check if any participants would prevent us from acquiring the lock] do
|
139
|
+
ex_lock = ZK::Locker.exclusive_locker(zk, path)
|
140
|
+
ex_lock.lock.should be_true
|
141
|
+
shared_locker.should_not be_acquirable
|
142
|
+
end
|
143
|
+
end
|
83
144
|
end
|
84
145
|
|
85
|
-
describe :lock
|
146
|
+
describe :lock do
|
86
147
|
describe 'non-blocking success' do
|
87
148
|
before do
|
88
|
-
@rval =
|
89
|
-
@rval2 =
|
149
|
+
@rval = shared_locker.lock
|
150
|
+
@rval2 = shared_locker2.lock
|
90
151
|
end
|
91
152
|
|
92
153
|
it %[should acquire the first lock] do
|
93
154
|
@rval.should be_true
|
94
|
-
|
155
|
+
shared_locker.should be_locked
|
95
156
|
end
|
96
157
|
|
97
158
|
it %[should acquire the second lock] do
|
98
159
|
@rval2.should be_true
|
99
|
-
|
160
|
+
shared_locker2.should be_locked
|
100
161
|
end
|
101
162
|
end
|
102
163
|
|
@@ -104,11 +165,7 @@ shared_examples_for 'SharedLocker' do
|
|
104
165
|
before do
|
105
166
|
zk.mkdir_p(root_lock_path)
|
106
167
|
@write_lock_path = zk.create("#{root_lock_path}/#{ZK::Locker::EXCLUSIVE_LOCK_PREFIX}", '', :mode => :ephemeral_sequential)
|
107
|
-
@rval =
|
108
|
-
end
|
109
|
-
|
110
|
-
after do
|
111
|
-
zk.rm_rf('/_zklocking')
|
168
|
+
@rval = shared_locker.lock
|
112
169
|
end
|
113
170
|
|
114
171
|
it %[should return false] do
|
@@ -116,7 +173,7 @@ shared_examples_for 'SharedLocker' do
|
|
116
173
|
end
|
117
174
|
|
118
175
|
it %[should not be locked] do
|
119
|
-
|
176
|
+
shared_locker.should_not be_locked
|
120
177
|
end
|
121
178
|
end
|
122
179
|
|
@@ -130,15 +187,15 @@ shared_examples_for 'SharedLocker' do
|
|
130
187
|
it %[should acquire the lock after the write lock is released] do
|
131
188
|
ary = []
|
132
189
|
|
133
|
-
|
190
|
+
shared_locker.lock.should be_false
|
134
191
|
|
135
192
|
th = Thread.new do
|
136
|
-
|
193
|
+
shared_locker.lock(true)
|
137
194
|
ary << :locked
|
138
195
|
end
|
139
196
|
|
140
197
|
ary.should be_empty
|
141
|
-
|
198
|
+
shared_locker.should_not be_locked
|
142
199
|
|
143
200
|
zk.delete(@write_lock_path)
|
144
201
|
|
@@ -147,23 +204,82 @@ shared_examples_for 'SharedLocker' do
|
|
147
204
|
wait_until(2) { !ary.empty? }
|
148
205
|
ary.length.should == 1
|
149
206
|
|
150
|
-
|
207
|
+
shared_locker.should be_locked
|
151
208
|
end
|
152
209
|
end
|
153
210
|
end
|
154
211
|
end # SharedLocker
|
155
212
|
|
156
213
|
shared_examples_for 'ExclusiveLocker' do
|
157
|
-
|
158
|
-
|
159
|
-
|
214
|
+
let(:ex_locker) { ZK::Locker.exclusive_locker(zk, path) }
|
215
|
+
let(:ex_locker2) { ZK::Locker.exclusive_locker(zk2, path) }
|
216
|
+
|
217
|
+
describe :assert! do
|
218
|
+
it %[should raise LockAssertionFailedError if its connection is no longer connected?] do
|
219
|
+
zk.close!
|
220
|
+
lambda { ex_locker.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
221
|
+
end
|
222
|
+
|
223
|
+
it %[should raise LockAssertionFailedError if locked? is false] do
|
224
|
+
ex_locker.should_not be_locked
|
225
|
+
lambda { ex_locker.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
226
|
+
end
|
227
|
+
|
228
|
+
it %[should raise LockAssertionFailedError lock_path does not exist] do
|
229
|
+
ex_locker.lock
|
230
|
+
lambda { ex_locker.assert! }.should_not raise_error
|
231
|
+
|
232
|
+
zk.delete(ex_locker.lock_path)
|
233
|
+
lambda { ex_locker.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
234
|
+
end
|
235
|
+
|
236
|
+
it %[should raise LockAssertionFailedError if there is an exclusive lock with a number lower than ours] do
|
237
|
+
# this should *really* never happen
|
238
|
+
ex_locker.lock.should be_true
|
239
|
+
exl_path = ex_locker.lock_path
|
240
|
+
|
241
|
+
th = Thread.new do
|
242
|
+
ex_locker2.lock(true)
|
243
|
+
end
|
244
|
+
|
245
|
+
wait_until { th.status == 'sleep' }
|
246
|
+
|
247
|
+
ex_locker.unlock.should be_true
|
248
|
+
ex_locker.should_not be_locked
|
249
|
+
zk.exists?(exl_path).should be_false
|
250
|
+
|
251
|
+
th.join(5).should == th
|
252
|
+
|
253
|
+
ex_locker2.lock_path.should_not == exl_path
|
254
|
+
|
255
|
+
zk.create(exl_path, :ephemeral => true)
|
256
|
+
|
257
|
+
lambda { ex_locker2.assert! }.should raise_error(ZK::Exceptions::LockAssertionFailedError)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
describe :acquirable? do
|
262
|
+
it %[should work if the lock root doesn't exist] do
|
263
|
+
zk.rm_rf(ZK::Locker.default_root_lock_node)
|
264
|
+
ex_locker.should be_acquirable
|
265
|
+
end
|
266
|
+
|
267
|
+
it %[should check local state of lockedness] do
|
268
|
+
ex_locker.lock.should be_true
|
269
|
+
ex_locker.should be_acquirable
|
270
|
+
end
|
271
|
+
|
272
|
+
it %[should check if any participants would prevent us from acquiring the lock] do
|
273
|
+
ex_locker.lock.should be_true
|
274
|
+
ex_locker2.should_not be_acquirable
|
275
|
+
end
|
160
276
|
end
|
161
277
|
|
162
|
-
describe :lock
|
278
|
+
describe :lock do
|
163
279
|
describe 'non-blocking' do
|
164
280
|
before do
|
165
|
-
@rval =
|
166
|
-
@rval2 =
|
281
|
+
@rval = ex_locker.lock
|
282
|
+
@rval2 = ex_locker2.lock
|
167
283
|
end
|
168
284
|
|
169
285
|
it %[should acquire the first lock] do
|
@@ -175,8 +291,8 @@ shared_examples_for 'ExclusiveLocker' do
|
|
175
291
|
end
|
176
292
|
|
177
293
|
it %[should acquire the second lock after the first lock is released] do
|
178
|
-
|
179
|
-
|
294
|
+
ex_locker.unlock.should be_true
|
295
|
+
ex_locker2.lock.should be_true
|
180
296
|
end
|
181
297
|
end
|
182
298
|
|
@@ -189,41 +305,39 @@ shared_examples_for 'ExclusiveLocker' do
|
|
189
305
|
it %[should block waiting for the lock] do
|
190
306
|
ary = []
|
191
307
|
|
192
|
-
|
308
|
+
ex_locker.lock.should be_false
|
193
309
|
|
194
310
|
th = Thread.new do
|
195
|
-
|
311
|
+
ex_locker.lock(true)
|
196
312
|
ary << :locked
|
197
313
|
end
|
198
314
|
|
199
315
|
th.run
|
200
316
|
|
201
317
|
ary.should be_empty
|
202
|
-
|
318
|
+
ex_locker.should_not be_locked
|
203
319
|
|
204
320
|
zk.delete(@read_lock_path)
|
205
321
|
|
206
322
|
th.join(2)
|
207
323
|
|
208
324
|
ary.length.should == 1
|
209
|
-
|
325
|
+
ex_locker.should be_locked
|
210
326
|
end
|
211
327
|
end
|
212
|
-
|
213
328
|
end
|
214
329
|
end # ExclusiveLocker
|
215
330
|
|
216
331
|
shared_examples_for 'shared-exclusive interaction' do
|
217
332
|
before do
|
218
|
-
zk.rm_rf('/_zklocking')
|
219
333
|
@sh_lock = ZK::Locker.shared_locker(zk, path)
|
220
334
|
@ex_lock = ZK::Locker.exclusive_locker(zk2, path)
|
221
335
|
end
|
222
336
|
|
223
337
|
describe 'shared lock acquired first' do
|
224
338
|
it %[should block exclusive locks from acquiring until released] do
|
225
|
-
@sh_lock.lock
|
226
|
-
@ex_lock.lock
|
339
|
+
@sh_lock.lock.should be_true
|
340
|
+
@ex_lock.lock.should be_false
|
227
341
|
|
228
342
|
mutex = Monitor.new
|
229
343
|
cond = mutex.new_cond
|
@@ -242,7 +356,7 @@ shared_examples_for 'shared-exclusive interaction' do
|
|
242
356
|
|
243
357
|
mutex.synchronize do
|
244
358
|
logger.debug { "unlocking the shared lock" }
|
245
|
-
@sh_lock.unlock
|
359
|
+
@sh_lock.unlock.should be_true
|
246
360
|
cond.wait_until { th[:got_lock] } # make sure they actually received the lock (avoid race)
|
247
361
|
th[:got_lock].should be_true
|
248
362
|
logger.debug { "ok, they got the lock" }
|
@@ -258,8 +372,8 @@ shared_examples_for 'shared-exclusive interaction' do
|
|
258
372
|
|
259
373
|
describe 'exclusive lock acquired first' do
|
260
374
|
it %[should block shared lock from acquiring until released] do
|
261
|
-
@ex_lock.lock
|
262
|
-
@sh_lock.lock
|
375
|
+
@ex_lock.lock.should be_true
|
376
|
+
@sh_lock.lock.should be_false
|
263
377
|
|
264
378
|
mutex = Monitor.new
|
265
379
|
cond = mutex.new_cond
|
@@ -278,7 +392,7 @@ shared_examples_for 'shared-exclusive interaction' do
|
|
278
392
|
|
279
393
|
mutex.synchronize do
|
280
394
|
logger.debug { "unlocking the shared lock" }
|
281
|
-
@ex_lock.unlock
|
395
|
+
@ex_lock.unlock.should be_true
|
282
396
|
cond.wait_until { th[:got_lock] } # make sure they actually received the lock (avoid race)
|
283
397
|
th[:got_lock].should be_true
|
284
398
|
logger.debug { "ok, they got the lock" }
|
@@ -301,16 +415,16 @@ shared_examples_for 'shared-exclusive interaction' do
|
|
301
415
|
it %[should act something like a queue] do
|
302
416
|
@array = []
|
303
417
|
|
304
|
-
@sh_lock.lock
|
418
|
+
@sh_lock.lock.should be_true
|
305
419
|
@sh_lock.should be_locked
|
306
420
|
|
307
421
|
ex_th = Thread.new do
|
308
422
|
begin
|
309
|
-
@ex_lock.lock
|
423
|
+
@ex_lock.lock(true) # blocking lock
|
310
424
|
Thread.current[:got_lock] = true
|
311
425
|
@array << :ex_lock
|
312
426
|
ensure
|
313
|
-
@ex_lock.unlock
|
427
|
+
@ex_lock.unlock
|
314
428
|
end
|
315
429
|
end
|
316
430
|
|
@@ -320,22 +434,22 @@ shared_examples_for 'shared-exclusive interaction' do
|
|
320
434
|
|
321
435
|
# this is the important one, does the second shared lock get blocked by
|
322
436
|
# the exclusive lock
|
323
|
-
@sh_lock2.lock
|
437
|
+
@sh_lock2.lock.should_not be_true
|
324
438
|
|
325
439
|
sh2_th = Thread.new do
|
326
440
|
begin
|
327
|
-
@sh_lock2.lock
|
441
|
+
@sh_lock2.lock(true)
|
328
442
|
Thread.current[:got_lock] = true
|
329
443
|
@array << :sh_lock2
|
330
444
|
ensure
|
331
|
-
@sh_lock2.unlock
|
445
|
+
@sh_lock2.unlock
|
332
446
|
end
|
333
447
|
end
|
334
448
|
|
335
449
|
sh2_th.join_until { @sh_lock2.waiting? }
|
336
450
|
@sh_lock2.should be_waiting
|
337
451
|
|
338
|
-
@sh_lock.unlock
|
452
|
+
@sh_lock.unlock.should be_true
|
339
453
|
|
340
454
|
ex_th.join_until { ex_th[:got_lock] }
|
341
455
|
ex_th[:got_lock].should be_true
|
@@ -349,7 +463,6 @@ shared_examples_for 'shared-exclusive interaction' do
|
|
349
463
|
end
|
350
464
|
end # shared-exclusive interaction
|
351
465
|
|
352
|
-
|
353
466
|
describe ZK::Locker do
|
354
467
|
include_context 'connection opts'
|
355
468
|
|
@@ -360,15 +473,17 @@ describe ZK::Locker do
|
|
360
473
|
let(:connections) { [zk, zk2, zk3] }
|
361
474
|
|
362
475
|
let(:path) { "shlock" }
|
363
|
-
let(:root_lock_path) { "
|
476
|
+
let(:root_lock_path) { "#{ZK::Locker.default_root_lock_node}/#{path}" }
|
364
477
|
|
365
478
|
before do
|
366
479
|
wait_until{ connections.all?(&:connected?) }
|
480
|
+
zk.rm_rf(ZK::Locker.default_root_lock_node)
|
367
481
|
end
|
368
482
|
|
369
483
|
after do
|
370
484
|
connections.each { |c| c.close! }
|
371
485
|
wait_until { !connections.any?(&:connected?) }
|
486
|
+
ZK.open("localhost:#{ZK.test_port}") { |z| z.rm_rf(ZK::Locker.default_root_lock_node) }
|
372
487
|
end
|
373
488
|
|
374
489
|
it_should_behave_like 'SharedLocker'
|
@@ -376,7 +491,7 @@ describe ZK::Locker do
|
|
376
491
|
it_should_behave_like 'shared-exclusive interaction'
|
377
492
|
end # ZK::Locker
|
378
493
|
|
379
|
-
describe
|
494
|
+
describe ZK::Locker, :chrooted => true do
|
380
495
|
include_context 'connection opts'
|
381
496
|
|
382
497
|
let(:chroot_path) { '/_zk_chroot_' }
|
@@ -390,7 +505,7 @@ describe "ZK::Locker chrooted" do
|
|
390
505
|
let(:connections) { [zk, zk2, zk3] }
|
391
506
|
|
392
507
|
let(:path) { "shlock" }
|
393
|
-
let(:root_lock_path) { "
|
508
|
+
let(:root_lock_path) { "#{ZK::Locker.default_root_lock_node}/#{path}" }
|
394
509
|
|
395
510
|
before do
|
396
511
|
ZK.open("localhost:#{ZK.test_port}") do |zk|
|