zk 1.4.2 → 1.5.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/.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
|
+
|