zkruby 3.4.3 → 3.4.4.rc3

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.
@@ -1,8 +1,29 @@
1
1
  require 'set'
2
+ require 'strand'
3
+ require 'strand/monitor'
4
+ #TODO Move async_op into this file
5
+ require 'zkruby/async_op'
2
6
  module ZooKeeper
3
7
 
4
- # Represents an session that may span connections
8
+ # Represents a session that may span connect/disconnect
9
+ #
10
+ # @note this is a private API not intended for client use
5
11
  class Session
12
+ # TODO this class is too big
13
+
14
+ # There are multiple threads of execution involved in a session
15
+ # Client threads - send requests
16
+ # Connection/Read thread
17
+ # EventLoop - callback execution
18
+ #
19
+ # All client activity is synchronised on this session as a monitor
20
+ # The read thread synchronises with client activity
21
+ # only around connection/disconnection
22
+ # ie any changes to @session_state which always happens in
23
+ # conjunction with processing all entries in @pending_queue
24
+ #
25
+ # All interaction with the event loop occurs via a Queue
26
+ include Strand::MonitorMixin
6
27
 
7
28
  DEFAULT_TIMEOUT = 4
8
29
  DEFAULT_CONNECT_DELAY = 0.2
@@ -16,24 +37,30 @@ module ZooKeeper
16
37
  attr_reader :conn
17
38
  attr_accessor :watcher
18
39
 
19
- def initialize(binding,addresses,options=nil)
20
-
21
- @binding = binding
22
-
40
+ # @api zookeeper
41
+ # See {ZooKeeper.connect}
42
+ def initialize(addresses,options=nil)
43
+ super()
23
44
  @addresses = parse_addresses(addresses)
24
45
  parse_options(options)
25
46
 
26
- # These are the server states
27
- # :disconnected, :connected, :auth_failed, :expired
28
- @keeper_state = nil
47
+ # our transaction id
48
+ @xid=0
29
49
 
30
- # Client state is
31
- # :ready, :closing, :closed
32
- @client_state = :ready
50
+ # The state of the connection, nil, :disconnected, :connected, :closed, :expired
51
+ @session_state = nil
33
52
 
34
- @xid=0
53
+ # The connection we'll send packets to
54
+ @conn = nil
55
+
56
+ # The list of pending requests
57
+ # When disconnected this builds up a list to send through when we are connected
58
+ # When connected represents the order in which we expect to see responses
35
59
  @pending_queue = []
36
60
 
61
+ # Client state is :ready, :closing, :closed
62
+ @client_state = :ready
63
+
37
64
  # Create the watch list
38
65
  # hash by watch type of hashes by path of set of watchers
39
66
  @watches = [ :children, :data, :exists ].inject({}) do |ws,wtype|
@@ -41,33 +68,14 @@ module ZooKeeper
41
68
  ws
42
69
  end
43
70
 
71
+ # the default watcher
44
72
  @watcher = nil
45
73
 
46
74
  @ping_logger = Slf4r::LoggerFacade.new("ZooKeeper::Session::Ping")
47
-
48
- end
49
-
50
- def chroot(path)
51
- return @chroot + path
52
75
  end
53
76
 
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
77
+ # @api connection
78
+ # Injects a new connection that is ready to receive records
71
79
  # @param conn that responds to #send_records(record...) and #disconnect()
72
80
  def prime_connection(conn)
73
81
  @conn = conn
@@ -76,96 +84,157 @@ module ZooKeeper
76
84
  reset_watches()
77
85
  end
78
86
 
79
-
80
- # Connection API - called when data is available, reads and processes one packet/event
81
- # @param io <IO>
87
+ # @api connection
88
+ # called when data is available, reads and processes one packet/event
89
+ # @param [IO] io
82
90
  def receive_records(io)
83
- case @keeper_state
91
+ case @session_state
84
92
  when :disconnected
85
93
  complete_connection(io)
86
94
  when :connected
87
95
  process_reply(io)
88
96
  else
89
- logger.warn { "Receive packet for closed session #{@keeper_state}" }
97
+ logger.warn { "Receive packet for closed session #{@session_state}" }
90
98
  end
91
99
  end
92
100
 
93
- # Connection API - called when no data has been received for #ping_interval
101
+ # @api connection
102
+ # Connection API - testing whether to send a ping
103
+ def connected?()
104
+ @session_state == :connected
105
+ end
106
+
107
+
108
+ # @api connection
109
+ # called when no data has been received for #ping_interval
94
110
  def ping()
95
- if @keeper_state == :connected
111
+ if connected?
96
112
  ping_logger.debug { "Ping send" }
97
113
  hdr = Proto::RequestHeader.new(:xid => -2, :_type => 11)
98
- conn.send_records(hdr)
114
+ conn.send_records(hdr)
99
115
  end
100
116
  end
101
117
 
102
- # Connection API - called when the connection has dropped from either end
118
+ # @api connection
119
+ # called when the connection has dropped from either end
103
120
  def disconnected()
104
- @conn = nil
105
- logger.info { "Disconnected id=#{@session_id}, keeper=:#{@keeper_state}, client=:#{@client_state}" }
121
+ logger.info { "Disconnected id=#{@session_id}, keeper=:#{@session_state}, client=:#{@client_state}" }
106
122
 
107
123
  # We keep trying to reconnect until the session expiration time is reached
108
- @disconnect_time = Time.now if @keeper_state == :connected
124
+ @disconnect_time = Time.now if @session_state == :connected
109
125
  time_since_first_disconnect = (Time.now - @disconnect_time)
110
126
 
111
- if @client_state == :closed || time_since_first_disconnect > timeout
112
- session_expired()
127
+ if @session_state == :closing
128
+ #We were expecting this disconnect
129
+ session_expired(:closed)
130
+ elsif time_since_first_disconnect > timeout
131
+ session_expired(:expired)
113
132
  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
133
+ if @session_state == :connected
134
+ #first disconnect
118
135
  clear_pending_queue(:disconnected)
119
136
  invoke_watch(@watcher,KeeperState::DISCONNECTED,nil,WatchEvent::NONE) if @watcher
137
+ @conn = nil
120
138
  end
121
- @keeper_state = :disconnected
122
- reconnect()
123
139
  end
124
140
  end
125
141
 
126
- # Start the session - called by the ProtocolBinding
127
- def start()
128
- raise ProtocolError, "Already started!" unless @keeper_state.nil?
129
- @keeper_state = :disconnected
142
+ # @api zookeeper
143
+ # See {ZooKeeper.connect}
144
+ def start(client)
145
+ raise ProtocolError, "Already started!" unless @session_state.nil?
146
+ @session_state = :disconnected
130
147
  @disconnect_time = Time.now
131
148
  logger.debug("Starting new zookeeper client session")
132
- reconnect()
149
+ @event_loop = EventLoop.new(client)
150
+ Strand.new {
151
+ begin
152
+ reconnect()
153
+ while active?
154
+ delay = rand() * @max_connect_delay
155
+ Strand.sleep(delay)
156
+ reconnect()
157
+ end
158
+ rescue Exception => ex
159
+ logger.error("Exception in connect loop",ex)
160
+ end
161
+ logger.debug("Session complete")
162
+ }
133
163
  end
134
164
 
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
165
 
139
- xid = next_xid
166
+ # @api client
167
+ def chroot(path)
168
+ return @chroot if path == "/"
169
+ return @chroot + path
170
+ end
140
171
 
141
- packet = ptype.new(xid,op,opcode,request,response,watch_type,watcher, callback)
172
+ # @api client
173
+ def unchroot(path)
174
+ return path unless path
175
+ path.slice(@chroot.length..-1)
176
+ end
142
177
 
143
- queue_packet(packet)
178
+ # @api client
179
+ def request(*args,&callback)
180
+ AsyncOp.new(@event_loop,callback) do |op|
181
+ queue_request(*args) do |error,response|
182
+ op.resume(error,response)
183
+ end
184
+ end
144
185
  end
145
186
 
187
+ # @api client
146
188
  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}"
189
+ AsyncOp.new(@event_loop,callback) do |op|
190
+ close_session() do |error,response|
191
+ op.resume(error,response)
192
+ end
163
193
  end
164
194
  end
165
195
 
166
196
  private
197
+ def active?
198
+ [:connected,:disconnected].include?(@session_state)
199
+ end
200
+
201
+ def queue_request(request,op,opcode,response=nil,watch_type=nil,watcher=nil,ptype=Packet,&callback)
202
+ synchronize do
203
+ raise ProtocolError, "Client closed #{@client_state}" unless @client_state == :ready
204
+ raise Error.SESSION_EXPIRED, "Session has expired #{@session_state}" unless active?
205
+ watch_type, watcher = resolve_watcher(watch_type,watcher)
206
+
207
+ xid = next_xid
208
+
209
+ packet = ptype.new(xid,op,opcode,request,response,watch_type,watcher, callback)
210
+
211
+ queue_packet(packet)
212
+ end
213
+ end
214
+
215
+ def close_session(&callback)
216
+ synchronize do
217
+ if @client_state == :ready
218
+ if active?
219
+ # we keep the requested block in a close packet but we don't send it
220
+ # until we've received all pending reponses
221
+ @close_packet = ClosePacket.new(next_xid(),:close,-11,nil,nil,nil,nil,callback)
222
+
223
+ # but we can force a response by sending a ping
224
+ ping()
225
+
226
+ else
227
+ # We've already expired put the close callback on the event loop
228
+ @event_loop.invoke_close(callback,nil,true)
229
+ end
230
+ @client_state = :closed
231
+ else
232
+ raise ProtocolError, "Client already #{@client_state}"
233
+ end
234
+ end
235
+ end
236
+
167
237
  attr_reader :watches
168
- attr_reader :binding
169
238
 
170
239
  def parse_addresses(addresses)
171
240
  case addresses
@@ -201,42 +270,36 @@ module ZooKeeper
201
270
  end
202
271
 
203
272
  def reconnect()
204
-
205
273
  #Rotate address
206
274
  host,port = @addresses.shift
207
275
  @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)
276
+ logger.debug { "Connecting id=#{@session_id} to #{host}:#{port} with timeout=#{@connect_timeout}" }
277
+ connect(host,port,@connect_timeout)
213
278
  end
214
279
 
215
-
216
280
  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}" }
281
+ if reason == :closed
282
+ logger.info { "Session closed id=#{@session_id}, keeper=:#{@session_state}, client=:#{@client_state}" }
223
283
  else
224
- logger.warn { "Session expired id=#{@session_id}, keeper=:#{@keeper_state}, client=:#{@client_state}" }
284
+ logger.warn { "Session expired reason=#{reason} id=#{@session_id}, keeper=:#{@session_state}, client=:#{@client_state}" }
225
285
  end
226
286
 
287
+ clear_pending_queue(reason)
288
+ #TODO Clients will want to distinguish between EXPIRED and CLOSED
227
289
  invoke_watch(@watcher,KeeperState::EXPIRED,nil,WatchEvent::NONE) if @watcher
228
- @keeper_state = reason
229
- @client_state = :closed
290
+ @event_loop.stop()
230
291
  end
231
292
 
232
293
  def complete_connection(response)
233
294
  result = Proto::ConnectResponse.read(response)
234
295
  if (result.time_out <= 0)
235
296
  #We're dead!
236
- session_expired()
297
+ session_expired()
237
298
  else
238
299
  @timeout = result.time_out.to_f / 1000.0
239
- @keeper_state = :connected
300
+ @session_id = result.session_id
301
+ @session_passwd = result.passwd
302
+ logger.info { "Connected session_id=#{@session_id}, timeout=#{@time_out}, ping=#{@ping_interval}" }
240
303
 
241
304
  # Why 2 / 7 of the timeout?. If a binding sees no server response in this period it is required to
242
305
  # generate a ping request
@@ -244,14 +307,14 @@ module ZooKeeper
244
307
  # so we are already more than half way through the session timeout
245
308
  # and we need to give ourselves time to reconnect to another server
246
309
  @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
310
 
251
- logger.debug { "Sending #{@pending_queue.length} queued packets" }
252
- @pending_queue.each { |p| send_packet(p) }
311
+ synchronize do
312
+ logger.debug { "Sending #{@pending_queue.length} queued packets" }
313
+ @session_state = :connected
314
+ @pending_queue.each { |p| send_packet(p) }
315
+ send_close_packet_if_necessary()
316
+ end
253
317
 
254
- queue_close_packet_if_necessary()
255
318
  invoke_watch(@watcher,KeeperState::CONNECTED,nil,WatchEvent::NONE) if @watcher
256
319
  end
257
320
  end
@@ -261,7 +324,6 @@ module ZooKeeper
261
324
  req.last_zxid_seen = @last_zxid_seen if @last_zxid_seen
262
325
  req.session_id = @session_id if @session_id
263
326
  req.passwd = @session_passwd if @session_passwd
264
-
265
327
  conn.send_records(req)
266
328
  end
267
329
 
@@ -296,6 +358,7 @@ module ZooKeeper
296
358
  case header.xid.to_i
297
359
  when -2
298
360
  ping_logger.debug { "Ping reply" }
361
+ send_close_packet_if_necessary()
299
362
  when -4
300
363
  logger.debug { "Auth reply" }
301
364
  session_expired(:auth_failed) unless header.err.to_i == 0
@@ -312,6 +375,13 @@ module ZooKeeper
312
375
  # A normal packet reply. They should come in the order we sent them
313
376
  # so we just match it to the packet at the front of the queue
314
377
  packet = @pending_queue.shift
378
+
379
+ if packet == nil && @close_packet
380
+ packet = @close_packet
381
+ @close_packet = nil
382
+ @session_state = :closing
383
+ end
384
+
315
385
  logger.debug { "Packet reply: #{packet.inspect}" }
316
386
 
317
387
  if (packet.xid.to_i != header.xid.to_i)
@@ -324,9 +394,8 @@ module ZooKeeper
324
394
  invoke_response(*packet.error(:disconnected))
325
395
  @conn.disconnect()
326
396
  else
327
-
328
397
  @last_zxid_seen = header.zxid
329
-
398
+
330
399
  callback, error, response, watch_type = packet.result(header.err.to_i)
331
400
  invoke_response(callback, error, response, packet_io)
332
401
 
@@ -334,11 +403,11 @@ module ZooKeeper
334
403
  @watches[watch_type][packet.path] << packet.watcher
335
404
  logger.debug { "Registered #{packet.watcher} for type=#{watch_type} at #{packet.path}" }
336
405
  end
337
- queue_close_packet_if_necessary()
406
+ send_close_packet_if_necessary()
338
407
  end
339
408
  end
340
- end
341
409
 
410
+ end
342
411
 
343
412
  def process_watch_notification(state,path,event)
344
413
 
@@ -347,17 +416,16 @@ module ZooKeeper
347
416
 
348
417
  keeper_state = KeeperState.fetch(state)
349
418
 
350
- watches = watch_types.inject(Set.new()) do | result, watch_type |
419
+ watches = watch_types.inject(Set.new()) do |result, watch_type|
351
420
  more_watches = @watches[watch_type].delete(path)
352
- result.merge(more_watches) if more_watches
353
- result
421
+ result.merge(more_watches) if more_watches
422
+ result
354
423
  end
355
424
 
356
425
  if watches.empty?
357
426
  logger.warn { "Received notification for unregistered watch #{state} #{path} #{event}" }
358
427
  end
359
428
  watches.each { | watch | invoke_watch(watch,keeper_state,path,watch_event) }
360
-
361
429
  end
362
430
 
363
431
  def invoke_watch(watch,state,path,event)
@@ -369,39 +437,48 @@ module ZooKeeper
369
437
  else
370
438
  raise ProtocolError("Bad watcher #{watch}")
371
439
  end
372
-
373
- binding.invoke(callback,state,unchroot(path),event)
440
+ @event_loop.invoke(callback,state,unchroot(path),event)
374
441
  end
375
442
 
376
443
  def clear_pending_queue(reason)
377
- @pending_queue.each { |p| invoke_response(*p.error(reason)) }
378
- @pending_queue.clear
444
+ synchronize do
445
+ @session_state = reason
446
+ @pending_queue.each { |p| invoke_response(*p.error(reason)) }
447
+ @pending_queue.clear()
448
+ if @close_packet
449
+ invoke_response(*@close_packet.error(reason))
450
+ @close_packet = nil
451
+ end
452
+ end
379
453
  end
380
454
 
381
- def queue_close_packet_if_necessary
382
- if @pending_queue.empty? && @keeper_state == :connected && @close_packet
455
+ def send_close_packet_if_necessary
456
+ # We don't need to synchronize this because the creation of
457
+ # the close packet was synchronized and after that all
458
+ # client requests are rejected
459
+ # we can receive watch and ping notifications after this
460
+ # but the server drops the connection as soon as this
461
+ # packet arrives
462
+ if @pending_queue.empty? && @session_state == :connected && @close_packet
383
463
  logger.debug { "Sending close packet!" }
384
- @client_state = :closed
385
- queue_packet(@close_packet)
386
- @close_packet = nil
464
+ send_packet(@close_packet)
387
465
  end
388
466
  end
389
467
 
390
468
  def invoke_response(callback,error,response,packet_io = nil)
391
469
  if callback
392
-
393
470
  result = if error
394
471
  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
472
+ elsif response.respond_to?(:read) && packet_io
473
+ response.read(packet_io)
474
+ elsif response
475
+ response
476
+ else
477
+ nil
478
+ end
402
479
 
403
480
  logger.debug { "Invoking response cb=#{callback} err=#{error} resp=#{result}" }
404
- binding.invoke(callback,error,result)
481
+ @event_loop.invoke(callback,error,result)
405
482
  end
406
483
  end
407
484
 
@@ -420,12 +497,10 @@ module ZooKeeper
420
497
  [watch_type,watcher]
421
498
  end
422
499
 
423
-
424
500
  def queue_packet(packet)
501
+ logger.debug { "Queuing: #{packet.inspect}" }
425
502
  @pending_queue.push(packet)
426
- logger.debug { "Queued: #{packet.inspect}" }
427
-
428
- if @keeper_state == :connected
503
+ if @session_state == :connected
429
504
  send_packet(packet)
430
505
  end
431
506
  end
@@ -440,6 +515,67 @@ module ZooKeeper
440
515
  conn.send_records(*records)
441
516
  end
442
517
 
518
+ class EventLoop
519
+ include Slf4r::Logger
520
+
521
+ def initialize(client)
522
+ @event_queue = Strand::Queue.new()
523
+
524
+ @alive = true
525
+ @event_thread = Strand.new() do
526
+ logger.debug { "Starting event loop" }
527
+ Strand.current[:name] = "ZooKeeper::EventLoop"
528
+ Strand.current[CURRENT] = [ client ]
529
+ begin
530
+ pop_event_queue until dead?
531
+ logger.debug { "Finished event loop" }
532
+ rescue Exception => ex
533
+ logger.error("Uncaught exception in event loop",ex)
534
+ end
535
+ end
536
+ end
443
537
 
538
+ def dead?
539
+ !@alive
540
+ end
541
+
542
+ # @api async_op
543
+ def pop_event_queue
544
+ #We're alive until we get a nil result from #stop
545
+ queued = @event_queue.pop if @alive
546
+ if queued
547
+ begin
548
+ callback,*args = queued
549
+ callback.call(*args)
550
+ rescue StandardError => ex
551
+ logger.error("Uncaught error in async callback", ex)
552
+ end
553
+ else
554
+ @alive = false
555
+ end
556
+ end
557
+
558
+ # @api session
559
+ def invoke(*args)
560
+ @event_queue.push(args)
561
+ end
562
+
563
+ def invoke_close(callback,*args)
564
+ Strand.new do
565
+ @event_thread.join()
566
+ callback.call(*args)
567
+ end
568
+ end
569
+
570
+ # @api session
571
+ def stop
572
+ @event_queue.push(nil)
573
+ end
574
+
575
+ # @api async_op
576
+ def current?
577
+ Strand.current == @event_thread
578
+ end
579
+ end
444
580
  end # Session
445
581
  end