zk 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,161 @@
1
+ module ZK
2
+ module Locker
3
+ # Common code for the shared and exclusive lock implementations
4
+ #
5
+ # One thing to note about this implementation is that the API unfortunately
6
+ # __does not__ follow the convention where bang ('!') methods raise
7
+ # exceptions when they fail. This was an oversight on the part of the
8
+ # author, and it may be corrected sometime in the future.
9
+ #
10
+ class LockerBase
11
+ include ZK::Logging
12
+
13
+ # @private
14
+ attr_accessor :zk
15
+
16
+ # our absolute lock node path
17
+ #
18
+ # @example
19
+ #
20
+ # '/_zklocking/foobar/__blah/lock000000007'
21
+ #
22
+ # @return [String]
23
+ attr_reader :lock_path
24
+
25
+ # @private
26
+ attr_reader :root_lock_path
27
+
28
+ # Extracts the integer from the zero-padded sequential lock path
29
+ #
30
+ # @return [Integer] our digit
31
+ # @private
32
+ def self.digit_from_lock_path(path)
33
+ path[/0*(\d+)$/, 1].to_i
34
+ end
35
+
36
+ # Create a new lock instance.
37
+ #
38
+ # @param [Client::Threaded] client a client instance
39
+ #
40
+ # @param [String] name Unique name that will be used to generate a key.
41
+ # All instances created with the same `root_lock_node` and `name` will be
42
+ # holding the same lock.
43
+ #
44
+ # @param [String] root_lock_node the root path on the server under which all
45
+ # locks will be generated
46
+ #
47
+ def initialize(client, name, root_lock_node = "/_zklocking")
48
+ @zk = client
49
+ @root_lock_node = root_lock_node
50
+ @path = name
51
+ @locked = false
52
+ @waiting = false
53
+ @root_lock_path = "#{@root_lock_node}/#{@path.gsub("/", "__")}"
54
+ end
55
+
56
+ # block caller until lock is aquired, then yield
57
+ #
58
+ # there is no non-blocking version of this method
59
+ #
60
+ def with_lock
61
+ lock!(true)
62
+ yield
63
+ ensure
64
+ unlock!
65
+ end
66
+
67
+ # the basename of our lock path
68
+ #
69
+ # @example
70
+ #
71
+ # > locker.lock_path
72
+ # # => '/_zklocking/foobar/__blah/lock000000007'
73
+ # > locker.lock_basename
74
+ # # => 'lock000000007'
75
+ #
76
+ # @return [nil] if lock_path is not set
77
+ # @return [String] last path component of our lock path
78
+ def lock_basename
79
+ lock_path and File.basename(lock_path)
80
+ end
81
+
82
+ # @return [true,false] true if we hold the lock
83
+ def locked?
84
+ false|@locked
85
+ end
86
+
87
+ # @return [true] if we held the lock and this method has
88
+ # unlocked it successfully
89
+ #
90
+ # @return [false] we did not own the lock
91
+ #
92
+ def unlock!
93
+ if @locked
94
+ cleanup_lock_path!
95
+ @locked = false
96
+ true
97
+ else
98
+ false # i know, i know, but be explicit
99
+ end
100
+ end
101
+
102
+ # returns true if this locker is waiting to acquire lock
103
+ #
104
+ # @private
105
+ def waiting?
106
+ false|@waiting
107
+ end
108
+
109
+ protected
110
+ # @private
111
+ def in_waiting_status
112
+ w, @waiting = @waiting, true
113
+ yield
114
+ ensure
115
+ @waiting = w
116
+ end
117
+
118
+ # @private
119
+ def digit_from(path)
120
+ self.class.digit_from_lock_path(path)
121
+ end
122
+
123
+ # @private
124
+ def lock_children(watch=false)
125
+ @zk.children(root_lock_path, :watch => watch)
126
+ end
127
+
128
+ # @private
129
+ def ordered_lock_children(watch=false)
130
+ lock_children(watch).tap do |ary|
131
+ ary.sort! { |a,b| digit_from(a) <=> digit_from(b) }
132
+ end
133
+ end
134
+
135
+ # @private
136
+ def create_root_path!
137
+ @zk.mkdir_p(@root_lock_path)
138
+ end
139
+
140
+ # prefix is the string that will appear in front of the sequence num,
141
+ # defaults to 'lock'
142
+ #
143
+ # @private
144
+ def create_lock_path!(prefix='lock')
145
+ @lock_path = @zk.create("#{root_lock_path}/#{prefix}", "", :mode => :ephemeral_sequential)
146
+ logger.debug { "got lock path #{@lock_path}" }
147
+ @lock_path
148
+ rescue Exceptions::NoNode
149
+ create_root_path!
150
+ retry
151
+ end
152
+
153
+ # @private
154
+ def cleanup_lock_path!
155
+ logger.debug { "removing lock path #{@lock_path}" }
156
+ @zk.delete(@lock_path)
157
+ @zk.delete(root_lock_path) rescue Exceptions::NotEmpty
158
+ end
159
+ end # LockerBase
160
+ end # Locker
161
+ end # ZK
@@ -0,0 +1,101 @@
1
+ module ZK
2
+ module Locker
3
+ class SharedLocker < LockerBase
4
+ include Exceptions
5
+
6
+ # obtain a shared lock.
7
+ #
8
+ # @param blocking [true,false] if true we block the caller until we can obtain
9
+ # a lock on the resource
10
+ #
11
+ # @return [true] if we're already obtained a shared lock, or if we were able to
12
+ # obtain the lock in non-blocking mode.
13
+ #
14
+ # @return [false] if we did not obtain the lock in non-blocking mode
15
+ #
16
+ # @return [void] if we obtained the lock in blocking mode.
17
+ #
18
+ # @raise [InterruptedSession] raised when blocked waiting for a lock and
19
+ # the underlying client's session is interrupted.
20
+ #
21
+ # @see ZK::Client::Unixisms#block_until_node_deleted more about possible execptions
22
+ #
23
+ def lock!(blocking=false)
24
+ return true if @locked
25
+ create_lock_path!(SHARED_LOCK_PREFIX)
26
+
27
+ if got_read_lock?
28
+ @locked = true
29
+ elsif blocking
30
+ in_waiting_status do
31
+ block_until_read_lock!
32
+ end
33
+ else
34
+ # we didn't get the lock, and we're not gonna wait around for it, so
35
+ # clean up after ourselves
36
+ cleanup_lock_path!
37
+ false
38
+ end
39
+ end
40
+
41
+ # @private
42
+ def lock_number
43
+ @lock_number ||= (lock_path and digit_from(lock_path))
44
+ end
45
+
46
+ # returns the sequence number of the next lowest write lock node
47
+ #
48
+ # raises NoWriteLockFoundException when there are no write nodes with a
49
+ # sequence less than ours
50
+ #
51
+ # @private
52
+ def next_lowest_write_lock_num
53
+ digit_from(next_lowest_write_lock_name)
54
+ end
55
+
56
+ # the next lowest write lock number to ours
57
+ #
58
+ # so if we're "read010" and the children of the lock node are:
59
+ #
60
+ # %w[write008 write009 read010 read011]
61
+ #
62
+ # then this method will return write009
63
+ #
64
+ # raises NoWriteLockFoundException if there were no write nodes with an
65
+ # index lower than ours
66
+ #
67
+ # @private
68
+ def next_lowest_write_lock_name
69
+ ary = ordered_lock_children()
70
+ my_idx = ary.index(lock_basename) # our idx would be 2
71
+
72
+ not_found = lambda { raise NoWriteLockFoundException }
73
+
74
+ ary[0..my_idx].reverse.find(not_found) { |n| n =~ /^#{EXCLUSIVE_LOCK_PREFIX}/ }
75
+ end
76
+
77
+ # @private
78
+ def got_read_lock?
79
+ false if next_lowest_write_lock_num
80
+ rescue NoWriteLockFoundException
81
+ true
82
+ end
83
+
84
+ protected
85
+ # TODO: make this generic, can either block or non-block
86
+ # @private
87
+ def block_until_read_lock!
88
+ begin
89
+ path = [root_lock_path, next_lowest_write_lock_name].join('/')
90
+ logger.debug { "SharedLocker#block_until_read_lock! path=#{path.inspect}" }
91
+ @zk.block_until_node_deleted(path)
92
+ rescue NoWriteLockFoundException
93
+ # next_lowest_write_lock_name may raise NoWriteLockFoundException,
94
+ # which means we should not block as we have the lock (there is nothing to wait for)
95
+ end
96
+
97
+ @locked = true
98
+ end
99
+ end # SharedLocker
100
+ end # Locker
101
+ end # ZK
data/lib/zk/locker.rb CHANGED
@@ -1,258 +1,127 @@
1
1
  module ZK
2
- # Implements locking primitives [described here](http://hadoop.apache.org/zookeeper/docs/current/recipes.html#sc_recipes_Locks)
2
+ # This module contains implementations of the locking primitives described in
3
+ # [the ZooKeeper recipes][recipes] that allow a user to obtain cluster-wide
4
+ # global locks (with both blocking and non-blocking semantics). One
5
+ # important (and attractive) attribute of these locks is that __they are
6
+ # automatically released when the connection closes__. You never have to
7
+ # worry about a stale lock mucking up coordination because some process was
8
+ # killed and couldn't clean up after itself.
3
9
  #
4
10
  # There are both shared and exclusive lock implementations.
5
11
  #
6
- # NOTE: These lock instances are _not_ safe for use across threads. If you
7
- # want to use the same Locker instance between threads, it is your
8
- # responsibility to synchronize operations.
12
+ # The implementation is fairly true to the description in the [recipes][], and
13
+ # the key is generated using a combination of the name provided, and a
14
+ # `root_lock_node` path whose default value is `/_zklocking`. If you look
15
+ # below at the 'Key path creation' example, you'll see that we do a very
16
+ # simple escaping of the name given. There was a distinct tradeoff to be made
17
+ # between making the locks easy to debug in zookeeper and making them more
18
+ # collision tolerant. If the key naming causes issues, please [file a bug] and
19
+ # we'll try to work out a solution (hearing about use cases is incredibly helpful
20
+ # in guiding development).
9
21
  #
22
+ # If you're interested in how the algorithm works, have a look at
23
+ # {ZK::Locker::ExclusiveLocker}'s documentation.
24
+ #
25
+ # [recipes]: http://zookeeper.apache.org/doc/r3.3.5/recipes.html#sc_recipes_Locks
26
+ # [file a bug]: https://github.com/slyphon/zk/issues
27
+ #
28
+ # ## Shared/Exclusive lock interaction ##
29
+ #
30
+ # The shared and exclusive locks can be used to create traditional read/write locks,
31
+ # and are designed to be fair in terms of ordering. Given the following children
32
+ # of a given lock node (where 'sh' is shared, and 'ex' is exclusive)
33
+ #
34
+ # [ex00, sh01, sh02, sh03, ex04, ex05, sh06, sh07]
35
+ #
36
+ # Assuming all of these locks are blocking, the following is how the callers would
37
+ # obtain the lock
38
+ #
39
+ # * `ex00` holds the lock, everyone else is blocked
40
+ # * `ex00` releases the lock
41
+ # * `[sh01, sh02, sh03]` all unblock and hold a shared lock
42
+ # * `[ex04, ...]` are blocked
43
+ # * `[sh01, sh02, sh03]` all release
44
+ # * `ex04` is unblocked, holds the lock
45
+ # * `[ex05, ...]` are blocked
46
+ # * `ex04` releases the lock
47
+ # * `ex05` unblocks, holds the lock
48
+ # * `[sh06, sh07]` are blocked
49
+ # * `ex05` releases the lock
50
+ # * `[sh06, sh07]` are unblocked, hold the lock
51
+ #
52
+ #
53
+ # In this way, the locks are fair-queued (FIFO), and shared locks will not
54
+ # starve exclusive locks (all lock types have the same priority)
55
+ #
56
+ # @example Key path creation
57
+ #
58
+ # "#{root_lock_node}/#{name.gsub('/', '__')}/#{shared_or_exclusive_prefix}"
59
+ #
60
+ # @note These lock instances are _not_ safe for use across threads. If you
61
+ # want to use the same Locker instance between threads, it is your
62
+ # responsibility to synchronize operations.
63
+ #
64
+ # @note Lockers are *instances* that hold the lock. A single connection may
65
+ # have many instances trying to lock the same path and only *one* (in the
66
+ # case of an ExclusiveLocker) will hold the lock.
67
+ #
68
+ # @example Creating locks directly from a client instance
69
+ #
70
+ # # this same example would work for zk.shared_locker('key_name') only
71
+ # # the lock returned would be a shared lock, instead of an exclusive lock
72
+ #
73
+ # ex_locker = zk.locker('key_name')
74
+ #
75
+ # begin
76
+ # if ex_locker.lock!
77
+ # # do something while holding lock
78
+ # else
79
+ # raise "Oh noes, we didn't get teh lock!"
80
+ # end
81
+ # ensure
82
+ # ex_locker.unlock!
83
+ # end
84
+ #
85
+ # @example Creating a blocking lock around a cluster-wide critical section
86
+ #
87
+ # zk.with_lock('key_name') do # this will block us until we get the lock
88
+ #
89
+ # # this is the critical section
90
+ #
91
+ # end
10
92
  module Locker
11
93
  SHARED_LOCK_PREFIX = 'sh'.freeze
12
94
  EXCLUSIVE_LOCK_PREFIX = 'ex'.freeze
13
95
 
14
- def self.shared_locker(zk, name)
15
- SharedLocker.new(zk, name)
96
+ # Create a {SharedLocker} instance
97
+ #
98
+ # @param client (see LockerBase#initialize)
99
+ # @param name (see LockerBase#initialize)
100
+ # @return [SharedLocker]
101
+ def self.shared_locker(client, name, *args)
102
+ SharedLocker.new(client, name, *args)
16
103
  end
17
104
 
18
- def self.exclusive_locker(zk, name)
19
- ExclusiveLocker.new(zk, name)
105
+ # Create an {ExclusiveLocker} instance
106
+ #
107
+ # @param client (see LockerBase#initialize)
108
+ # @param name (see LockerBase#initialize)
109
+ # @return [ExclusiveLocker]
110
+ def self.exclusive_locker(client, name, *args)
111
+ ExclusiveLocker.new(client, name, *args)
20
112
  end
21
113
 
22
- class NoWriteLockFoundException < StandardError #:nodoc:
23
- end
24
-
25
- class WeAreTheLowestLockNumberException < StandardError #:nodoc:
114
+ # @private
115
+ class NoWriteLockFoundException < StandardError
26
116
  end
27
117
 
28
- class LockerBase
29
- include ZK::Logging
30
-
31
- # @private
32
- attr_accessor :zk
33
-
34
- # our absolute lock node path
35
- #
36
- # ex. '/_zklocking/foobar/__blah/lock000000007'
37
- #
38
- # @private
39
- attr_reader :lock_path
40
-
41
- # @private
42
- attr_reader :root_lock_path
43
-
44
- # @private
45
- def self.digit_from_lock_path(path)
46
- path[/0*(\d+)$/, 1].to_i
47
- end
48
-
49
- def initialize(zookeeper_client, name, root_lock_node = "/_zklocking")
50
- @zk = zookeeper_client
51
- @root_lock_node = root_lock_node
52
- @path = name
53
- @locked = false
54
- @waiting = false
55
- @root_lock_path = "#{@root_lock_node}/#{@path.gsub("/", "__")}"
56
- end
57
-
58
- # block caller until lock is aquired, then yield
59
- def with_lock
60
- lock!(true)
61
- yield
62
- ensure
63
- unlock!
64
- end
65
-
66
- # the basename of our lock path
67
- #
68
- # for the lock_path '/_zklocking/foobar/__blah/lock000000007'
69
- # lock_basename is 'lock000000007'
70
- #
71
- # returns nil if lock_path is not set
72
- def lock_basename
73
- lock_path and File.basename(lock_path)
74
- end
75
-
76
- def locked?
77
- false|@locked
78
- end
79
-
80
- def unlock!
81
- if @locked
82
- cleanup_lock_path!
83
- @locked = false
84
- true
85
- end
86
- end
87
-
88
- # returns true if this locker is waiting to acquire lock
89
- def waiting? #:nodoc:
90
- false|@waiting
91
- end
92
-
93
- protected
94
- def in_waiting_status
95
- w, @waiting = @waiting, true
96
- yield
97
- ensure
98
- @waiting = w
99
- end
100
-
101
- def digit_from(path)
102
- self.class.digit_from_lock_path(path)
103
- end
104
-
105
- def lock_children(watch=false)
106
- @zk.children(root_lock_path, :watch => watch)
107
- end
108
-
109
- def ordered_lock_children(watch=false)
110
- lock_children(watch).tap do |ary|
111
- ary.sort! { |a,b| digit_from(a) <=> digit_from(b) }
112
- end
113
- end
114
-
115
- def create_root_path!
116
- @zk.mkdir_p(@root_lock_path)
117
- end
118
-
119
- # prefix is the string that will appear in front of the sequence num,
120
- # defaults to 'lock'
121
- def create_lock_path!(prefix='lock')
122
- @lock_path = @zk.create("#{root_lock_path}/#{prefix}", "", :mode => :ephemeral_sequential)
123
- logger.debug { "got lock path #{@lock_path}" }
124
- @lock_path
125
- rescue Exceptions::NoNode
126
- create_root_path!
127
- retry
128
- end
129
-
130
- def cleanup_lock_path!
131
- logger.debug { "removing lock path #{@lock_path}" }
132
- @zk.delete(@lock_path)
133
- @zk.delete(root_lock_path) rescue Exceptions::NotEmpty
134
- end
118
+ # @private
119
+ class WeAreTheLowestLockNumberException < StandardError
135
120
  end
121
+ end # Locker
122
+ end # ZK
136
123
 
137
- class SharedLocker < LockerBase
138
- def lock!(blocking=false)
139
- return true if @locked
140
- create_lock_path!(SHARED_LOCK_PREFIX)
141
-
142
- if got_read_lock?
143
- @locked = true
144
- elsif blocking
145
- in_waiting_status do
146
- block_until_read_lock!
147
- end
148
- else
149
- # we didn't get the lock, and we're not gonna wait around for it, so
150
- # clean up after ourselves
151
- cleanup_lock_path!
152
- false
153
- end
154
- end
155
-
156
- def lock_number #:nodoc:
157
- @lock_number ||= (lock_path and digit_from(lock_path))
158
- end
159
-
160
- # returns the sequence number of the next lowest write lock node
161
- #
162
- # raises NoWriteLockFoundException when there are no write nodes with a
163
- # sequence less than ours
164
- #
165
- def next_lowest_write_lock_num #:nodoc:
166
- digit_from(next_lowest_write_lock_name)
167
- end
168
-
169
- # the next lowest write lock number to ours
170
- #
171
- # so if we're "read010" and the children of the lock node are:
172
- #
173
- # %w[write008 write009 read010 read011]
174
- #
175
- # then this method will return write009
176
- #
177
- # raises NoWriteLockFoundException if there were no write nodes with an
178
- # index lower than ours
179
- #
180
- def next_lowest_write_lock_name #:nodoc:
181
- ary = ordered_lock_children()
182
- my_idx = ary.index(lock_basename) # our idx would be 2
183
-
184
- not_found = lambda { raise NoWriteLockFoundException }
185
-
186
- ary[0..my_idx].reverse.find(not_found) { |n| n =~ /^#{EXCLUSIVE_LOCK_PREFIX}/ }
187
- end
188
-
189
- def got_read_lock? #:nodoc:
190
- false if next_lowest_write_lock_num
191
- rescue NoWriteLockFoundException
192
- true
193
- end
194
-
195
- protected
196
- # TODO: make this generic, can either block or non-block
197
- def block_until_read_lock!
198
- begin
199
- path = [root_lock_path, next_lowest_write_lock_name].join('/')
200
- logger.debug { "SharedLocker#block_until_read_lock! path=#{path.inspect}" }
201
- @zk.block_until_node_deleted(path)
202
- rescue NoWriteLockFoundException
203
- # next_lowest_write_lock_name may raise NoWriteLockFoundException,
204
- # which means we should not block as we have the lock (there is nothing to wait for)
205
- end
206
-
207
- @locked = true
208
- end
209
- end # SharedLocker
210
-
211
- # An exclusive lock implementation
212
- class ExclusiveLocker < LockerBase
213
- def lock!(blocking=false)
214
- return true if @locked
215
- create_lock_path!(EXCLUSIVE_LOCK_PREFIX)
216
-
217
- if got_write_lock?
218
- @locked = true
219
- elsif blocking
220
- in_waiting_status do
221
- block_until_write_lock!
222
- end
223
- else
224
- cleanup_lock_path!
225
- false
226
- end
227
- end
228
-
229
- protected
230
- # the node that is next-lowest in sequence number to ours, the one we
231
- # watch for updates to
232
- def next_lowest_node
233
- ary = ordered_lock_children()
234
- my_idx = ary.index(lock_basename)
235
-
236
- raise WeAreTheLowestLockNumberException if my_idx == 0
237
-
238
- ary[(my_idx - 1)]
239
- end
240
-
241
- def got_write_lock?
242
- ordered_lock_children.first == lock_basename
243
- end
244
-
245
- def block_until_write_lock!
246
- begin
247
- path = [root_lock_path, next_lowest_node].join('/')
248
- logger.debug { "SharedLocker#block_until_write_lock! path=#{path.inspect}" }
249
- @zk.block_until_node_deleted(path)
250
- rescue WeAreTheLowestLockNumberException
251
- end
252
-
253
- @locked = true
254
- end
255
- end # ExclusiveLocker
256
- end # SharedLocker
257
- end # ZooKeeper
124
+ require 'zk/locker/locker_base'
125
+ require 'zk/locker/shared_locker'
126
+ require 'zk/locker/exclusive_locker'
258
127
 
@@ -1,9 +1,13 @@
1
1
  module ZK
2
2
  # implements a simple message queue based on Zookeeper recipes
3
+ #
3
4
  # @see http://hadoop.apache.org/zookeeper/docs/r3.0.0/recipes.html#sc_recipes_Queues
5
+ #
4
6
  # these are good for low-volume queues only
7
+ #
5
8
  # because of the way zookeeper works, all message *titles* have to be read into memory
6
9
  # in order to see what message to process next
10
+ #
7
11
  # @example
8
12
  # queue = zk.queue("somequeue")
9
13
  # queue.publish(some_string)
@@ -29,9 +33,12 @@ module ZK
29
33
 
30
34
  # publish a message to the queue, you can (optionally) use message titles
31
35
  # to guarantee unique messages in the queue
32
- # @param [String] data - any arbitrary string value
33
- # @param optional [String] message_title - specify a unique message title for this
34
- # message
36
+ #
37
+ # @param [String] data any arbitrary string value
38
+ #
39
+ # @param [String] message_title specify a unique message title for this
40
+ # message (optional)
41
+ #
35
42
  def publish(data, message_title = nil)
36
43
  mode = :persistent_sequential
37
44
  if message_title
@@ -44,9 +51,9 @@ module ZK
44
51
  return false
45
52
  end
46
53
 
47
- # you barely ever need to actually use this method
48
- # but lets you remove a message from the queue by specifying
49
- # its title
54
+ # you barely ever need to actually use this method but lets you remove a
55
+ # message from the queue by specifying its title
56
+ #
50
57
  # @param [String] message_title the title of the message to remove
51
58
  def delete_message(message_title)
52
59
  full_path = "#{full_queue_path}/#{message_title}"
@@ -64,7 +71,9 @@ module ZK
64
71
  end
65
72
 
66
73
  # grab one message from the queue
74
+ #
67
75
  # used when you don't want to or can't subscribe
76
+ #
68
77
  # @see ZooKeeper::MessageQueue#subscribe
69
78
  def poll!
70
79
  find_and_process_next_available(messages)
@@ -74,6 +83,7 @@ module ZK
74
83
  # # subscribe like this:
75
84
  # subscribe {|title, data| handle_message!; true}
76
85
  # # returning true in the block deletes the message, false unlocks and requeues
86
+ #
77
87
  # @yield [title, data] yield to your block with the message title and the data of
78
88
  # the message
79
89
  def subscribe(&block)
data/lib/zk/pool.rb CHANGED
@@ -305,12 +305,13 @@ module ZK
305
305
  # pool.checkout do |zk|
306
306
  # zk.create("/mynew_path")
307
307
  # end
308
+ #
308
309
  class Simple < Bounded
309
310
  # initialize a connection pool using the same optons as ZK.new
310
- # @param String host the same arguments as ZK.new
311
- # @param Integer number_of_connections the number of connections to put in the pool
312
- # @param optional Hash opts Options to pass on to each connection
313
- # @return ZK::ClientPool
311
+ # @param [String] host the same arguments as ZK.new
312
+ # @param [Integer] number_of_connections the number of connections to put in the pool
313
+ # @param [Hash] opts Options to pass on to each connection
314
+ # @return [ZK::ClientPool]
314
315
  def initialize(host, number_of_connections=10, opts = {})
315
316
  opts = opts.dup
316
317
  opts[:max_clients] = opts[:min_clients] = number_of_connections.to_i