zk 1.4.2 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
+