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