zk 0.6.4

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