zk 0.6.4

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.
@@ -0,0 +1,101 @@
1
+ module ZK
2
+ module Exceptions
3
+ OK = 0
4
+ # System and server-side errors
5
+ SYSTEMERROR = -1
6
+ RUNTIMEINCONSISTENCY = SYSTEMERROR - 1
7
+ DATAINCONSISTENCY = SYSTEMERROR - 2
8
+ CONNECTIONLOSS = SYSTEMERROR - 3
9
+ MARSHALLINGERROR = SYSTEMERROR - 4
10
+ UNIMPLEMENTED = SYSTEMERROR - 5
11
+ OPERATIONTIMEOUT = SYSTEMERROR - 6
12
+ BADARGUMENTS = SYSTEMERROR - 7
13
+ # API errors
14
+ APIERROR = -100;
15
+ NONODE = APIERROR - 1 # Node does not exist
16
+ NOAUTH = APIERROR - 2 # Current operation not permitted
17
+ BADVERSION = APIERROR - 3 # Version conflict
18
+ NOCHILDRENFOREPHEMERALS = APIERROR - 8
19
+ NODEEXISTS = APIERROR - 10
20
+ NOTEMPTY = APIERROR - 11
21
+ SESSIONEXPIRED = APIERROR - 12
22
+ INVALIDCALLBACK = APIERROR - 13
23
+ INVALIDACL = APIERROR - 14
24
+ AUTHFAILED = APIERROR - 15 # client authentication failed
25
+
26
+
27
+ # these errors are returned rather than the driver level errors
28
+ class KeeperException < StandardError
29
+ def self.recognized_code?(code)
30
+ ERROR_MAP.include?(code)
31
+ end
32
+
33
+ def self.by_code(code)
34
+ ERROR_MAP.fetch(code.to_i) { raise "API ERROR: no exception defined for code: #{code}" }
35
+ end
36
+ end
37
+
38
+ class SystemError < KeeperException; end
39
+ class RunTimeInconsistency < KeeperException; end
40
+ class DataInconsistency < KeeperException; end
41
+ class ConnectionLoss < KeeperException; end
42
+ class MarshallingError < KeeperException; end
43
+ class Unimplemented < KeeperException; end
44
+ class OperationTimeOut < KeeperException; end
45
+ class BadArguments < KeeperException; end
46
+ class ApiError < KeeperException; end
47
+ class NoNode < KeeperException; end
48
+ class NoAuth < KeeperException; end
49
+ class BadVersion < KeeperException; end
50
+ class NoChildrenForEphemerals < KeeperException; end
51
+ class NodeExists < KeeperException; end
52
+ class NotEmpty < KeeperException; end
53
+ class SessionExpired < KeeperException; end
54
+ class InvalidCallback < KeeperException; end
55
+ class InvalidACL < KeeperException; end
56
+ class AuthFailed < KeeperException; end
57
+
58
+ ERROR_MAP = {
59
+ SYSTEMERROR => SystemError,
60
+ RUNTIMEINCONSISTENCY => RunTimeInconsistency,
61
+ DATAINCONSISTENCY => DataInconsistency,
62
+ CONNECTIONLOSS => ConnectionLoss,
63
+ MARSHALLINGERROR => MarshallingError,
64
+ UNIMPLEMENTED => Unimplemented,
65
+ OPERATIONTIMEOUT => OperationTimeOut,
66
+ BADARGUMENTS => BadArguments,
67
+ APIERROR => ApiError,
68
+ NONODE => NoNode,
69
+ NOAUTH => NoAuth,
70
+ BADVERSION => BadVersion,
71
+ NOCHILDRENFOREPHEMERALS => NoChildrenForEphemerals,
72
+ NODEEXISTS => NodeExists,
73
+ NOTEMPTY => NotEmpty,
74
+ SESSIONEXPIRED => SessionExpired,
75
+ INVALIDCALLBACK => InvalidCallback,
76
+ INVALIDACL => InvalidACL,
77
+ AUTHFAILED => AuthFailed,
78
+ }
79
+
80
+ # base class of ZK generated errors (not driver-level errors)
81
+ class ZKError < StandardError; end
82
+
83
+ class LockFileNameParseError < ZKError; end
84
+
85
+ # raised when you try to vote twice in a given leader election
86
+ class ThisIsNotChicagoError < ZKError; end
87
+
88
+ # raised when close_all! has been called on a pool and some thread attempts a checkout
89
+ class PoolIsShuttingDownException < ZKError; end
90
+
91
+ # raised when defer is called on a threadpool that is not running
92
+ class ThreadpoolIsNotRunningException < ZKError; end
93
+
94
+ # raised when assert_locked_for_update! is called and no exclusive lock is held
95
+ class MustBeExclusivelyLockedException < ZKError; end
96
+
97
+ # raised when assert_locked_for_share! is called and no shared lock is held
98
+ class MustBeShareLockedException < ZKError; end
99
+ end
100
+ end
101
+
@@ -0,0 +1,144 @@
1
+ module ZK
2
+ module Extensions
3
+ # some extensions to the ZookeeperCallbacks classes, mainly convenience
4
+ # interrogators
5
+ module Callbacks
6
+ module Callback
7
+ # allow access to the connection that fired this callback
8
+ attr_accessor :zk
9
+
10
+ def self.included(mod)
11
+ mod.extend(ZK::Extensions::Callbacks::Callback::ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ # allows for easier construction of a user callback block that will be
16
+ # called with the callback object itself as an argument.
17
+ #
18
+ # *args, if given, will be passed on *after* the callback
19
+ #
20
+ # example:
21
+ #
22
+ # WatcherCallback.create do |cb|
23
+ # puts "watcher callback called with argument: #{cb.inspect}"
24
+ # end
25
+ #
26
+ # "watcher callback called with argument: #<ZookeeperCallbacks::WatcherCallback:0x1018a3958 @state=3, @type=1, ...>"
27
+ #
28
+ #
29
+ def create(*args, &block)
30
+ # honestly, i have no idea how this could *possibly* work, but it does...
31
+ cb_inst = new { block.call(cb_inst) }
32
+ end
33
+ end
34
+ end
35
+
36
+ module WatcherCallbackExt
37
+ include ZookeeperConstants
38
+
39
+ EVENT_NAME_MAP = {
40
+ 1 => 'created',
41
+ 2 => 'deleted',
42
+ 3 => 'changed',
43
+ 4 => 'child',
44
+ -1 => 'session',
45
+ -2 => 'notwatching',
46
+ }.freeze
47
+
48
+ STATES = %w[connecting associating connected auth_failed expired_session].freeze unless defined?(STATES)
49
+
50
+ EVENT_TYPES = %w[created deleted changed child session notwatching].freeze unless defined?(EVENT_TYPES)
51
+
52
+ STATES.each do |state|
53
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
54
+ def state_#{state}?
55
+ @state == ZOO_#{state.upcase}_STATE
56
+ end
57
+ RUBY
58
+ end
59
+
60
+ EVENT_TYPES.each do |ev|
61
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
62
+ def node_#{ev}?
63
+ @type == ZOO_#{ev.upcase}_EVENT
64
+ end
65
+ RUBY
66
+ end
67
+
68
+ alias :node_not_watching? :node_notwatching?
69
+
70
+ # has this watcher been called because of a change in connection state?
71
+ def state_event?
72
+ path.nil? or path.empty?
73
+ end
74
+
75
+ # has this watcher been called because of a change to a zookeeper node?
76
+ def node_event?
77
+ path and not path.empty?
78
+ end
79
+
80
+ # cause this watch to be re-registered
81
+ # def renew_watch!
82
+ # zk.stat(path, :watch => true)
83
+ # nil
84
+ # end
85
+ end
86
+ end # Callbacks
87
+
88
+ # aliases for long-names of properties from mb-zookeeper version
89
+ module Stat
90
+ [ %w[created_zxid czxid],
91
+ %w[last_modified_zxid mzxid],
92
+ %w[created_time ctime],
93
+ %w[last_modified_time mtime],
94
+ %w[child_list_version cversion],
95
+ %w[acl_list_version aversion] ].each do |long, short|
96
+
97
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
98
+ def #{long}
99
+ #{short}
100
+ end
101
+ RUBY
102
+ end
103
+
104
+ MEMBERS = [:version, :exists, :czxid, :mzxid, :ctime, :mtime, :cversion, :aversion, :ephemeralOwner, :dataLength, :numChildren, :pzxid]
105
+
106
+ def self.included(mod)
107
+ mod.class_eval do
108
+ unless method_defined?(:exists?)
109
+ alias :exists? :exists
110
+ end
111
+ end
112
+ end
113
+
114
+ def ==(other)
115
+ MEMBERS.all? { |m| self.__send__(m) == other.__send__(m) }
116
+ end
117
+ end
118
+
119
+ end # Extensions
120
+ end # ZK
121
+
122
+ # ZookeeperCallbacks::Callback.extend(ZK::Extensions::Callbacks::Callback)
123
+ ZookeeperCallbacks::Callback.send(:include, ZK::Extensions::Callbacks::Callback)
124
+ ZookeeperCallbacks::WatcherCallback.send(:include, ZK::Extensions::Callbacks::WatcherCallbackExt)
125
+ ZookeeperStat::Stat.send(:include, ZK::Extensions::Stat)
126
+
127
+ class ::Exception
128
+ unless method_defined?(:to_std_format)
129
+ def to_std_format
130
+ "#{self.class}: #{message}\n\t" + backtrace.join("\n\t")
131
+ end
132
+ end
133
+ end
134
+
135
+ class ::Thread
136
+ def zk_mongoid_lock_registry
137
+ self[:_zk_mongoid_lock_registry]
138
+ end
139
+
140
+ def zk_mongoid_lock_registry=(obj)
141
+ self[:_zk_mongoid_lock_registry] = obj
142
+ end
143
+ end
144
+
data/lib/z_k/locker.rb ADDED
@@ -0,0 +1,254 @@
1
+ module ZK
2
+ # Implements locking primitives {described here}[http://hadoop.apache.org/zookeeper/docs/current/recipes.html#sc_recipes_Locks]
3
+ #
4
+ # There are both shared and exclusive lock implementations.
5
+ #
6
+ #
7
+ # NOTE: These locks are _not_ safe for use across threads. If you want to use
8
+ # the same Locker class between threads, it is your responsibility to
9
+ # synchronize operations.
10
+ #
11
+ module Locker
12
+ SHARED_LOCK_PREFIX = 'sh'.freeze
13
+ EXCLUSIVE_LOCK_PREFIX = 'ex'.freeze
14
+
15
+ def self.shared_locker(zk, name)
16
+ SharedLocker.new(zk, name)
17
+ end
18
+
19
+ def self.exclusive_locker(zk, name)
20
+ ExclusiveLocker.new(zk, name)
21
+ end
22
+
23
+ class NoWriteLockFoundException < StandardError #:nodoc:
24
+ end
25
+
26
+ class WeAreTheLowestLockNumberException < StandardError #:nodoc:
27
+ end
28
+
29
+ class LockerBase
30
+ include ZK::Logging
31
+
32
+ attr_accessor :zk #:nodoc:
33
+
34
+ # our absolute lock node path
35
+ #
36
+ # ex. '/_zklocking/foobar/__blah/lock000000007'
37
+ attr_reader :lock_path #;nodoc:
38
+
39
+ attr_reader :root_lock_path #:nodoc:
40
+
41
+ def self.digit_from_lock_path(path) #:nodoc:
42
+ path[/0*(\d+)$/, 1].to_i
43
+ end
44
+
45
+ def initialize(zookeeper_client, name, root_lock_node = "/_zklocking")
46
+ @zk = zookeeper_client
47
+ @root_lock_node = root_lock_node
48
+ @path = name
49
+ @locked = false
50
+ @waiting = false
51
+ @root_lock_path = "#{@root_lock_node}/#{@path.gsub("/", "__")}"
52
+ end
53
+
54
+ # block caller until lock is aquired, then yield
55
+ def with_lock
56
+ lock!(true)
57
+ yield
58
+ ensure
59
+ unlock!
60
+ end
61
+
62
+ # the basename of our lock path
63
+ #
64
+ # for the lock_path '/_zklocking/foobar/__blah/lock000000007'
65
+ # lock_basename is 'lock000000007'
66
+ #
67
+ # returns nil if lock_path is not set
68
+ def lock_basename
69
+ lock_path and File.basename(lock_path)
70
+ end
71
+
72
+ def locked?
73
+ false|@locked
74
+ end
75
+
76
+ def unlock!
77
+ if @locked
78
+ cleanup_lock_path!
79
+ @locked = false
80
+ true
81
+ end
82
+ end
83
+
84
+ # returns true if this locker is waiting to acquire lock
85
+ def waiting? #:nodoc:
86
+ false|@waiting
87
+ end
88
+
89
+ protected
90
+ def in_waiting_status
91
+ w, @waiting = @waiting, true
92
+ yield
93
+ ensure
94
+ @waiting = w
95
+ end
96
+
97
+ def digit_from(path)
98
+ self.class.digit_from_lock_path(path)
99
+ end
100
+
101
+ def lock_children(watch=false)
102
+ @zk.children(root_lock_path, :watch => watch)
103
+ end
104
+
105
+ def ordered_lock_children(watch=false)
106
+ lock_children(watch).tap do |ary|
107
+ ary.sort! { |a,b| digit_from(a) <=> digit_from(b) }
108
+ end
109
+ end
110
+
111
+ def create_root_path!
112
+ @zk.mkdir_p(@root_lock_path)
113
+ end
114
+
115
+ # prefix is the string that will appear in front of the sequence num,
116
+ # defaults to 'lock'
117
+ def create_lock_path!(prefix='lock')
118
+ @lock_path = @zk.create("#{root_lock_path}/#{prefix}", "", :mode => :ephemeral_sequential)
119
+ logger.debug { "got lock path #{@lock_path}" }
120
+ @lock_path
121
+ rescue Exceptions::NoNode
122
+ create_root_path!
123
+ retry
124
+ end
125
+
126
+ def cleanup_lock_path!
127
+ logger.debug { "removing lock path #{@lock_path}" }
128
+ @zk.delete(@lock_path)
129
+ @zk.delete(root_lock_path) rescue Exceptions::NotEmpty
130
+ end
131
+ end
132
+
133
+ class SharedLocker < LockerBase
134
+ def lock!(blocking=false)
135
+ return true if @locked
136
+ create_lock_path!(SHARED_LOCK_PREFIX)
137
+
138
+ if got_read_lock?
139
+ @locked = true
140
+ elsif blocking
141
+ in_waiting_status do
142
+ block_until_read_lock!
143
+ end
144
+ else
145
+ # we didn't get the lock, and we're not gonna wait around for it, so
146
+ # clean up after ourselves
147
+ cleanup_lock_path!
148
+ false
149
+ end
150
+ end
151
+
152
+ def lock_number #:nodoc:
153
+ @lock_number ||= (lock_path and digit_from(lock_path))
154
+ end
155
+
156
+ # returns the sequence number of the next lowest write lock node
157
+ #
158
+ # raises NoWriteLockFoundException when there are no write nodes with a
159
+ # sequence less than ours
160
+ #
161
+ def next_lowest_write_lock_num #:nodoc:
162
+ digit_from(next_lowest_write_lock_name)
163
+ end
164
+
165
+ # the next lowest write lock number to ours
166
+ #
167
+ # so if we're "read010" and the children of the lock node are:
168
+ #
169
+ # %w[write008 write009 read010 read011]
170
+ #
171
+ # then this method will return write009
172
+ #
173
+ # raises NoWriteLockFoundException if there were no write nodes with an
174
+ # index lower than ours
175
+ #
176
+ def next_lowest_write_lock_name #:nodoc:
177
+ ary = ordered_lock_children()
178
+ my_idx = ary.index(lock_basename) # our idx would be 2
179
+
180
+ not_found = lambda { raise NoWriteLockFoundException }
181
+
182
+ ary[0..my_idx].reverse.find(not_found) { |n| n =~ /^#{EXCLUSIVE_LOCK_PREFIX}/ }
183
+ end
184
+
185
+ def got_read_lock? #:nodoc:
186
+ false if next_lowest_write_lock_num
187
+ rescue NoWriteLockFoundException
188
+ true
189
+ end
190
+
191
+ protected
192
+ # TODO: make this generic, can either block or non-block
193
+ def block_until_read_lock!
194
+ begin
195
+ path = [root_lock_path, next_lowest_write_lock_name].join('/')
196
+ logger.debug { "SharedLocker#block_until_read_lock! path=#{path.inspect}" }
197
+ @zk.block_until_node_deleted(path)
198
+ rescue NoWriteLockFoundException
199
+ # next_lowest_write_lock_name may raise NoWriteLockFoundException,
200
+ # which means we should not block as we have the lock (there is nothing to wait for)
201
+ end
202
+
203
+ @locked = true
204
+ end
205
+ end # SharedLocker
206
+
207
+ # An exclusive lock implementation
208
+ class ExclusiveLocker < LockerBase
209
+ def lock!(blocking=false)
210
+ return true if @locked
211
+ create_lock_path!(EXCLUSIVE_LOCK_PREFIX)
212
+
213
+ if got_write_lock?
214
+ @locked = true
215
+ elsif blocking
216
+ in_waiting_status do
217
+ block_until_write_lock!
218
+ end
219
+ else
220
+ cleanup_lock_path!
221
+ false
222
+ end
223
+ end
224
+
225
+ protected
226
+ # the node that is next-lowest in sequence number to ours, the one we
227
+ # watch for updates to
228
+ def next_lowest_node
229
+ ary = ordered_lock_children()
230
+ my_idx = ary.index(lock_basename)
231
+
232
+ raise WeAreTheLowestLockNumberException if my_idx == 0
233
+
234
+ ary[(my_idx - 1)]
235
+ end
236
+
237
+ def got_write_lock?
238
+ ordered_lock_children.first == lock_basename
239
+ end
240
+
241
+ def block_until_write_lock!
242
+ begin
243
+ path = [root_lock_path, next_lowest_node].join('/')
244
+ logger.debug { "SharedLocker#block_until_write_lock! path=#{path.inspect}" }
245
+ @zk.block_until_node_deleted(path)
246
+ rescue WeAreTheLowestLockNumberException
247
+ end
248
+
249
+ @locked = true
250
+ end
251
+ end # ExclusiveLocker
252
+ end # SharedLocker
253
+ end # ZooKeeper
254
+
@@ -0,0 +1,15 @@
1
+ module ZK
2
+ module Logging
3
+ def self.included(mod)
4
+ mod.extend(ZK::Logging::Methods)
5
+ mod.send(:include, ZK::Logging::Methods)
6
+ end
7
+
8
+ module Methods
9
+ def logger
10
+ ZK.logger
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,143 @@
1
+ module ZK
2
+ # implements a simple message queue based on Zookeeper recipes
3
+ # @see http://hadoop.apache.org/zookeeper/docs/r3.0.0/recipes.html#sc_recipes_Queues
4
+ # these are good for low-volume queues only
5
+ # because of the way zookeeper works, all message *titles* have to be read into memory
6
+ # in order to see what message to process next
7
+ # @example
8
+ # queue = zk.queue("somequeue")
9
+ # queue.publish(some_string)
10
+ # queue.poll! # will return one message
11
+ # #subscribe will handle messages as they come in
12
+ # queue.subscribe do |title, data|
13
+ # #handle message
14
+ # end
15
+ class MessageQueue
16
+
17
+ # @private
18
+ # :nodoc:
19
+ attr_accessor :zk
20
+
21
+ # @private
22
+ # :nodoc:
23
+ def initialize(zookeeper_client, queue_name, queue_root = "/_zkqueues")
24
+ @zk = zookeeper_client
25
+ @queue = queue_name
26
+ @queue_root = queue_root
27
+ @zk.create(@queue_root, "", :mode => :persistent) unless @zk.exists?(@queue_root)
28
+ @zk.create(full_queue_path, "", :mode => :persistent) unless @zk.exists?(full_queue_path)
29
+ end
30
+
31
+ # publish a message to the queue, you can (optionally) use message titles
32
+ # to guarantee unique messages in the queue
33
+ # @param [String] data - any arbitrary string value
34
+ # @param optional [String] message_title - specify a unique message title for this
35
+ # message
36
+ def publish(data, message_title = nil)
37
+ mode = :persistent_sequential
38
+ if message_title
39
+ mode = :persistent
40
+ else
41
+ message_title = "message"
42
+ end
43
+ @zk.create("#{full_queue_path}/#{message_title}", data, :mode => mode)
44
+ rescue KeeperException::NodeExists
45
+ return false
46
+ end
47
+
48
+ # you barely ever need to actually use this method
49
+ # but lets you remove a message from the queue by specifying
50
+ # its title
51
+ # @param [String] message_title the title of the message to remove
52
+ def delete_message(message_title)
53
+ full_path = "#{full_queue_path}/#{message_title}"
54
+ locker = @zk.locker("#{full_queue_path}/#{message_title}")
55
+ if locker.lock!
56
+ begin
57
+ @zk.delete(full_path)
58
+ return true
59
+ ensure
60
+ locker.unlock!
61
+ end
62
+ else
63
+ return false
64
+ end
65
+ end
66
+
67
+ # grab one message from the queue
68
+ # used when you don't want to or can't subscribe
69
+ # @see ZooKeeper::MessageQueue#subscribe
70
+ def poll!
71
+ find_and_process_next_available(messages)
72
+ end
73
+
74
+ # @example
75
+ # # subscribe like this:
76
+ # subscribe {|title, data| handle_message!; true}
77
+ # # returning true in the block deletes the message, false unlocks and requeues
78
+ # @yield [title, data] yield to your block with the message title and the data of
79
+ # the message
80
+ def subscribe(&block)
81
+ @subscription_block = block
82
+ @subscription_reference = @zk.watcher.subscribe(full_queue_path) do |event, zk|
83
+ find_and_process_next_available(@zk.children(full_queue_path, :watch => true))
84
+ end
85
+ find_and_process_next_available(@zk.children(full_queue_path, :watch => true))
86
+ end
87
+
88
+ # stop listening to this queue
89
+ def unsubscribe
90
+ @subscription_reference.unsubscribe
91
+ end
92
+
93
+ # a list of the message titles in the queue
94
+ def messages
95
+ @zk.children(full_queue_path)
96
+ end
97
+
98
+ # highly destructive method!
99
+ # WARNING! Will delete the queue and all messages in it
100
+ def destroy!
101
+ children = @zk.children(full_queue_path)
102
+ locks = []
103
+ children.each do |path|
104
+ lock = @zk.locker("#{full_queue_path}/#{path}")
105
+ lock.lock! # XXX(slyphon): should this be a blocking lock?
106
+ locks << lock
107
+ end
108
+ children.each do |path|
109
+ @zk.delete("#{full_queue_path}/#{path}") rescue ZK::Exceptions::NoNode
110
+ end
111
+ @zk.delete(full_queue_path) rescue ZK::Exceptions::NoNode
112
+ locks.each do |lock|
113
+ lock.unlock!
114
+ end
115
+ end
116
+
117
+ private
118
+ def find_and_process_next_available(messages)
119
+ messages.sort! {|a,b| digit_from_path(a) <=> digit_from_path(b)}
120
+ messages.each do |message_title|
121
+ message_path = "#{full_queue_path}/#{message_title}"
122
+ locker = @zk.locker(message_path)
123
+ if locker.lock!
124
+ begin
125
+ data = @zk.get(message_path).first
126
+ result = @subscription_block.call(message_title, data)
127
+ @zk.delete(message_path) if result
128
+ ensure
129
+ locker.unlock!
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def full_queue_path
136
+ @full_queue_path ||= "#{@queue_root}/#{@queue}"
137
+ end
138
+
139
+ def digit_from_path(path)
140
+ path[/\d+$/].to_i
141
+ end
142
+ end
143
+ end