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.
- data/.gitignore +7 -0
- data/Gemfile +10 -0
- data/Rakefile +13 -0
- data/lib/z_k/client.rb +906 -0
- data/lib/z_k/election.rb +411 -0
- data/lib/z_k/event_handler.rb +202 -0
- data/lib/z_k/event_handler_subscription.rb +29 -0
- data/lib/z_k/exceptions.rb +101 -0
- data/lib/z_k/extensions.rb +144 -0
- data/lib/z_k/locker.rb +254 -0
- data/lib/z_k/logging.rb +15 -0
- data/lib/z_k/message_queue.rb +143 -0
- data/lib/z_k/mongoid.rb +172 -0
- data/lib/z_k/pool.rb +254 -0
- data/lib/z_k/threadpool.rb +109 -0
- data/lib/z_k/version.rb +3 -0
- data/lib/z_k.rb +73 -0
- data/lib/zk.rb +2 -0
- data/spec/client_pool_spec.rb +329 -0
- data/spec/client_spec.rb +102 -0
- data/spec/election_spec.rb +301 -0
- data/spec/locker_spec.rb +386 -0
- data/spec/log4j.properties +17 -0
- data/spec/message_queue_spec.rb +55 -0
- data/spec/mongoid_spec.rb +330 -0
- data/spec/spec_helper.rb +96 -0
- data/spec/support/bogus_mongoid.rb +11 -0
- data/spec/support/queuey_thread.rb +11 -0
- data/spec/test_file.txt +4 -0
- data/spec/threadpool_spec.rb +71 -0
- data/spec/watch_spec.rb +118 -0
- data/spec/zookeeper_spec.rb +176 -0
- data/zk.gemspec +24 -0
- metadata +176 -0
@@ -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
|
+
|
data/lib/z_k/logging.rb
ADDED
@@ -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
|