zk 0.6.4

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