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.
Files changed (49) hide show
  1. data/.dotfiles/ctags_paths +1 -0
  2. data/.dotfiles/rspec-logging +2 -2
  3. data/.gitignore +1 -0
  4. data/Gemfile +9 -3
  5. data/Guardfile +36 -0
  6. data/README.markdown +21 -18
  7. data/RELEASES.markdown +10 -0
  8. data/Rakefile +1 -1
  9. data/lib/zk.rb +28 -21
  10. data/lib/zk/client/threaded.rb +107 -17
  11. data/lib/zk/client/unixisms.rb +1 -41
  12. data/lib/zk/core_ext.rb +28 -0
  13. data/lib/zk/election.rb +14 -3
  14. data/lib/zk/event_handler.rb +36 -37
  15. data/lib/zk/event_handler_subscription/actor.rb +37 -2
  16. data/lib/zk/event_handler_subscription/base.rb +9 -0
  17. data/lib/zk/exceptions.rb +5 -0
  18. data/lib/zk/fork_hook.rb +112 -0
  19. data/lib/zk/install_fork_hooks.rb +37 -0
  20. data/lib/zk/locker/exclusive_locker.rb +14 -10
  21. data/lib/zk/locker/locker_base.rb +43 -26
  22. data/lib/zk/locker/shared_locker.rb +9 -5
  23. data/lib/zk/logging.rb +29 -7
  24. data/lib/zk/node_deletion_watcher.rb +167 -0
  25. data/lib/zk/pool.rb +14 -4
  26. data/lib/zk/subscription.rb +15 -34
  27. data/lib/zk/threaded_callback.rb +113 -29
  28. data/lib/zk/threadpool.rb +136 -40
  29. data/lib/zk/version.rb +1 -1
  30. data/spec/logging_progress_bar_formatter.rb +12 -0
  31. data/spec/shared/client_contexts.rb +13 -1
  32. data/spec/shared/client_examples.rb +3 -1
  33. data/spec/spec_helper.rb +28 -3
  34. data/spec/support/client_forker.rb +49 -8
  35. data/spec/support/latch.rb +1 -19
  36. data/spec/support/logging.rb +26 -10
  37. data/spec/support/wait_watchers.rb +2 -2
  38. data/spec/zk/00_forked_client_integration_spec.rb +1 -1
  39. data/spec/zk/client_spec.rb +11 -2
  40. data/spec/zk/election_spec.rb +21 -7
  41. data/spec/zk/locker_spec.rb +42 -22
  42. data/spec/zk/node_deletion_watcher_spec.rb +69 -0
  43. data/spec/zk/pool_spec.rb +32 -18
  44. data/spec/zk/threaded_callback_spec.rb +78 -0
  45. data/spec/zk/threadpool_spec.rb +52 -0
  46. data/spec/zk/watch_spec.rb +4 -0
  47. data/zk.gemspec +2 -1
  48. metadata +36 -10
  49. 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
- in_waiting_status do
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 = [root_lock_path, next_lowest_node].join('/')
78
- logger.debug { "SharedLocker#block_until_write_lock! path=#{path.inspect}" }
79
- @zk.block_until_node_deleted(path)
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?(check_if_any=false)
95
- false|@locked
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
- if @locked
123
- cleanup_lock_path!
124
- @locked = false
125
- true
126
- else
127
- false # i know, i know, but be explicit
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
- false|@waiting
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
- raise LockAssertionFailedError, "have not obtained the lock yet" unless locked?
200
- raise LockAssertionFailedError, "not connected" unless zk.connected?
201
- raise LockAssertionFailedError, "lock_path was #{lock_path.inspect}" unless lock_path
202
- raise LockAssertionFailedError, "the lock path #{lock_path} did not exist!" unless zk.exists?(lock_path)
203
- raise LockAssertionFailedError, "we do not actually hold the lock" unless got_lock?
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 in_waiting_status
208
- w, @waiting = @waiting, true
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
- @lock_path = @zk.create("#{root_lock_path}/#{prefix}", "", :mode => :ephemeral_sequential)
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
- begin
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
- in_waiting_status do
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
- @zk.block_until_node_deleted(path)
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
@@ -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
- def self.included(mod)
4
- mod.extend(ZK::Logging::Methods)
5
- mod.send(:include, ZK::Logging::Methods)
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
- module Methods
9
- def logger
10
- ZK.logger
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
+