zk 1.4.2 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.dotfiles/ctags_paths +1 -0
- data/.dotfiles/rspec-logging +2 -2
- data/.gitignore +1 -0
- data/Gemfile +9 -3
- data/Guardfile +36 -0
- data/README.markdown +21 -18
- data/RELEASES.markdown +10 -0
- data/Rakefile +1 -1
- data/lib/zk.rb +28 -21
- data/lib/zk/client/threaded.rb +107 -17
- data/lib/zk/client/unixisms.rb +1 -41
- data/lib/zk/core_ext.rb +28 -0
- data/lib/zk/election.rb +14 -3
- data/lib/zk/event_handler.rb +36 -37
- data/lib/zk/event_handler_subscription/actor.rb +37 -2
- data/lib/zk/event_handler_subscription/base.rb +9 -0
- data/lib/zk/exceptions.rb +5 -0
- data/lib/zk/fork_hook.rb +112 -0
- data/lib/zk/install_fork_hooks.rb +37 -0
- data/lib/zk/locker/exclusive_locker.rb +14 -10
- data/lib/zk/locker/locker_base.rb +43 -26
- data/lib/zk/locker/shared_locker.rb +9 -5
- data/lib/zk/logging.rb +29 -7
- data/lib/zk/node_deletion_watcher.rb +167 -0
- data/lib/zk/pool.rb +14 -4
- data/lib/zk/subscription.rb +15 -34
- data/lib/zk/threaded_callback.rb +113 -29
- data/lib/zk/threadpool.rb +136 -40
- data/lib/zk/version.rb +1 -1
- data/spec/logging_progress_bar_formatter.rb +12 -0
- data/spec/shared/client_contexts.rb +13 -1
- data/spec/shared/client_examples.rb +3 -1
- data/spec/spec_helper.rb +28 -3
- data/spec/support/client_forker.rb +49 -8
- data/spec/support/latch.rb +1 -19
- data/spec/support/logging.rb +26 -10
- data/spec/support/wait_watchers.rb +2 -2
- data/spec/zk/00_forked_client_integration_spec.rb +1 -1
- data/spec/zk/client_spec.rb +11 -2
- data/spec/zk/election_spec.rb +21 -7
- data/spec/zk/locker_spec.rb +42 -22
- data/spec/zk/node_deletion_watcher_spec.rb +69 -0
- data/spec/zk/pool_spec.rb +32 -18
- data/spec/zk/threaded_callback_spec.rb +78 -0
- data/spec/zk/threadpool_spec.rb +52 -0
- data/spec/zk/watch_spec.rb +4 -0
- data/zk.gemspec +2 -1
- metadata +36 -10
- data/spec/support/logging_progress_bar_formatter.rb +0 -14
@@ -0,0 +1,37 @@
|
|
1
|
+
module ::Kernel
|
2
|
+
def fork_with_zk_hooks(&block)
|
3
|
+
if block
|
4
|
+
new_block = proc do
|
5
|
+
::ZK::ForkHook.fire_after_child_hooks!
|
6
|
+
block.call
|
7
|
+
end
|
8
|
+
|
9
|
+
::ZK::ForkHook.fire_prepare_hooks!
|
10
|
+
fork_without_zk_hooks(&new_block).tap do
|
11
|
+
::ZK::ForkHook.fire_after_parent_hooks!
|
12
|
+
end
|
13
|
+
else
|
14
|
+
::ZK::ForkHook.fire_prepare_hooks!
|
15
|
+
if pid = fork_without_zk_hooks
|
16
|
+
::ZK::ForkHook.fire_after_parent_hooks!
|
17
|
+
# we're in the parent
|
18
|
+
return pid
|
19
|
+
else
|
20
|
+
# we're in the child
|
21
|
+
::ZK::ForkHook.fire_after_child_hooks!
|
22
|
+
return nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
module_function :fork_with_zk_hooks
|
27
|
+
|
28
|
+
if defined?(fork_without_zk_hooks)
|
29
|
+
remove_method :fork
|
30
|
+
alias fork fork_without_zk_hooks
|
31
|
+
remove_method :fork_without_zk_hooks
|
32
|
+
end
|
33
|
+
|
34
|
+
alias fork_without_zk_hooks fork
|
35
|
+
alias fork fork_with_zk_hooks
|
36
|
+
end
|
37
|
+
|
@@ -18,15 +18,13 @@ module ZK
|
|
18
18
|
# obtain an exclusive lock.
|
19
19
|
#
|
20
20
|
def lock(blocking=false)
|
21
|
-
return true if @locked
|
21
|
+
return true if synchronize { @locked }
|
22
22
|
create_lock_path!(EXCLUSIVE_LOCK_PREFIX)
|
23
23
|
|
24
24
|
if got_write_lock?
|
25
|
-
@locked = true
|
25
|
+
synchronize { @locked = true }
|
26
26
|
elsif blocking
|
27
|
-
|
28
|
-
block_until_write_lock!
|
29
|
-
end
|
27
|
+
block_until_write_lock!
|
30
28
|
else
|
31
29
|
cleanup_lock_path!
|
32
30
|
false
|
@@ -51,7 +49,7 @@ module ZK
|
|
51
49
|
return true if locked?
|
52
50
|
stat = zk.stat(root_lock_path)
|
53
51
|
!stat.exists? or stat.num_children == 0
|
54
|
-
rescue Exceptions::NoNode
|
52
|
+
rescue Exceptions::NoNode # XXX: is this ever hit? stat shouldn't raise
|
55
53
|
true
|
56
54
|
end
|
57
55
|
|
@@ -74,13 +72,19 @@ module ZK
|
|
74
72
|
|
75
73
|
def block_until_write_lock!
|
76
74
|
begin
|
77
|
-
path =
|
78
|
-
logger.debug { "
|
79
|
-
|
75
|
+
path = "#{root_lock_path}/#{next_lowest_node}"
|
76
|
+
logger.debug { "#{self.class}##{__method__} path=#{path.inspect}" }
|
77
|
+
|
78
|
+
synchronize do
|
79
|
+
@node_deletion_watcher = NodeDeletionWatcher.new(zk, path)
|
80
|
+
@cond.broadcast
|
81
|
+
end
|
82
|
+
|
83
|
+
@node_deletion_watcher.block_until_deleted
|
80
84
|
rescue WeAreTheLowestLockNumberException
|
81
85
|
end
|
82
86
|
|
83
|
-
@locked = true
|
87
|
+
synchronize { @locked = true }
|
84
88
|
end
|
85
89
|
end # ExclusiveLocker
|
86
90
|
end # Locker
|
@@ -53,6 +53,9 @@ module ZK
|
|
53
53
|
@waiting = false
|
54
54
|
@lock_path = nil
|
55
55
|
@root_lock_path = "#{@root_lock_node}/#{@path.gsub("/", "__")}"
|
56
|
+
@mutex = Monitor.new
|
57
|
+
@cond = @mutex.new_cond
|
58
|
+
@node_deletion_watcher = nil
|
56
59
|
end
|
57
60
|
|
58
61
|
# block caller until lock is aquired, then yield
|
@@ -78,7 +81,7 @@ module ZK
|
|
78
81
|
# @return [nil] if lock_path is not set
|
79
82
|
# @return [String] last path component of our lock path
|
80
83
|
def lock_basename
|
81
|
-
lock_path and File.basename(lock_path)
|
84
|
+
synchronize { lock_path and File.basename(lock_path) }
|
82
85
|
end
|
83
86
|
|
84
87
|
# returns our current idea of whether or not we hold the lock, which does
|
@@ -91,8 +94,8 @@ module ZK
|
|
91
94
|
# @return [true] if we hold the lock
|
92
95
|
# @return [false] if we don't hold the lock
|
93
96
|
#
|
94
|
-
def locked?
|
95
|
-
|
97
|
+
def locked?
|
98
|
+
synchronize { !!@locked }
|
96
99
|
end
|
97
100
|
|
98
101
|
# * If this instance holds the lock {#locked? is true} we return true (as
|
@@ -119,12 +122,15 @@ module ZK
|
|
119
122
|
# @return [false] we did not own the lock
|
120
123
|
#
|
121
124
|
def unlock
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
125
|
+
synchronize do
|
126
|
+
if @locked
|
127
|
+
cleanup_lock_path!
|
128
|
+
@locked = false
|
129
|
+
@node_deletion_watcher = nil
|
130
|
+
true
|
131
|
+
else
|
132
|
+
false # i know, i know, but be explicit
|
133
|
+
end
|
128
134
|
end
|
129
135
|
end
|
130
136
|
|
@@ -161,10 +167,23 @@ module ZK
|
|
161
167
|
end
|
162
168
|
|
163
169
|
# returns true if this locker is waiting to acquire lock
|
170
|
+
# this should be used in tests only.
|
164
171
|
#
|
165
172
|
# @private
|
166
173
|
def waiting?
|
167
|
-
|
174
|
+
synchronize do
|
175
|
+
!!(@node_deletion_watcher and @node_deletion_watcher.blocked?)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# blocks the caller until this lock is blocked
|
180
|
+
# @private
|
181
|
+
def wait_until_blocked(timeout=nil)
|
182
|
+
synchronize do
|
183
|
+
@cond.wait_until { @node_deletion_watcher }
|
184
|
+
end
|
185
|
+
|
186
|
+
@node_deletion_watcher.wait_until_blocked(timeout)
|
168
187
|
end
|
169
188
|
|
170
189
|
# This is for users who wish to check that the assumption is correct
|
@@ -196,19 +215,18 @@ module ZK
|
|
196
215
|
# end
|
197
216
|
#
|
198
217
|
def assert!
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
218
|
+
synchronize do
|
219
|
+
raise LockAssertionFailedError, "have not obtained the lock yet" unless locked?
|
220
|
+
raise LockAssertionFailedError, "not connected" unless zk.connected?
|
221
|
+
raise LockAssertionFailedError, "lock_path was #{lock_path.inspect}" unless lock_path
|
222
|
+
raise LockAssertionFailedError, "the lock path #{lock_path} did not exist!" unless zk.exists?(lock_path)
|
223
|
+
raise LockAssertionFailedError, "we do not actually hold the lock" unless got_lock?
|
224
|
+
end
|
204
225
|
end
|
205
226
|
|
206
227
|
protected
|
207
|
-
def
|
208
|
-
|
209
|
-
yield
|
210
|
-
ensure
|
211
|
-
@waiting = w
|
228
|
+
def synchronize
|
229
|
+
@mutex.synchronize { yield }
|
212
230
|
end
|
213
231
|
|
214
232
|
def digit_from(path)
|
@@ -245,7 +263,10 @@ module ZK
|
|
245
263
|
# defaults to 'lock'
|
246
264
|
#
|
247
265
|
def create_lock_path!(prefix='lock')
|
248
|
-
|
266
|
+
synchronize do
|
267
|
+
@lock_path = @zk.create("#{root_lock_path}/#{prefix}", "", :mode => :ephemeral_sequential)
|
268
|
+
end
|
269
|
+
|
249
270
|
logger.debug { "got lock path #{@lock_path}" }
|
250
271
|
@lock_path
|
251
272
|
rescue NoNode
|
@@ -257,11 +278,7 @@ module ZK
|
|
257
278
|
logger.debug { "removing lock path #{@lock_path}" }
|
258
279
|
zk.delete(@lock_path)
|
259
280
|
|
260
|
-
|
261
|
-
zk.delete(root_lock_path)
|
262
|
-
rescue NotEmpty
|
263
|
-
end
|
264
|
-
|
281
|
+
zk.delete(root_lock_path, :ignore => :not_empty)
|
265
282
|
@lock_path = nil
|
266
283
|
end
|
267
284
|
end # LockerBase
|
@@ -13,9 +13,7 @@ module ZK
|
|
13
13
|
if got_read_lock?
|
14
14
|
@locked = true
|
15
15
|
elsif blocking
|
16
|
-
|
17
|
-
block_until_read_lock!
|
18
|
-
end
|
16
|
+
block_until_read_lock!
|
19
17
|
else
|
20
18
|
# we didn't get the lock, and we're not gonna wait around for it, so
|
21
19
|
# clean up after ourselves
|
@@ -100,13 +98,19 @@ module ZK
|
|
100
98
|
begin
|
101
99
|
path = "#{root_lock_path}/#{next_lowest_write_lock_name}"
|
102
100
|
logger.debug { "SharedLocker#block_until_read_lock! path=#{path.inspect}" }
|
103
|
-
|
101
|
+
|
102
|
+
synchronize do
|
103
|
+
@node_deletion_watcher = NodeDeletionWatcher.new(zk, path)
|
104
|
+
@cond.broadcast
|
105
|
+
end
|
106
|
+
|
107
|
+
@node_deletion_watcher.block_until_deleted
|
104
108
|
rescue NoWriteLockFoundException
|
105
109
|
# next_lowest_write_lock_name may raise NoWriteLockFoundException,
|
106
110
|
# which means we should not block as we have the lock (there is nothing to wait for)
|
107
111
|
end
|
108
112
|
|
109
|
-
@locked = true
|
113
|
+
synchronize { @locked = true }
|
110
114
|
end
|
111
115
|
end # SharedLocker
|
112
116
|
end # Locker
|
data/lib/zk/logging.rb
CHANGED
@@ -1,15 +1,37 @@
|
|
1
1
|
module ZK
|
2
|
+
# use the ZK.logger if non-nil (to allow users to override the logger)
|
3
|
+
# otherwise, use a Loggging logger based on the class name
|
2
4
|
module Logging
|
3
|
-
|
4
|
-
|
5
|
-
|
5
|
+
extend ZK::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
def self.logger
|
9
|
+
::ZK.logger || ::Logging.logger[logger_name]
|
10
|
+
end
|
6
11
|
end
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
12
|
+
|
13
|
+
def self.set_default
|
14
|
+
::Logging.logger['ZK'].tap do |ch_root|
|
15
|
+
::Logging.appenders.stderr.tap do |serr|
|
16
|
+
serr.layout = ::Logging.layouts.pattern(
|
17
|
+
:pattern => '%.1l, [%d] %c30.30{2}: %m\n',
|
18
|
+
:date_pattern => '%Y-%m-%d %H:%M:%S.%6N'
|
19
|
+
)
|
20
|
+
|
21
|
+
ch_root.add_appenders(serr)
|
22
|
+
end
|
23
|
+
|
24
|
+
ch_root.level = ENV['ZK_DEBUG'] ? :debug : :off
|
11
25
|
end
|
12
26
|
end
|
27
|
+
|
28
|
+
# cache the logger at the instance level, as that's where most of the
|
29
|
+
# logging is done, this means that the user should set up the override
|
30
|
+
# of the ZK.logger early, before creating instances.
|
31
|
+
#
|
32
|
+
def logger
|
33
|
+
@logger ||= (::ZK.logger || ::Logging.logger[self.class.logger_name]) # logger_name defined in ::Logging::Utils
|
34
|
+
end
|
13
35
|
end
|
14
36
|
end
|
15
37
|
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module ZK
|
2
|
+
class NodeDeletionWatcher
|
3
|
+
include Zookeeper::Constants
|
4
|
+
include Exceptions
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
# @private
|
8
|
+
module Constants
|
9
|
+
NOT_YET = :not_yet
|
10
|
+
BLOCKED = :yes
|
11
|
+
NOT_ANYMORE = :not_anymore
|
12
|
+
INTERRUPTED = :interrupted
|
13
|
+
end
|
14
|
+
include Constants
|
15
|
+
|
16
|
+
attr_reader :zk, :path
|
17
|
+
|
18
|
+
def initialize(zk, path)
|
19
|
+
@zk = zk
|
20
|
+
@path = path.dup
|
21
|
+
|
22
|
+
@subs = []
|
23
|
+
|
24
|
+
@mutex = Monitor.new # ffs, 1.8.7 compatibility w/ timeouts
|
25
|
+
@cond = @mutex.new_cond
|
26
|
+
|
27
|
+
@blocked = :not_yet
|
28
|
+
@result = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def done?
|
32
|
+
@mutex.synchronize { !!@result }
|
33
|
+
end
|
34
|
+
|
35
|
+
def blocked?
|
36
|
+
@mutex.synchronize { @blocked == BLOCKED }
|
37
|
+
end
|
38
|
+
|
39
|
+
# this is for testing, allows us to wait until this object has gone into
|
40
|
+
# blocking state.
|
41
|
+
#
|
42
|
+
# avoids the race where if we have already been blocked and released
|
43
|
+
# this will not block the caller
|
44
|
+
#
|
45
|
+
# pass optional timeout to return after that amount of time or nil to block
|
46
|
+
# forever
|
47
|
+
#
|
48
|
+
# @return [true] if we have been blocked previously or are currently blocked,
|
49
|
+
# @return [nil] if we timeout
|
50
|
+
#
|
51
|
+
def wait_until_blocked(timeout=nil)
|
52
|
+
@mutex.synchronize do
|
53
|
+
return true unless @blocked == NOT_YET
|
54
|
+
|
55
|
+
start = Time.now
|
56
|
+
time_to_stop = timeout ? (start + timeout) : nil
|
57
|
+
|
58
|
+
@cond.wait(timeout)
|
59
|
+
|
60
|
+
if (time_to_stop and (Time.now > time_to_stop)) and (@blocked == NOT_YET)
|
61
|
+
return nil
|
62
|
+
end
|
63
|
+
|
64
|
+
(@blocked == NOT_YET) ? nil : true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# cause a thread blocked us to be awakened and have a WakeUpException
|
69
|
+
# raised.
|
70
|
+
#
|
71
|
+
# if a result has already been delivered, then this does nothing
|
72
|
+
#
|
73
|
+
# if a result has not *yet* been delivered, any thread calling
|
74
|
+
# block_until_deleted will receive the exception immediately
|
75
|
+
#
|
76
|
+
def interrupt!
|
77
|
+
@mutex.synchronize do
|
78
|
+
case @blocked
|
79
|
+
when NOT_YET, BLOCKED
|
80
|
+
@result = INTERRUPTED
|
81
|
+
@cond.broadcast
|
82
|
+
else
|
83
|
+
return
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def block_until_deleted
|
89
|
+
@mutex.synchronize do
|
90
|
+
raise InvalidStateError, "Already fired for #{path}" if @result
|
91
|
+
register_callbacks
|
92
|
+
|
93
|
+
unless zk.exists?(path, :watch => true)
|
94
|
+
# we are done, these are one-shot, so write the results
|
95
|
+
@result = :deleted
|
96
|
+
@blocked = NOT_ANYMORE
|
97
|
+
@cond.broadcast # wake any waiting threads
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
|
101
|
+
logger.debug { "ok, going to block: #{path}" }
|
102
|
+
|
103
|
+
while true # this is probably unnecessary
|
104
|
+
@blocked = BLOCKED
|
105
|
+
@cond.broadcast # wake threads waiting for @blocked to change
|
106
|
+
@cond.wait_until { @result } # wait until we get a result
|
107
|
+
@blocked = NOT_ANYMORE
|
108
|
+
|
109
|
+
case @result
|
110
|
+
when :deleted
|
111
|
+
logger.debug { "path #{path} was deleted" }
|
112
|
+
return true
|
113
|
+
when INTERRUPTED
|
114
|
+
raise ZK::Exceptions::WakeUpException
|
115
|
+
when ZOO_EXPIRED_SESSION_STATE
|
116
|
+
raise Zookeeper::Exceptions::SessionExpired
|
117
|
+
when ZOO_CONNECTING_STATE
|
118
|
+
raise Zookeeper::Exceptions::NotConnected
|
119
|
+
when ZOO_CLOSED_STATE
|
120
|
+
raise Zookeeper::Exceptions::ConnectionClosed
|
121
|
+
else
|
122
|
+
raise "Hit unexpected case in block_until_node_deleted, result was: #{@result.inspect}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
ensure
|
127
|
+
unregister_callbacks
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def unregister_callbacks
|
132
|
+
@subs.each(&:unregister)
|
133
|
+
end
|
134
|
+
|
135
|
+
def register_callbacks
|
136
|
+
@subs << zk.register(path, &method(:node_deletion_cb))
|
137
|
+
|
138
|
+
[:expired_session, :connecting, :closed].each do |sym|
|
139
|
+
@subs << zk.event_handler.register_state_handler(sym, &method(:session_cb))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def node_deletion_cb(event)
|
144
|
+
@mutex.synchronize do
|
145
|
+
if event.node_deleted?
|
146
|
+
@result = :deleted
|
147
|
+
@cond.broadcast
|
148
|
+
else
|
149
|
+
unless zk.exists?(path, :watch => true)
|
150
|
+
@result = :deleted
|
151
|
+
@cond.broadcast
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def session_cb(event)
|
158
|
+
@mutex.synchronize do
|
159
|
+
unless @result
|
160
|
+
@result = event.state
|
161
|
+
@cond.broadcast
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|