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,172 @@
1
+ module ZK
2
+ module Mongoid
3
+ # provides a lock_for_update method based on the current class name
4
+ # and Mongoid document _id.
5
+ #
6
+ # Before use (in one of your Rails initializers, for example) you should
7
+ # assign either a ZK::Client or ZK::Pool subclass to
8
+ # ZooKeeperLockMixin.zk_lock_pool.
9
+ #
10
+ # this class assumes the availability of a 'logger' method in the mixee
11
+ #
12
+ module Locking
13
+ VALID_MODES = [:exclusive, :shared].freeze
14
+
15
+ @@zk_lock_pool = nil unless defined?(@@zk_lock_pool)
16
+
17
+ def self.zk_lock_pool
18
+ @@zk_lock_pool
19
+ end
20
+
21
+ def self.zk_lock_pool=(pool)
22
+ @@zk_lock_pool = pool
23
+ end
24
+
25
+ # Provides a re-entrant zookeeper-based lock of a record.
26
+ #
27
+ # This also makes it possible to detect if the record has been locked before
28
+ # performing a potentially dangerous operation by using the assert_locked_for_update!
29
+ # instance method
30
+ #
31
+ # Locks are re-entrant per-thread, but will work as a mutex between
32
+ # threads.
33
+ #
34
+ # You can optionally provide a 'name' which will act as a sub-lock of
35
+ # sorts. For example, if you are going to create an embedded document,
36
+ # and only want one process to be able to create it at a time (without
37
+ # clobbering one another), but don't want to lock the entire record, you
38
+ # can specify a name for the lock, that way the same code running
39
+ # elsewhere will synchronize based on the parent record and the
40
+ # particular action specified by +name+.
41
+ #
42
+ # ==== Example
43
+ #
44
+ # use of "name"
45
+ #
46
+ # class Thing
47
+ # include Mongoid::Document
48
+ # include ZK::Mongoid::Locking
49
+ #
50
+ # embedded_in :parent, :inverse_of => :thing
51
+ # end
52
+ #
53
+ # class Parent
54
+ # include Mongoid::Document
55
+ # include ZK::Mongoid::Locking
56
+ #
57
+ # embeds_one :thing
58
+ #
59
+ # def lets_create_a_thing
60
+ # lock_for_update('thing_creation') do
61
+ # raise "We already got one! it's very nice!" if thing
62
+ #
63
+ # do_something_that_might_take_a_while
64
+ # create_thing
65
+ # end
66
+ # end
67
+ # end
68
+ #
69
+ #
70
+ # Now, while the creation of the Thing is synchronized, other processes
71
+ # can update other aspects of Parent.
72
+ #
73
+ #
74
+ def lock_for_update(name=nil)
75
+ if locked_for_update?(name)
76
+ logger.debug { "we are locked for update, yield to the block" }
77
+ yield
78
+ else
79
+ zk_with_lock(:mode => :exclusive, :name => name) { yield }
80
+ end
81
+ end
82
+ alias :with_exclusive_lock :lock_for_update
83
+
84
+ def with_shared_lock(name=nil)
85
+ if locked_for_share?(name)
86
+ yield
87
+ else
88
+ zk_with_lock(:mode => :shared, :name => name) { yield }
89
+ end
90
+ end
91
+
92
+ # raises MustBeExclusivelyLockedException if we're not currently inside a
93
+ # lock (optionally with +name+)
94
+ def assert_locked_for_update!(name=nil)
95
+ raise ZK::Exceptions::MustBeExclusivelyLockedException unless locked_for_update?(name)
96
+ end
97
+
98
+ # raises MustBeShareLockedException if we're not currently inside a shared lock
99
+ # (optionally with +name+)
100
+ def assert_locked_for_share!(name=nil)
101
+ raise ZK::Exceptions::MustBeShareLockedException unless locked_for_share?(name)
102
+ end
103
+
104
+ def locked_for_update?(name=nil) #:nodoc:
105
+ zk_mongoid_lock_registry[:exclusive].include?(zk_lock_name(name))
106
+ end
107
+
108
+ def locked_for_share?(name=nil) #:nodoc:
109
+ zk_mongoid_lock_registry[:shared].include?(zk_lock_name(name))
110
+ end
111
+
112
+ def zk_lock_name(name=nil) #:nodoc:
113
+ [self.class.to_s, self.id.to_s, name].compact.join('-')
114
+ end
115
+
116
+ protected
117
+ def zk_mongoid_lock_registry
118
+ Thread.current.zk_mongoid_lock_registry ||= { :shared => Set.new, :exclusive => Set.new }
119
+ end
120
+
121
+ def zk_add_path_lock(opts={})
122
+ mode, name = opts.values_at(:mode, :name)
123
+
124
+ raise ArgumentError, "You must specify a :mode option" unless mode
125
+
126
+ zk_assert_valid_mode!(mode)
127
+
128
+ logger.debug { "adding #{zk_lock_name(name).inspect} to #{mode} lock registry" }
129
+
130
+ self.zk_mongoid_lock_registry[mode] << zk_lock_name(name)
131
+ end
132
+
133
+ def zk_remove_path_lock(opts={})
134
+ mode, name = opts.values_at(:mode, :name)
135
+
136
+ raise ArgumentError, "You must specify a :mode option" unless mode
137
+
138
+ zk_assert_valid_mode!(mode)
139
+
140
+ logger.debug { "removing #{zk_lock_name(name).inspect} from #{mode} lock registry" }
141
+
142
+ zk_mongoid_lock_registry[mode].delete(zk_lock_name(name))
143
+ end
144
+
145
+ def zk_with_lock(opts={})
146
+ mode, name = opts.values_at(:mode, :name)
147
+
148
+ zk_assert_valid_mode!(mode)
149
+
150
+ zk_lock_pool.with_lock(zk_lock_name(name), :mode => mode) do
151
+ zk_add_path_lock(opts)
152
+
153
+ begin
154
+ logger.debug { "acquired #{zk_lock_name(name).inspect}" }
155
+ yield
156
+ ensure
157
+ logger.debug { "releasing #{zk_lock_name(name).inspect}" }
158
+ zk_remove_path_lock(opts)
159
+ end
160
+ end
161
+ end
162
+
163
+ def zk_lock_pool
164
+ @zk_lock_pool ||= ::ZK::Mongoid::Locking.zk_lock_pool
165
+ end
166
+
167
+ def zk_assert_valid_mode!(mode)
168
+ raise ArgumentError, "#{mode.inspect} is not a valid mode value" unless VALID_MODES.include?(mode)
169
+ end
170
+ end
171
+ end
172
+ end
data/lib/z_k/pool.rb ADDED
@@ -0,0 +1,254 @@
1
+ module ZK
2
+ module Pool
3
+ class Base
4
+ attr_reader :connections #:nodoc:
5
+
6
+ def initialize
7
+ @state = :init
8
+
9
+ @connections = []
10
+ @connections.extend(MonitorMixin)
11
+ @checkin_cond = @connections.new_cond
12
+ end
13
+
14
+ # has close_all! been called on this ConnectionPool ?
15
+ def closed?
16
+ @state == :closed
17
+ end
18
+
19
+ # is the pool shutting down?
20
+ def closing?
21
+ @state == :closing
22
+ end
23
+
24
+ # is the pool initialized and in normal operation?
25
+ def open?
26
+ @state == :open
27
+ end
28
+
29
+ # has the pool entered the take-no-prisoners connection closing part of shutdown?
30
+ def forced?
31
+ @state == :forced
32
+ end
33
+
34
+ # close all the connections on the pool
35
+ # @param optional Boolean graceful allow the checked out connections to come back first?
36
+ def close_all!
37
+ synchronize do
38
+ return unless open?
39
+ @state = :closing
40
+
41
+ @checkin_cond.wait_until { (@pool.size == @connections.length) or closed? }
42
+
43
+ force_close!
44
+ end
45
+ end
46
+
47
+ # calls close! on all connection objects, whether or not they're back in the pool
48
+ # this is DANGEROUS!
49
+ def force_close! #:nodoc:
50
+ synchronize do
51
+ return if (closed? or forced?)
52
+ @state = :forced
53
+
54
+ @pool.clear
55
+
56
+ while cnx = @connections.shift
57
+ cnx.close!
58
+ end
59
+
60
+ @state = :closed
61
+
62
+ # free any waiting
63
+ @checkin_cond.broadcast
64
+ end
65
+ end
66
+
67
+ # yields next available connection to the block
68
+ #
69
+ # raises PoolIsShuttingDownException immediately if close_all! has been
70
+ # called on this pool
71
+ def with_connection
72
+ assert_open!
73
+
74
+ cnx = checkout(true)
75
+ yield cnx
76
+ ensure
77
+ checkin(cnx)
78
+ end
79
+
80
+ #lock lives on past the connection checkout
81
+ def locker(path)
82
+ with_connection do |connection|
83
+ connection.locker(path)
84
+ end
85
+ end
86
+
87
+ #prefer this method if you can (keeps connection checked out)
88
+ def with_lock(name, opts={}, &block)
89
+ with_connection do |connection|
90
+ connection.with_lock(name, opts, &block)
91
+ end
92
+ end
93
+
94
+ # handle all
95
+ def method_missing(meth, *args, &block)
96
+ with_connection do |connection|
97
+ connection.__send__(meth, *args, &block)
98
+ end
99
+ end
100
+
101
+ def size #:nodoc:
102
+ @pool.size
103
+ end
104
+
105
+ def pool_state #:nodoc:
106
+ @state
107
+ end
108
+
109
+ protected
110
+ def synchronize
111
+ @connections.synchronize { yield }
112
+ end
113
+
114
+ def assert_open!
115
+ raise Exceptions::PoolIsShuttingDownException unless open?
116
+ end
117
+
118
+ end # Base
119
+
120
+ # like a Simple pool but has high/low watermarks, and can grow dynamically as needed
121
+ class Bounded < Base
122
+ DEFAULT_OPTIONS = {
123
+ :timeout => 10,
124
+ :min_clients => 1,
125
+ :max_clients => 10,
126
+ }.freeze
127
+
128
+ # opts:
129
+ # * <tt>:timeout</tt>: connection establishement timeout
130
+ # * <tt>:min_clients</tt>: how many clients should be start out with
131
+ # * <tt>:max_clients</tt>: the maximum number of clients we will create in response to demand
132
+ def initialize(host, opts={})
133
+ super()
134
+ @host = host
135
+ @connection_args = opts
136
+
137
+ opts = DEFAULT_OPTIONS.merge(opts)
138
+
139
+ @min_clients = Integer(opts.delete(:min_clients))
140
+ @max_clients = Integer(opts.delete(:max_clients))
141
+ @connection_timeout = opts.delete(:timeout)
142
+
143
+ # for compatibility w/ ClientPool we'll use @connections for synchronization
144
+ @pool = [] # currently available connections
145
+
146
+ synchronize do
147
+ populate_pool!(@min_clients)
148
+ @state = :open
149
+ end
150
+ end
151
+
152
+ # returns the current number of allocated clients in the pool (not
153
+ # available clients)
154
+ def size
155
+ @connections.length
156
+ end
157
+
158
+ # clients available for checkout (at time of call)
159
+ def available_size
160
+ @pool.length
161
+ end
162
+
163
+ def checkin(connection)
164
+ synchronize do
165
+ return if @pool.include?(connection)
166
+
167
+ @pool.unshift(connection)
168
+ @checkin_cond.signal
169
+ end
170
+ end
171
+
172
+ # number of threads waiting for connections
173
+ def count_waiters #:nodoc:
174
+ @checkin_cond.count_waiters
175
+ end
176
+
177
+ def checkout(blocking=true)
178
+ raise ArgumentError, "checkout does not take a block, use .with_connection" if block_given?
179
+ synchronize do
180
+ while true
181
+ assert_open!
182
+
183
+ if @pool.length > 0
184
+ cnx = @pool.shift
185
+
186
+ # if the cnx isn't connected? then remove it from the pool and go
187
+ # through the loop again. when the cnx's on_connected event fires, it
188
+ # will add the connection back into the pool
189
+ next unless cnx.connected?
190
+
191
+ # otherwise we return the cnx
192
+ return cnx
193
+ elsif can_grow_pool?
194
+ add_connection!
195
+ next
196
+ elsif blocking
197
+ @checkin_cond.wait_while { @pool.empty? and open? }
198
+ next
199
+ else
200
+ return false
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ protected
207
+ def populate_pool!(num_cnx)
208
+ num_cnx.times { add_connection! }
209
+ end
210
+
211
+ def add_connection!
212
+ synchronize do
213
+ cnx = create_connection
214
+ @connections << cnx
215
+
216
+ cnx.on_connected { checkin(cnx) }
217
+ end
218
+ end
219
+
220
+ def can_grow_pool?
221
+ synchronize { @connections.size < @max_clients }
222
+ end
223
+
224
+ def create_connection
225
+ ZK.new(@host, @connection_timeout, @connection_args)
226
+ end
227
+ end # Bounded
228
+
229
+ # create a connection pool useful for multithreaded applications
230
+ #
231
+ # Will spin up +number_of_connections+ at creation time and remain fixed at
232
+ # that number for the life of the pool.
233
+ #
234
+ # ==== Example
235
+ # pool = ZK::Pool::Simple.new("localhost:2181", 10)
236
+ # pool.checkout do |zk|
237
+ # zk.create("/mynew_path")
238
+ # end
239
+ class Simple < Bounded
240
+ # initialize a connection pool using the same optons as ZK.new
241
+ # @param String host the same arguments as ZK.new
242
+ # @param Integer number_of_connections the number of connections to put in the pool
243
+ # @param optional Hash opts Options to pass on to each connection
244
+ # @return ZK::ClientPool
245
+ def initialize(host, number_of_connections=10, opts = {})
246
+ opts = opts.dup
247
+ opts[:max_clients] = opts[:min_clients] = number_of_connections.to_i
248
+
249
+ super(host, opts)
250
+ end
251
+ end # Simple
252
+ end # Pool
253
+ end # ZK
254
+
@@ -0,0 +1,109 @@
1
+ module ZK
2
+ # a simple threadpool for running blocks of code off the main thread
3
+ class Threadpool
4
+ include Logging
5
+
6
+ DEFAULT_SIZE = 5
7
+
8
+ class << self
9
+ # size of the ZK.defer threadpool (defaults to 5)
10
+ attr_accessor :default_size
11
+ ZK::Threadpool.default_size = DEFAULT_SIZE
12
+ end
13
+
14
+ # the size of this threadpool
15
+ attr_reader :size
16
+
17
+ def initialize(size=nil)
18
+ @size = size || self.class.default_size
19
+
20
+ @threadpool = []
21
+ @threadqueue = ::Queue.new
22
+
23
+ @mutex = Mutex.new
24
+
25
+ start!
26
+ end
27
+
28
+ # Queue an operation to be run on an internal threadpool. You may either
29
+ # provide an object that responds_to?(:call) or pass a block. There is no
30
+ # mechanism for retrieving the result of the operation, it is purely
31
+ # fire-and-forget, so the user is expected to make arrangements for this in
32
+ # their code.
33
+ #
34
+ def defer(callable=nil, &blk)
35
+ callable ||= blk
36
+
37
+ # XXX(slyphon): do we care if the threadpool is not running?
38
+ raise Exceptions::ThreadpoolIsNotRunningException unless running?
39
+ raise ArgumentError, "Argument to Threadpool#defer must respond_to?(:call)" unless callable.respond_to?(:call)
40
+
41
+ @threadqueue << callable
42
+ nil
43
+ end
44
+
45
+ def running?
46
+ @mutex.synchronize { @running }
47
+ end
48
+
49
+ # starts the threadpool if not already running
50
+ def start!
51
+ @mutex.synchronize do
52
+ return false if @running
53
+ @running = true
54
+ spawn_threadpool
55
+ end
56
+ true
57
+ end
58
+
59
+ # join all threads in this threadpool, they will be given a maximum of +timeout+
60
+ # seconds to exit before they are considered hung and will be ignored (this is an
61
+ # issue with threads in general: see
62
+ # http://blog.headius.com/2008/02/rubys-threadraise-threadkill-timeoutrb.html for more info)
63
+ #
64
+ # the default timeout is 2 seconds per thread
65
+ #
66
+ def shutdown(timeout=2)
67
+ @mutex.synchronize do
68
+ return unless @running
69
+
70
+ @running = false
71
+
72
+ @threadqueue.clear
73
+ @size.times { @threadqueue << KILL_TOKEN }
74
+
75
+ while th = @threadpool.shift
76
+ begin
77
+ th.join(timeout)
78
+ rescue Exception => e
79
+ logger.error { "Caught exception shutting down threadpool" }
80
+ logger.error { e.to_std_format }
81
+ end
82
+ end
83
+ end
84
+
85
+ nil
86
+ end
87
+
88
+ private
89
+ def spawn_threadpool #:nodoc:
90
+ until @threadpool.size == @size.to_i
91
+ thread = Thread.new do
92
+ while @running
93
+ begin
94
+ op = @threadqueue.pop
95
+ break if op == KILL_TOKEN
96
+ op.call
97
+ rescue Exception => e
98
+ logger.error { "Exception caught in threadpool" }
99
+ logger.error { e.to_std_format }
100
+ end
101
+ end
102
+ end
103
+
104
+ @threadpool << thread
105
+ end
106
+ end
107
+ end
108
+ end
109
+
@@ -0,0 +1,3 @@
1
+ module ZK
2
+ VERSION = "0.6.4"
3
+ end
data/lib/z_k.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'logger'
5
+ require 'zookeeper'
6
+ require 'forwardable'
7
+ require 'thread'
8
+ require 'monitor'
9
+ require 'set'
10
+
11
+ require 'z_k/logging'
12
+ require 'z_k/exceptions'
13
+ require 'z_k/threadpool'
14
+ require 'z_k/event_handler_subscription'
15
+ require 'z_k/event_handler'
16
+ require 'z_k/message_queue'
17
+ # require 'z_k/locker_base'
18
+ require 'z_k/locker'
19
+ require 'z_k/extensions'
20
+ require 'z_k/election'
21
+ require 'z_k/mongoid'
22
+ require 'z_k/client'
23
+ require 'z_k/pool'
24
+
25
+ module ZK
26
+ ZK_ROOT = File.expand_path('../..', __FILE__)
27
+
28
+ KILL_TOKEN = :__kill_token__ #:nodoc:
29
+
30
+
31
+ # The logger used by the ZK library. uses a Logger to +/dev/null+ by default
32
+ #
33
+ def self.logger
34
+ @logger ||= Logger.new('/dev/null')
35
+ end
36
+
37
+ # Assign the Logger instance to be used by ZK
38
+ def self.logger=(logger)
39
+ @logger = logger
40
+ end
41
+
42
+ # Create a new ZK::Client instance. If no arguments are given, the default
43
+ # config of 'localhost:2181' will be used. Otherwise all args will be passed
44
+ # to ZK::Client#new
45
+ #
46
+ # if a block is given, it will be yielded the client *before* the connection
47
+ # is established, this is useful for registering connected-state handlers.
48
+ #
49
+ def self.new(*args, &block)
50
+ # XXX: might need to do some param parsing here
51
+
52
+ opts = args.pop if args.last.kind_of?(Hash)
53
+ args = %w[localhost:2181] if args.empty?
54
+
55
+ # ignore opts for now
56
+ Client.new(*args, &block)
57
+ end
58
+
59
+ # Like new, yields a connection to the given block and closes it when the
60
+ # block returns
61
+ def self.open(*args)
62
+ cnx = new(*args)
63
+ yield cnx
64
+ ensure
65
+ cnx.close! if cnx
66
+ end
67
+
68
+ # creates a new ZK::Pool::Bounded with the default options.
69
+ def self.new_pool(host, opts={})
70
+ ZK::Pool::Bounded.new(host, opts)
71
+ end
72
+ end
73
+
data/lib/zk.rb ADDED
@@ -0,0 +1,2 @@
1
+ require File.expand_path('../z_k', __FILE__)
2
+