zkruby 3.4.3 → 3.4.4.rc3

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