zkruby 3.4.3

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,310 @@
1
+ require 'socket'
2
+ require 'thread'
3
+ require 'monitor'
4
+
5
+ # Binding over standard ruby sockets
6
+ #
7
+ # Manages 3 threads per zookeeper session
8
+ #
9
+ # Read thread
10
+ # manages connecting to and reading from the tcp socket. Uses non blocking io to manage timeouts
11
+ # and initiate the required ping requests.
12
+ #
13
+ # Write thread
14
+ # each new connection spawns a new thread. Requests coming from the session in response
15
+ # to multiple threads are written to a blocking queue. While the connection is alive
16
+ # this thread reads from the queue and writes to the socket, all in blocking fashion
17
+ # TODO: Is it really ok to do a non-blocking read during a blocking write?
18
+ #
19
+ # Event thread
20
+ # All response and watch callbacks are put on another blocking queue to be read and executed
21
+ # by this thread.
22
+ #
23
+ # All interaction with the session is synchronized
24
+ #
25
+ # Client synchronous code is implemented with a condition variable that waits on the callback/errback
26
+ module ZooKeeper::RubyIO
27
+
28
+ class Connection
29
+ include ZooKeeper::Protocol
30
+ include Slf4r::Logger
31
+ include Socket::Constants
32
+
33
+ def initialize(host,port,timeout,session)
34
+ @session = session
35
+ @write_queue = Queue.new()
36
+
37
+ # JRuby cannot do non-blocking connects, which means there is
38
+ # no way to properly implement the connection-timeout
39
+ # See http://jira.codehaus.org/browse/JRUBY-5165
40
+ # In any case this should be encapsulated in TCPSocket.open(host,port,timeout)
41
+ if RUBY_PLATFORM == "java"
42
+ begin
43
+ sock = TCPSocket.new(host,port.to_i)
44
+ rescue Errno::ECONNREFUSED
45
+ logger.warn("TCP Connection refused to #{host}:#{port}")
46
+ sock = nil
47
+ end
48
+ else
49
+ addr = Socket.getaddrinfo(host, nil)
50
+ sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
51
+ sock.setsockopt(SOL_SOCKET, SO_LINGER, [0,-1].pack("ii"))
52
+ sock.setsockopt(SOL_TCP, TCP_NODELAY,[0].pack("i_"))
53
+ sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
54
+ begin
55
+ sock.connect_nonblock(sockaddr)
56
+ rescue Errno::EINPROGRESS
57
+ resp = IO.select(nil, [sock], nil, timeout)
58
+ begin
59
+ sock.connect_nonblock(sockaddr)
60
+ rescue Errno::ECONNREFUSED
61
+ logger.warn("Connection refused to #{ host }:#{ port }")
62
+ sock = nil
63
+ rescue Errno::EISCONN
64
+ end
65
+ end
66
+ end
67
+ @socket = sock
68
+ Thread.new(sock) { |sock| write_loop(sock) } if sock
69
+ end
70
+
71
+ # This is called from random client threads, but only within
72
+ # a @session.synchronized() block
73
+ def send_data(data)
74
+ @write_queue.push(data)
75
+ end
76
+
77
+ # Since this runs in its very own thread
78
+ # we can use boring blocking IO
79
+ def write_loop(socket)
80
+ Thread.current[:name] = "ZooKeeper::RubyIO::WriteLoop"
81
+ begin
82
+ while socket
83
+ data = @write_queue.pop()
84
+ if socket.write(data) != data.length()
85
+ #TODO - will this really ever happen
86
+ logger.warn("Incomplete write!")
87
+ end
88
+ logger.debug { "Sending: " + data.unpack("H*")[0] }
89
+ end
90
+ rescue Exception => ex
91
+ logger.warn("Exception in write loop",ex)
92
+ disconnect()
93
+ end
94
+
95
+ end
96
+
97
+ def read_loop()
98
+ socket = @socket
99
+ ping = 0
100
+ while socket # effectively forever
101
+ begin
102
+ data = socket.read_nonblock(1024)
103
+ logger.debug { "Received (#{data.length})" + data.unpack("H*")[0] }
104
+ receive_data(data)
105
+ ping = 0
106
+ rescue IO::WaitReadable
107
+ select_result = IO.select([socket],[],[],@session.ping_interval)
108
+ unless select_result
109
+ ping += 1
110
+ # two timeouts in a row mean we need to send a ping
111
+ case ping
112
+ when 1 ; @session.synchronize { @session.ping() }
113
+ when 2
114
+ logger.debug{"No response to ping in #{@session.ping_interval}*2"}
115
+ break
116
+ end
117
+ end
118
+ rescue EOFError
119
+ logger.debug { "EOF reading from socket" }
120
+ break
121
+ rescue Exception => ex
122
+ logger.warn( "#{ex.class} exception in readloop",ex )
123
+ break
124
+ end
125
+ end
126
+ disconnect()
127
+ end
128
+
129
+ def disconnect()
130
+ socket = @socket
131
+ @socket = nil
132
+ socket.close if socket
133
+ rescue Exception => ex
134
+ #oh well
135
+ logger.debug("Exception closing socket",ex)
136
+ end
137
+
138
+ # Protocol requirement
139
+ def receive_records(packet_io)
140
+ @session.synchronize { @session.receive_records(packet_io) }
141
+ end
142
+
143
+ end #Class connection
144
+
145
+ class Binding
146
+ include Slf4r::Logger
147
+ attr_reader :session
148
+ def self.available?
149
+ true
150
+ end
151
+
152
+ def self.context(&context_block)
153
+ yield Thread
154
+ end
155
+
156
+ def initialize()
157
+ @event_queue = Queue.new()
158
+ end
159
+
160
+ def pop_event_queue()
161
+ queued = @event_queue.pop()
162
+ return false unless queued
163
+ logger.debug { "Invoking #{queued[0]}" }
164
+ callback,*args = queued
165
+ callback.call(*args)
166
+ logger.debug { "Completed #{queued[0]}" }
167
+ return true
168
+ rescue Exception => ex
169
+ logger.warn( "Exception in event thread", ex )
170
+ return true
171
+ end
172
+
173
+ def start(client,session)
174
+ @session = session
175
+ @session.extend(MonitorMixin)
176
+
177
+ # start the event thread
178
+ @event_thread = Thread.new() do
179
+ Thread.current[:name] = "ZooKeeper::RubyIO::EventLoop"
180
+
181
+ # In this thread, the current client is always this client!
182
+ Thread.current[ZooKeeper::CURRENT] = [client]
183
+ loop do
184
+ break unless pop_event_queue()
185
+ end
186
+ end
187
+
188
+ # and the read thread
189
+ Thread.new() do
190
+ begin
191
+ Thread.current[:name] = "ZooKeeper::RubyIO::ReadLoop"
192
+ conn = session.synchronize { session.start(); session.conn() } # will invoke connect
193
+ loop do
194
+ break unless conn
195
+ conn.read_loop()
196
+ conn = session.synchronize { session.disconnected(); session.conn() }
197
+ end
198
+ #event of death
199
+ logger.debug("Pushing nil (event of death) to event queue")
200
+ @event_queue.push(nil)
201
+ rescue Exception => ex
202
+ logger.error( "Exception in session thread", ex )
203
+ end
204
+ end
205
+ end
206
+
207
+ # session callback, IO thread
208
+ def connect(host,port,delay,timeout)
209
+ sleep(delay)
210
+ conn = Connection.new(host,port,timeout,session)
211
+ session.synchronize() { session.prime_connection(conn) }
212
+ end
213
+
214
+
215
+ def close(&callback)
216
+ op = AsyncOp.new(self,&callback)
217
+
218
+ session.synchronize do
219
+ session.close() do |error,response|
220
+ op.resume(error,response)
221
+ end
222
+ end
223
+
224
+ op
225
+
226
+ end
227
+
228
+ def queue_request(*args,&callback)
229
+
230
+ op = AsyncOp.new(self,&callback)
231
+
232
+ session.synchronize do
233
+ session.queue_request(*args) do |error,response|
234
+ op.resume(error,response)
235
+ end
236
+ end
237
+
238
+ op
239
+ end
240
+
241
+ def event_thread?
242
+ Thread.current.equal?(@event_thread)
243
+ end
244
+
245
+ def invoke(*args)
246
+ @event_queue.push(args)
247
+ end
248
+
249
+ end #Binding
250
+
251
+ class AsyncOp < ::ZooKeeper::AsyncOp
252
+
253
+ def initialize(binding,&callback)
254
+ @mutex = Monitor.new
255
+ @cv = @mutex.new_cond()
256
+ @callback = callback
257
+ @rubyio = binding
258
+ end
259
+
260
+ def resume(error,response)
261
+ mutex.synchronize do
262
+ @error = error
263
+ @result = nil
264
+ begin
265
+ @result = @callback.call(response) unless error
266
+ rescue StandardError => ex
267
+ @error = ex
268
+ end
269
+
270
+ if @error && @errback
271
+ begin
272
+ @result = @errback.call(@error)
273
+ @error = nil
274
+ rescue StandardError => ex
275
+ @error = ex
276
+ end
277
+ end
278
+
279
+ cv.signal()
280
+ end
281
+ end
282
+
283
+ private
284
+ attr_reader :mutex, :cv, :error, :result
285
+
286
+ def set_error_handler(errback)
287
+ @errback = errback
288
+ end
289
+
290
+ def wait_value()
291
+ if @rubyio.event_thread?
292
+ #Waiting in the event thread (eg made a synchronous call inside a callback)
293
+ #Keep processing events until we are resumed
294
+ until defined?(@result)
295
+ break unless @rubyio.pop_event_queue()
296
+ end
297
+ else
298
+ mutex.synchronize do
299
+ cv.wait()
300
+ end
301
+ end
302
+
303
+ raise error if error
304
+ result
305
+ end
306
+ end
307
+
308
+ end #ZooKeeper::RubyIO
309
+ # Add our binding
310
+ ZooKeeper.add_binding(ZooKeeper::RubyIO::Binding)
@@ -0,0 +1,445 @@
1
+ require 'set'
2
+ module ZooKeeper
3
+
4
+ # Represents an session that may span connections
5
+ class Session
6
+
7
+ DEFAULT_TIMEOUT = 4
8
+ DEFAULT_CONNECT_DELAY = 0.2
9
+ DEFAULT_PORT = 2181
10
+
11
+ include Slf4r::Logger
12
+
13
+ attr_reader :ping_interval
14
+ attr_reader :ping_logger
15
+ attr_reader :timeout
16
+ attr_reader :conn
17
+ attr_accessor :watcher
18
+
19
+ def initialize(binding,addresses,options=nil)
20
+
21
+ @binding = binding
22
+
23
+ @addresses = parse_addresses(addresses)
24
+ parse_options(options)
25
+
26
+ # These are the server states
27
+ # :disconnected, :connected, :auth_failed, :expired
28
+ @keeper_state = nil
29
+
30
+ # Client state is
31
+ # :ready, :closing, :closed
32
+ @client_state = :ready
33
+
34
+ @xid=0
35
+ @pending_queue = []
36
+
37
+ # Create the watch list
38
+ # hash by watch type of hashes by path of set of watchers
39
+ @watches = [ :children, :data, :exists ].inject({}) do |ws,wtype|
40
+ ws[wtype] = Hash.new() { |h,k| h[k] = Set.new() }
41
+ ws
42
+ end
43
+
44
+ @watcher = nil
45
+
46
+ @ping_logger = Slf4r::LoggerFacade.new("ZooKeeper::Session::Ping")
47
+
48
+ end
49
+
50
+ def chroot(path)
51
+ return @chroot + path
52
+ end
53
+
54
+ def unchroot(path)
55
+ return path unless path
56
+ path.slice(@chroot.length..-1)
57
+ end
58
+
59
+ # close won't run your block if the connection is
60
+ # already closed, so this is how you can check
61
+ def closed?
62
+ @client_state == :closed
63
+ end
64
+
65
+ # Connection API - testing whether to send a ping
66
+ def connected?()
67
+ @keeper_state == :connected
68
+ end
69
+
70
+ # Connection API - Injects a new connection that is ready to receive records
71
+ # @param conn that responds to #send_records(record...) and #disconnect()
72
+ def prime_connection(conn)
73
+ @conn = conn
74
+ send_session_connect()
75
+ send_auth_data()
76
+ reset_watches()
77
+ end
78
+
79
+
80
+ # Connection API - called when data is available, reads and processes one packet/event
81
+ # @param io <IO>
82
+ def receive_records(io)
83
+ case @keeper_state
84
+ when :disconnected
85
+ complete_connection(io)
86
+ when :connected
87
+ process_reply(io)
88
+ else
89
+ logger.warn { "Receive packet for closed session #{@keeper_state}" }
90
+ end
91
+ end
92
+
93
+ # Connection API - called when no data has been received for #ping_interval
94
+ def ping()
95
+ if @keeper_state == :connected
96
+ ping_logger.debug { "Ping send" }
97
+ hdr = Proto::RequestHeader.new(:xid => -2, :_type => 11)
98
+ conn.send_records(hdr)
99
+ end
100
+ end
101
+
102
+ # Connection API - called when the connection has dropped from either end
103
+ def disconnected()
104
+ @conn = nil
105
+ logger.info { "Disconnected id=#{@session_id}, keeper=:#{@keeper_state}, client=:#{@client_state}" }
106
+
107
+ # We keep trying to reconnect until the session expiration time is reached
108
+ @disconnect_time = Time.now if @keeper_state == :connected
109
+ time_since_first_disconnect = (Time.now - @disconnect_time)
110
+
111
+ if @client_state == :closed || time_since_first_disconnect > timeout
112
+ session_expired()
113
+ else
114
+ # if we are connected then everything in the pending queue has been sent so
115
+ # we must clear
116
+ # if not, then we'll keep them and hope the next reconnect works
117
+ if @keeper_state == :connected
118
+ clear_pending_queue(:disconnected)
119
+ invoke_watch(@watcher,KeeperState::DISCONNECTED,nil,WatchEvent::NONE) if @watcher
120
+ end
121
+ @keeper_state = :disconnected
122
+ reconnect()
123
+ end
124
+ end
125
+
126
+ # Start the session - called by the ProtocolBinding
127
+ def start()
128
+ raise ProtocolError, "Already started!" unless @keeper_state.nil?
129
+ @keeper_state = :disconnected
130
+ @disconnect_time = Time.now
131
+ logger.debug("Starting new zookeeper client session")
132
+ reconnect()
133
+ end
134
+
135
+ def queue_request(request,op,opcode,response=nil,watch_type=nil,watcher=nil,ptype=Packet,&callback)
136
+ raise Error.SESSION_EXPIRED, "Session expired due to client state #{@client_state}" unless @client_state == :ready
137
+ watch_type, watcher = resolve_watcher(watch_type,watcher)
138
+
139
+ xid = next_xid
140
+
141
+ packet = ptype.new(xid,op,opcode,request,response,watch_type,watcher, callback)
142
+
143
+ queue_packet(packet)
144
+ end
145
+
146
+ def close(&callback)
147
+ case @client_state
148
+ when :ready
149
+ # we keep the requested block in a close packet
150
+ @close_packet = ClosePacket.new(next_xid(),:close,-11,nil,nil,nil,nil,callback)
151
+ close_packet = @close_packet
152
+ @client_state = :closing
153
+
154
+ # If there are other requests in flight, then we wait for them to finish
155
+ # before sending the close packet since it immediately causes the socket
156
+ # to close.
157
+ queue_close_packet_if_necessary()
158
+ @close_packet
159
+ when :closed, :closing
160
+ raise ProtocolError, "Already closed"
161
+ else
162
+ raise ProtocolError, "Unexpected state #{@client_state}"
163
+ end
164
+ end
165
+
166
+ private
167
+ attr_reader :watches
168
+ attr_reader :binding
169
+
170
+ def parse_addresses(addresses)
171
+ case addresses
172
+ when String
173
+ parse_addresses(addresses.split(","))
174
+ when Array
175
+ result = addresses.collect() { |addr| parse_address(addr) }
176
+ #Randomise the connection order
177
+ result.shuffle!
178
+ else
179
+ raise ArgumentError "Not able to parse addresses from #{addresses}"
180
+ end
181
+ end
182
+
183
+ def parse_address(address)
184
+ case address
185
+ when String
186
+ host,port = address.split(":")
187
+ port = DEFAULT_PORT unless port
188
+ [host,port]
189
+ when Array
190
+ address[0..1]
191
+ end
192
+ end
193
+
194
+ def parse_options(options)
195
+ @timeout = options.fetch(:timeout,DEFAULT_TIMEOUT)
196
+ @max_connect_delay = options.fetch(:connect_delay,DEFAULT_CONNECT_DELAY)
197
+ @connect_timeout = options.fetch(:connect_timeout,@timeout * 1.0 / 7.0)
198
+ @scheme = options.fetch(:scheme,nil)
199
+ @auth = options.fetch(:auth,nil)
200
+ @chroot = options.fetch(:chroot,"").chomp("/")
201
+ end
202
+
203
+ def reconnect()
204
+
205
+ #Rotate address
206
+ host,port = @addresses.shift
207
+ @addresses.push([host,port])
208
+
209
+ delay = rand() * @max_connect_delay
210
+
211
+ logger.debug { "Connecting id=#{@session_id} to #{host}:#{port} with delay=#{delay}, timeout=#{@connect_timeout}" }
212
+ binding.connect(host,port,delay,@connect_timeout)
213
+ end
214
+
215
+
216
+ def session_expired(reason=:expired)
217
+ clear_pending_queue(reason)
218
+
219
+ invoke_response(*@close_packet.error(reason)) if @close_packet
220
+
221
+ if @client_state == :closed
222
+ logger.info { "Session closed id=#{@session_id}, keeper=:#{@keeper_state}, client=:#{@client_state}" }
223
+ else
224
+ logger.warn { "Session expired id=#{@session_id}, keeper=:#{@keeper_state}, client=:#{@client_state}" }
225
+ end
226
+
227
+ invoke_watch(@watcher,KeeperState::EXPIRED,nil,WatchEvent::NONE) if @watcher
228
+ @keeper_state = reason
229
+ @client_state = :closed
230
+ end
231
+
232
+ def complete_connection(response)
233
+ result = Proto::ConnectResponse.read(response)
234
+ if (result.time_out <= 0)
235
+ #We're dead!
236
+ session_expired()
237
+ else
238
+ @timeout = result.time_out.to_f / 1000.0
239
+ @keeper_state = :connected
240
+
241
+ # Why 2 / 7 of the timeout?. If a binding sees no server response in this period it is required to
242
+ # generate a ping request
243
+ # if 2 periods go by without activity it is required to disconnect
244
+ # so we are already more than half way through the session timeout
245
+ # and we need to give ourselves time to reconnect to another server
246
+ @ping_interval = @timeout * 2.0 / 7.0
247
+ @session_id = result.session_id
248
+ @session_passwd = result.passwd
249
+ logger.info { "Connected session_id=#{@session_id}, timeout=#{@time_out}, ping=#{@ping_interval}" }
250
+
251
+ logger.debug { "Sending #{@pending_queue.length} queued packets" }
252
+ @pending_queue.each { |p| send_packet(p) }
253
+
254
+ queue_close_packet_if_necessary()
255
+ invoke_watch(@watcher,KeeperState::CONNECTED,nil,WatchEvent::NONE) if @watcher
256
+ end
257
+ end
258
+
259
+ def send_session_connect()
260
+ req = Proto::ConnectRequest.new( :timeout => timeout )
261
+ req.last_zxid_seen = @last_zxid_seen if @last_zxid_seen
262
+ req.session_id = @session_id if @session_id
263
+ req.passwd = @session_passwd if @session_passwd
264
+
265
+ conn.send_records(req)
266
+ end
267
+
268
+ def send_auth_data()
269
+ if @scheme
270
+ req = Proto::AuthPacket.new(:scheme => @scheme, :auth => @auth)
271
+ packet = Packet.new(-4,:auth,100,req,nil,nil,nil,nil)
272
+ send_packet(packet)
273
+ end
274
+ end
275
+ # Watches are dropped on disconnect, we reset them here
276
+ # dropping connections can be a good way of cleaning up on the server side
277
+ # #TODO If watch reset is disabled the watches will be notified of connection loss
278
+ # otherwise they will be seemlessly re-added
279
+ # This way a watch is only ever triggered exactly once
280
+ def reset_watches()
281
+ unless watches[:children].empty? && watches[:data].empty? && watches[:exists].empty?
282
+ req = Proto::SetWatches.new()
283
+ req.relative_zxid = @last_zxid_seen
284
+ req.data_watches = watches[:data].keys
285
+ req.child_watches = watches[:children].keys
286
+ req.exist_watches = watches[:exists].keys
287
+
288
+ packet = Packet.new(-8,:set_watches,101,req,nil,nil,nil,nil)
289
+ send_packet(packet)
290
+ end
291
+ end
292
+
293
+ def process_reply(packet_io)
294
+ header = Proto::ReplyHeader.read(packet_io)
295
+
296
+ case header.xid.to_i
297
+ when -2
298
+ ping_logger.debug { "Ping reply" }
299
+ when -4
300
+ logger.debug { "Auth reply" }
301
+ session_expired(:auth_failed) unless header.err.to_i == 0
302
+ when -1
303
+ #Watch notification
304
+ event = Proto::WatcherEvent.read(packet_io)
305
+ logger.debug { "Watch notification #{event.inspect} " }
306
+ process_watch_notification(event.state.to_i,event.path,event._type.to_i)
307
+ when -8
308
+ #Reset watch reply
309
+ logger.debug { "SetWatch reply"}
310
+ #TODO If error, send :disconnected to all watches
311
+ else
312
+ # A normal packet reply. They should come in the order we sent them
313
+ # so we just match it to the packet at the front of the queue
314
+ packet = @pending_queue.shift
315
+ logger.debug { "Packet reply: #{packet.inspect}" }
316
+
317
+ if (packet.xid.to_i != header.xid.to_i)
318
+
319
+ logger.error { "Bad XID! expected=#{packet.xid}, received=#{header.xid}" }
320
+
321
+ # Treat this like a dropped connection, and then force the connection
322
+ # to be dropped. But wait for the connection to notify us before
323
+ # we actually update our keeper_state
324
+ invoke_response(*packet.error(:disconnected))
325
+ @conn.disconnect()
326
+ else
327
+
328
+ @last_zxid_seen = header.zxid
329
+
330
+ callback, error, response, watch_type = packet.result(header.err.to_i)
331
+ invoke_response(callback, error, response, packet_io)
332
+
333
+ if (watch_type)
334
+ @watches[watch_type][packet.path] << packet.watcher
335
+ logger.debug { "Registered #{packet.watcher} for type=#{watch_type} at #{packet.path}" }
336
+ end
337
+ queue_close_packet_if_necessary()
338
+ end
339
+ end
340
+ end
341
+
342
+
343
+ def process_watch_notification(state,path,event)
344
+
345
+ watch_event = WatchEvent.fetch(event)
346
+ watch_types = watch_event.watch_types()
347
+
348
+ keeper_state = KeeperState.fetch(state)
349
+
350
+ watches = watch_types.inject(Set.new()) do | result, watch_type |
351
+ more_watches = @watches[watch_type].delete(path)
352
+ result.merge(more_watches) if more_watches
353
+ result
354
+ end
355
+
356
+ if watches.empty?
357
+ logger.warn { "Received notification for unregistered watch #{state} #{path} #{event}" }
358
+ end
359
+ watches.each { | watch | invoke_watch(watch,keeper_state,path,watch_event) }
360
+
361
+ end
362
+
363
+ def invoke_watch(watch,state,path,event)
364
+ logger.debug { "Watch #{watch} triggered with #{state}, #{path}. #{event}" }
365
+ if watch.respond_to?(:process_watch)
366
+ callback = Proc.new() { |state,path,event| watch.process_watch(state,path,event) }
367
+ elsif watch.respond_to?(:call)
368
+ callback = watch
369
+ else
370
+ raise ProtocolError("Bad watcher #{watch}")
371
+ end
372
+
373
+ binding.invoke(callback,state,unchroot(path),event)
374
+ end
375
+
376
+ def clear_pending_queue(reason)
377
+ @pending_queue.each { |p| invoke_response(*p.error(reason)) }
378
+ @pending_queue.clear
379
+ end
380
+
381
+ def queue_close_packet_if_necessary
382
+ if @pending_queue.empty? && @keeper_state == :connected && @close_packet
383
+ logger.debug { "Sending close packet!" }
384
+ @client_state = :closed
385
+ queue_packet(@close_packet)
386
+ @close_packet = nil
387
+ end
388
+ end
389
+
390
+ def invoke_response(callback,error,response,packet_io = nil)
391
+ if callback
392
+
393
+ result = if error
394
+ nil
395
+ elsif response.respond_to?(:read) && packet_io
396
+ response.read(packet_io)
397
+ elsif response
398
+ response
399
+ else
400
+ nil
401
+ end
402
+
403
+ logger.debug { "Invoking response cb=#{callback} err=#{error} resp=#{result}" }
404
+ binding.invoke(callback,error,result)
405
+ end
406
+ end
407
+
408
+ def resolve_watcher(watch_type,watcher)
409
+ if watcher == true && @watcher
410
+ #the actual TrueClass refers to the default watcher
411
+ watcher = @watcher
412
+ elsif watcher.respond_to?(:call) || watcher.respond_to?(:process_watch)
413
+ # ok a proc or quacks like a watcher
414
+ elsif watcher
415
+ # something, but not something we can handle
416
+ raise ArgumentError, "Not a watcher #{watcher.inspect}"
417
+ else
418
+ watch_type = nil
419
+ end
420
+ [watch_type,watcher]
421
+ end
422
+
423
+
424
+ def queue_packet(packet)
425
+ @pending_queue.push(packet)
426
+ logger.debug { "Queued: #{packet.inspect}" }
427
+
428
+ if @keeper_state == :connected
429
+ send_packet(packet)
430
+ end
431
+ end
432
+
433
+ def next_xid
434
+ @xid += 1
435
+ end
436
+
437
+ def send_packet(packet)
438
+ records = [] << Proto::RequestHeader.new(:xid => packet.xid, :_type => packet.opcode)
439
+ records << packet.request if packet.request
440
+ conn.send_records(*records)
441
+ end
442
+
443
+
444
+ end # Session
445
+ end