amqp 1.1.0.pre1 → 1.1.0.pre2

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,12 @@
1
1
  # encoding: utf-8
2
2
 
3
- require "amq/client/adapters/event_machine"
3
+ require "eventmachine"
4
+ require "amqp/framing/string/frame"
5
+ require "amqp/auth_mechanism_adapter"
4
6
  require "amqp/broker"
5
7
 
8
+ require "amqp/channel"
9
+
6
10
  module AMQP
7
11
  # AMQP session represents connection to the broker. Session objects let you define callbacks for
8
12
  # various TCP connection lifecycle events, for instance:
@@ -28,23 +32,180 @@ module AMQP
28
32
  #
29
33
  #
30
34
  # @api public
31
- class Session < AMQ::Client::EventMachineClient
35
+ class Session < EM::Connection
36
+
37
+
38
+ #
39
+ # Behaviours
40
+ #
41
+
42
+ include Openable
43
+ include Callbacks
44
+
45
+ extend ProtocolMethodHandlers
46
+ extend RegisterEntityMixin
47
+
48
+
49
+ register_entity :channel, AMQP::Channel
32
50
 
33
51
  #
34
52
  # API
35
53
  #
36
54
 
55
+ attr_accessor :logger
56
+ attr_accessor :settings
57
+
58
+ # @return [Array<#call>]
59
+ attr_reader :callbacks
60
+
61
+
62
+ # The locale defines the language in which the server will send reply texts.
63
+ #
64
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.2)
65
+ attr_accessor :locale
66
+
67
+ # Client capabilities
68
+ #
69
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.2.1)
70
+ attr_accessor :client_properties
71
+
72
+ # Server properties
73
+ #
74
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.1.3)
75
+ attr_reader :server_properties
76
+
77
+ # Server capabilities
78
+ #
79
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.1.3)
80
+ attr_reader :server_capabilities
81
+
82
+ # Locales server supports
83
+ #
84
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.1.3)
85
+ attr_reader :server_locales
86
+
87
+ # Authentication mechanism used.
88
+ #
89
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.2)
90
+ attr_reader :mechanism
91
+
92
+ # Authentication mechanisms broker supports.
93
+ #
94
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.2)
95
+ attr_reader :server_authentication_mechanisms
96
+
97
+ # Channels within this connection.
98
+ #
99
+ # @see http://bit.ly/amqp091spec AMQP 0.9.1 specification (Section 2.2.5)
100
+ attr_reader :channels
101
+
102
+ # Maximum channel number that the server permits this connection to use.
103
+ # Usable channel numbers are in the range 1..channel_max.
104
+ # Zero indicates no specified limit.
105
+ #
106
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Sections 1.4.2.5.1 and 1.4.2.6.1)
107
+ attr_accessor :channel_max
108
+
109
+ # Maximum frame size that the server permits this connection to use.
110
+ #
111
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Sections 1.4.2.5.2 and 1.4.2.6.2)
112
+ attr_accessor :frame_max
113
+
114
+
115
+ attr_reader :known_hosts
116
+
117
+
118
+ class << self
119
+ # Settings
120
+ def settings
121
+ @settings ||= AMQP::Settings.default
122
+ end
123
+
124
+ def logger
125
+ @logger ||= begin
126
+ require "logger"
127
+ Logger.new(STDERR)
128
+ end
129
+ end
130
+
131
+ def logger=(logger)
132
+ methods = AMQP::Logging::REQUIRED_METHODS
133
+ unless methods.all? { |method| logger.respond_to?(method) }
134
+ raise AMQP::Logging::IncompatibleLoggerError.new(methods)
135
+ end
136
+
137
+ @logger = logger
138
+ end
139
+
140
+ # @return [Boolean] Current value of logging flag.
141
+ def logging
142
+ settings[:logging]
143
+ end
144
+
145
+ # Turns loggin on or off.
146
+ def logging=(boolean)
147
+ settings[:logging] = boolean
148
+ end
149
+
150
+
151
+ # Establishes connection to AMQ broker and returns it. New connection object is yielded to
152
+ # the block if it is given.
153
+ #
154
+ # @example Specifying adapter via the :adapter option
155
+ # AMQP::Adapter.connect(:adapter => "socket")
156
+ # @example Specifying using custom adapter class
157
+ # AMQP::SocketClient.connect
158
+ # @param [Hash] Connection parameters, including :adapter to use.
159
+ # @api public
160
+ def connect(settings = nil, &block)
161
+ @settings = Settings.configure(settings)
162
+
163
+ instance = self.new
164
+ instance.establish_connection(settings)
165
+ instance.register_connection_callback(&block)
166
+
167
+ instance
168
+ end
169
+ end
170
+
171
+
37
172
  # @group Connecting, reconnecting, disconnecting
38
173
 
39
174
  def initialize(*args, &block)
40
- super(*args, &block)
41
-
42
- @client_properties.merge!({
43
- :platform => ::RUBY_DESCRIPTION,
44
- :product => "AMQP gem",
45
- :information => "http://github.com/ruby-amqp/amqp",
46
- :version => AMQP::VERSION
47
- })
175
+ super(*args)
176
+
177
+ self.logger = self.class.logger
178
+
179
+ # channel => collected frames. MK.
180
+ @frames = Hash.new { Array.new }
181
+ @channels = Hash.new
182
+ @callbacks = Hash.new
183
+
184
+ opening!
185
+
186
+ # track TCP connection state, used to detect initial TCP connection failures.
187
+ @tcp_connection_established = false
188
+ @tcp_connection_failed = false
189
+ @intentionally_closing_connection = false
190
+
191
+ # EventMachine::Connection's and Adapter's constructors arity
192
+ # make it easier to use *args. MK.
193
+ @settings = Settings.configure(args.first)
194
+ @on_tcp_connection_failure = @settings[:on_tcp_connection_failure] || Proc.new { |settings|
195
+ raise self.class.tcp_connection_failure_exception_class.new(settings)
196
+ }
197
+ @on_possible_authentication_failure = @settings[:on_possible_authentication_failure] || Proc.new { |settings|
198
+ raise self.class.authentication_failure_exception_class.new(settings)
199
+ }
200
+
201
+ @mechanism = @settings.fetch(:auth_mechanism, "PLAIN")
202
+ @locale = @settings.fetch(:locale, "en_GB")
203
+ @client_properties = Settings.client_properties.merge(@settings.fetch(:client_properties, Hash.new))
204
+
205
+ @auto_recovery = (!!@settings[:auto_recovery])
206
+
207
+ self.reset
208
+ self.set_pending_connect_timeout((@settings[:timeout] || 3).to_f) unless defined?(JRUBY_VERSION)
48
209
  end # initialize(*args, &block)
49
210
 
50
211
  # @return [Boolean] true if this AMQP connection is currently open
@@ -80,43 +241,27 @@ module AMQP
80
241
  alias user username
81
242
 
82
243
 
83
- # Reconnect to the broker using current connection settings.
84
- #
85
- # @param [Boolean] force Enforce immediate connection
86
- # @param [Fixnum] period If given, reconnection will be delayed by this period, in seconds.
87
- # @api public
88
- def reconnect(force = false, period = 2)
89
- # we do this to make sure this method shows up in our documentation
90
- # this method is too important to leave out and YARD currently does not
91
- # support cross-referencing to dependencies. MK.
92
- super(force, period)
93
- end # reconnect(force = false)
94
-
95
- # A version of #reconnect that allows connecting to different endpoints (hosts).
96
- # @see #reconnect
97
- # @api public
98
- def reconnect_to(connection_string_or_options = {}, period = 2)
99
- opts = case connection_string_or_options
100
- when String then
101
- AMQP::Client.parse_connection_uri(connection_string_or_options)
102
- when Hash then
103
- connection_string_or_options
104
- else
105
- Hash.new
106
- end
107
-
108
- super(opts, period)
109
- end # reconnect_to(connection_string_or_options = {})
110
-
111
-
112
244
  # Properly close connection with AMQ broker, as described in
113
245
  # section 2.2.4 of the {http://files.travis-ci.org/docs/amqp/0.9.1/AMQP091Specification.pdf AMQP 0.9.1 specification}.
114
246
  #
115
247
  # @api plugin
116
248
  # @see #close_connection
117
249
  def disconnect(reply_code = 200, reply_text = "Goodbye", &block)
118
- # defined here to make this method appear in YARD documentation. MK.
119
- super(reply_code, reply_text, &block)
250
+ @intentionally_closing_connection = true
251
+ self.on_disconnection do
252
+ @frames.clear
253
+ block.call if block
254
+ end
255
+
256
+ # ruby-amqp/amqp#66, MK.
257
+ if self.open?
258
+ closing!
259
+ self.send_frame(AMQ::Protocol::Connection::Close.encode(reply_code, reply_text, 0, 0))
260
+ elsif self.closing?
261
+ # no-op
262
+ else
263
+ self.disconnection_successful
264
+ end
120
265
  end
121
266
  alias close disconnect
122
267
 
@@ -160,9 +305,9 @@ module AMQP
160
305
  # @see #on_closed
161
306
  # @api public
162
307
  def on_open(&block)
163
- # defined here to make this method appear in YARD documentation. MK.
164
- super(&block)
165
- end # on_open(&block)
308
+ @connection_deferrable.callback(&block)
309
+ end
310
+ alias on_connection on_open
166
311
 
167
312
 
168
313
  # @group Error Handling and Recovery
@@ -173,17 +318,16 @@ module AMQP
173
318
  # @see #on_closed
174
319
  # @api public
175
320
  def on_closed(&block)
176
- # defined here to make this method appear in YARD documentation. MK.
177
- super(&block)
178
- end # on_closed(&block)
321
+ @disconnection_deferrable.callback(&block)
322
+ end
323
+ alias on_disconnection on_closed
179
324
 
180
325
  # Defines a callback that will be run when initial TCP connection fails.
181
326
  # You can define only one callback.
182
327
  #
183
328
  # @api public
184
329
  def on_tcp_connection_failure(&block)
185
- # defined here to make this method appear in YARD documentation. MK.
186
- super(&block)
330
+ @on_tcp_connection_failure = block
187
331
  end
188
332
 
189
333
  # Defines a callback that will be run when TCP connection to AMQP broker is lost (interrupted).
@@ -191,8 +335,7 @@ module AMQP
191
335
  #
192
336
  # @api public
193
337
  def on_tcp_connection_loss(&block)
194
- # defined here to make this method appear in YARD documentation. MK.
195
- super(&block)
338
+ @on_tcp_connection_loss = block
196
339
  end
197
340
 
198
341
  # Defines a callback that will be run when TCP connection is closed before authentication
@@ -200,8 +343,7 @@ module AMQP
200
343
  #
201
344
  # @api public
202
345
  def on_possible_authentication_failure(&block)
203
- # defined here to make this method appear in YARD documentation. MK.
204
- super(&block)
346
+ @on_possible_authentication_failure = block
205
347
  end
206
348
 
207
349
  # Defines a callback that will be executed after TCP connection is interrupted (typically because of a network failure).
@@ -209,25 +351,18 @@ module AMQP
209
351
  #
210
352
  # @api public
211
353
  def on_connection_interruption(&block)
212
- super(&block)
213
- end # on_connection_interruption(&block)
354
+ self.redefine_callback(:after_connection_interruption, &block)
355
+ end
214
356
  alias after_connection_interruption on_connection_interruption
215
357
 
216
358
 
217
- # @private
218
- # @api plugin
219
- def handle_connection_interruption
220
- super
221
- end # handle_connection_interruption
222
-
223
-
224
359
  # Defines a callback that will be executed when connection is closed after
225
360
  # connection-level exception. Only one callback can be defined (the one defined last
226
361
  # replaces previously added ones).
227
362
  #
228
363
  # @api public
229
364
  def on_error(&block)
230
- super(&block)
365
+ self.redefine_callback(:error, &block)
231
366
  end
232
367
 
233
368
 
@@ -237,8 +372,8 @@ module AMQP
237
372
  #
238
373
  # @api public
239
374
  def before_recovery(&block)
240
- super(&block)
241
- end # before_recovery(&block)
375
+ self.redefine_callback(:before_recovery, &block)
376
+ end
242
377
 
243
378
 
244
379
  # Defines a callback that will be executed after AMQP connection has recovered after a network failure..
@@ -246,16 +381,16 @@ module AMQP
246
381
  #
247
382
  # @api public
248
383
  def on_recovery(&block)
249
- super(&block)
250
- end # on_recovery(&block)
384
+ self.redefine_callback(:after_recovery, &block)
385
+ end
251
386
  alias after_recovery on_recovery
252
387
 
253
388
 
254
389
  # @return [Boolean] whether connection is in the automatic recovery mode
255
390
  # @api public
256
391
  def auto_recovering?
257
- super
258
- end # auto_recovering?
392
+ !!@auto_recovery
393
+ end
259
394
  alias auto_recovery? auto_recovering?
260
395
 
261
396
 
@@ -266,7 +401,7 @@ module AMQP
266
401
  # @see Exchange#auto_recover
267
402
  # @api plugin
268
403
  def auto_recover
269
- super
404
+ @channels.select { |channel_id, ch| ch.auto_recovering? }.each { |n, ch| ch.auto_recover }
270
405
  end # auto_recover
271
406
 
272
407
  # @endgroup
@@ -297,5 +432,743 @@ module AMQP
297
432
  def self.authentication_failure_exception_class
298
433
  @authentication_failure_exception_class ||= AMQP::PossibleAuthenticationFailureError
299
434
  end # self.authentication_failure_exception_class
435
+
436
+ # @group Connection operations
437
+
438
+ # Initiates connection to AMQP broker. If callback is given, runs it when (and if) AMQP connection
439
+ # succeeds.
440
+ #
441
+ # @option settings [String] :host ("127.0.0.1") Hostname AMQ broker runs on.
442
+ # @option settings [String] :port (5672) Port AMQ broker listens on.
443
+ # @option settings [String] :vhost ("/") Virtual host to use.
444
+ # @option settings [String] :user ("guest") Username to use for authentication.
445
+ # @option settings [String] :pass ("guest") Password to use for authentication.
446
+ # @option settings [String] :auth_mechanism ("PLAIN") SASL authentication mechanism to use.
447
+ # @option settings [String] :ssl (false) Should be use TLS (SSL) for connection?
448
+ # @option settings [String] :timeout (nil) Connection timeout.
449
+ # @option settings [Fixnum] :heartbeat (0) Connection heartbeat, in seconds.
450
+ # @option settings [Fixnum] :frame_max (131072) Maximum frame size to use. If broker cannot support frames this large, broker's maximum value will be used instead.
451
+ #
452
+ # @param [Hash] settings
453
+ def self.connect(settings = {}, &block)
454
+ @settings = Settings.configure(settings)
455
+
456
+ instance = EventMachine.connect(@settings[:host], @settings[:port], self, @settings)
457
+ instance.register_connection_callback(&block)
458
+
459
+ instance
460
+ end
461
+
462
+ # Reconnect after a period of wait.
463
+ #
464
+ # @param [Fixnum] period Period of time, in seconds, to wait before reconnection attempt.
465
+ # @param [Boolean] force If true, enforces immediate reconnection.
466
+ # @api public
467
+ def reconnect(force = false, period = 5)
468
+ if @reconnecting and not force
469
+ EventMachine::Timer.new(period) {
470
+ reconnect(true, period)
471
+ }
472
+ return
473
+ end
474
+
475
+ if !@reconnecting
476
+ @reconnecting = true
477
+ self.reset
478
+ end
479
+
480
+ EventMachine.reconnect(@settings[:host], @settings[:port], self)
481
+ end
482
+
483
+ # Similar to #reconnect, but uses different connection settings
484
+ # @see #reconnect
485
+ # @api public
486
+ def reconnect_to(connection_string_or_options, period = 5)
487
+ settings = case connection_string_or_options
488
+ when String then
489
+ AMQP.parse_connection_uri(connection_string_or_options)
490
+ when Hash then
491
+ connection_string_or_options
492
+ else
493
+ Hash.new
494
+ end
495
+
496
+ if !@reconnecting
497
+ @reconnecting = true
498
+ self.reset
499
+ end
500
+
501
+ @settings = Settings.configure(settings)
502
+ EventMachine.reconnect(@settings[:host], @settings[:port], self)
503
+ end
504
+
505
+
506
+ # Periodically try to reconnect.
507
+ #
508
+ # @param [Fixnum] period Period of time, in seconds, to wait before reconnection attempt.
509
+ # @param [Boolean] force If true, enforces immediate reconnection.
510
+ # @api public
511
+ def periodically_reconnect(period = 5)
512
+ @reconnecting = true
513
+ self.reset
514
+
515
+ @periodic_reconnection_timer = EventMachine::PeriodicTimer.new(period) {
516
+ EventMachine.reconnect(@settings[:host], @settings[:port], self)
517
+ }
518
+ end
519
+
520
+ # @endgroup
521
+
522
+ # @see #on_open
523
+ # @private
524
+ def register_connection_callback(&block)
525
+ unless block.nil?
526
+ # delay calling block we were given till after we receive
527
+ # connection.open-ok. Connection will notify us when
528
+ # that happens.
529
+ self.on_open do
530
+ block.call(self)
531
+ end
532
+ end
533
+ end
534
+
535
+
536
+
537
+ # For EventMachine adapter, this is a no-op.
538
+ # @api public
539
+ def establish_connection(settings)
540
+ # Unfortunately there doesn't seem to be any sane way
541
+ # how to get EventMachine connect to the instance level.
542
+ end
543
+
544
+ alias close disconnect
545
+
546
+
547
+
548
+ # Whether we are in authentication state (after TCP connection was estabilished
549
+ # but before broker authenticated us).
550
+ #
551
+ # @return [Boolean]
552
+ # @api public
553
+ def authenticating?
554
+ @authenticating
555
+ end # authenticating?
556
+
557
+ # IS TCP connection estabilished and currently active?
558
+ # @return [Boolean]
559
+ # @api public
560
+ def tcp_connection_established?
561
+ @tcp_connection_established
562
+ end # tcp_connection_established?
563
+
564
+
565
+
566
+
567
+ #
568
+ # Implementation
569
+ #
570
+
571
+ # Backwards compatibility with 0.7.0.a25. MK.
572
+ Deferrable = EventMachine::DefaultDeferrable
573
+
574
+
575
+ alias send_raw send_data
576
+
577
+
578
+ # EventMachine reactor callback. Is run when TCP connection is estabilished
579
+ # but before resumption of the network loop. Note that this includes cases
580
+ # when TCP connection has failed.
581
+ # @private
582
+ def post_init
583
+ reset
584
+
585
+ # note that upgrading to TLS in #connection_completed causes
586
+ # Erlang SSL app that RabbitMQ relies on to report
587
+ # error on TCP connection <0.1465.0>:{ssl_upgrade_error,"record overflow"}
588
+ # and close TCP connection down. Investigation of this issue is likely
589
+ # to take some time and to not be worth in as long as #post_init
590
+ # works fine. MK.
591
+ upgrade_to_tls_if_necessary
592
+ rescue Exception => error
593
+ raise error
594
+ end # post_init
595
+
596
+
597
+
598
+ # Called by EventMachine reactor once TCP connection is successfully estabilished.
599
+ # @private
600
+ def connection_completed
601
+ # we only can safely set this value here because EventMachine is a lovely piece of
602
+ # software that calls #post_init before #unbind even when TCP connection
603
+ # fails. MK.
604
+ @tcp_connection_established = true
605
+ @periodic_reconnection_timer.cancel if @periodic_reconnection_timer
606
+
607
+
608
+ # again, this is because #unbind is called in different situations
609
+ # and there is no easy way to tell initial connection failure
610
+ # from connection loss. Not in EventMachine 0.12.x, anyway. MK.
611
+
612
+ if @had_successfully_connected_before
613
+ @recovered = true
614
+
615
+
616
+ self.start_automatic_recovery
617
+ self.upgrade_to_tls_if_necessary
618
+ end
619
+
620
+ # now we can set it. MK.
621
+ @had_successfully_connected_before = true
622
+ @reconnecting = false
623
+ @handling_skipped_hearbeats = false
624
+ @last_server_heartbeat = Time.now
625
+
626
+ self.handshake
627
+ end
628
+
629
+ # @private
630
+ def close_connection(*args)
631
+ @intentionally_closing_connection = true
632
+
633
+ super(*args)
634
+ end
635
+
636
+ # Called by EventMachine reactor when
637
+ #
638
+ # * We close TCP connection down
639
+ # * Our peer closes TCP connection down
640
+ # * There is a network connection issue
641
+ # * Initial TCP connection fails
642
+ # @private
643
+ def unbind(exception = nil)
644
+ if !@tcp_connection_established && !@had_successfully_connected_before && !@intentionally_closing_connection
645
+ @tcp_connection_failed = true
646
+ logger.error "[amqp] Detected TCP connection failure"
647
+ self.tcp_connection_failed
648
+ end
649
+
650
+ closing!
651
+ @tcp_connection_established = false
652
+
653
+ self.handle_connection_interruption if @reconnecting
654
+ @disconnection_deferrable.succeed
655
+
656
+ closed!
657
+
658
+
659
+ self.tcp_connection_lost if !@intentionally_closing_connection && @had_successfully_connected_before
660
+
661
+ # since AMQP spec dictates that authentication failure is a protocol exception
662
+ # and protocol exceptions result in connection closure, check whether we are
663
+ # in the authentication stage. If so, it is likely to signal an authentication
664
+ # issue. Java client behaves the same way. MK.
665
+ if authenticating? && !@intentionally_closing_connection
666
+ @on_possible_authentication_failure.call(@settings) if @on_possible_authentication_failure
667
+ end
668
+ end # unbind
669
+
670
+
671
+ #
672
+ # EventMachine receives data in chunks, sometimes those chunks are smaller
673
+ # than the size of AMQP frame. That's why you need to add some kind of buffer.
674
+ #
675
+ # @private
676
+ def receive_data(chunk)
677
+ @chunk_buffer << chunk
678
+ while frame = get_next_frame
679
+ self.receive_frame(AMQP::Framing::String::Frame.decode(frame))
680
+ end
681
+ end
682
+
683
+
684
+ # Called by AMQP::Connection after we receive connection.open-ok.
685
+ # @api public
686
+ def connection_successful
687
+ @authenticating = false
688
+ opened!
689
+
690
+ @connection_deferrable.succeed
691
+ end # connection_successful
692
+
693
+
694
+ # Called by AMQP::Connection after we receive connection.close-ok.
695
+ #
696
+ # @api public
697
+ def disconnection_successful
698
+ @disconnection_deferrable.succeed
699
+
700
+ # true for "after writing buffered data"
701
+ self.close_connection(true)
702
+ self.reset
703
+ closed!
704
+ end # disconnection_successful
705
+
706
+ # Called when time since last server heartbeat received is greater or equal to the
707
+ # heartbeat interval set via :heartbeat_interval option on connection.
708
+ #
709
+ # @api plugin
710
+ def handle_skipped_hearbeats
711
+ if !@handling_skipped_hearbeats && @tcp_connection_established && !@intentionally_closing_connection
712
+ @handling_skipped_hearbeats = true
713
+ self.cancel_heartbeat_sender
714
+
715
+ self.run_skipped_heartbeats_callbacks
716
+ end
717
+ end
718
+
719
+ # @private
720
+ def initialize_heartbeat_sender
721
+ @last_server_heartbeat = Time.now
722
+ @heartbeats_timer = EventMachine::PeriodicTimer.new(self.heartbeat_interval, &method(:send_heartbeat))
723
+ end
724
+
725
+ # @private
726
+ def cancel_heartbeat_sender
727
+ @heartbeats_timer.cancel if @heartbeats_timer
728
+ end
729
+
730
+
731
+
732
+ # Sends AMQ protocol header (also known as preamble).
733
+ #
734
+ # @note This must be implemented by all AMQP clients.
735
+ # @api plugin
736
+ # @see http://bit.ly/amqp091spec AMQP 0.9.1 specification (Section 2.2)
737
+ def send_preamble
738
+ self.send_raw(AMQ::Protocol::PREAMBLE)
739
+ end
740
+
741
+ # Sends frame to the peer, checking that connection is open.
742
+ #
743
+ # @raise [ConnectionClosedError]
744
+ def send_frame(frame)
745
+ if closed?
746
+ raise ConnectionClosedError.new(frame)
747
+ else
748
+ self.send_raw(frame.encode)
749
+ end
750
+ end
751
+
752
+ # Sends multiple frames, one by one. For thread safety this method takes a channel
753
+ # object and synchronizes on it.
754
+ #
755
+ # @api public
756
+ def send_frameset(frames, channel)
757
+ # some (many) developers end up sharing channels between threads and when multiple
758
+ # threads publish on the same channel aggressively, at some point frames will be
759
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
760
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
761
+ # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
762
+ channel.synchronize do
763
+ frames.each { |frame| self.send_frame(frame) }
764
+ end
765
+ end # send_frameset(frames)
766
+
767
+
768
+
769
+ # Returns heartbeat interval this client uses, in seconds.
770
+ # This value may or may not be used depending on broker capabilities.
771
+ # Zero means the server does not want a heartbeat.
772
+ #
773
+ # @return [Fixnum] Heartbeat interval this client uses, in seconds.
774
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.6)
775
+ def heartbeat_interval
776
+ @heartbeat_interval
777
+ end # heartbeat_interval
778
+
779
+ # Returns true if heartbeats are enabled (heartbeat interval is greater than 0)
780
+ # @return [Boolean]
781
+ def heartbeats_enabled?
782
+ @heartbeat_interval && (@heartbeat_interval > 0)
783
+ end
784
+
785
+
786
+ # vhost this connection uses. Default is "/", a historically estabilished convention
787
+ # of RabbitMQ and amqp gem.
788
+ #
789
+ # @return [String] vhost this connection uses
790
+ # @api public
791
+ def vhost
792
+ @settings.fetch(:vhost, "/")
793
+ end # vhost
794
+
795
+
796
+
797
+ # @group Error Handling and Recovery
798
+
799
+ # Called when initial TCP connection fails.
800
+ # @api public
801
+ def tcp_connection_failed
802
+ @recovered = false
803
+
804
+ @on_tcp_connection_failure.call(@settings) if @on_tcp_connection_failure
805
+ end
806
+
807
+ # Called when previously established TCP connection fails.
808
+ # @api public
809
+ def tcp_connection_lost
810
+ @recovered = false
811
+
812
+ @on_tcp_connection_loss.call(self, @settings) if @on_tcp_connection_loss
813
+ self.handle_connection_interruption
814
+ end
815
+
816
+ # @return [Boolean]
817
+ def reconnecting?
818
+ @reconnecting
819
+ end # reconnecting?
820
+
821
+ # @private
822
+ # @api plugin
823
+ def handle_connection_interruption
824
+ self.cancel_heartbeat_sender
825
+
826
+ @channels.each { |n, c| c.handle_connection_interruption }
827
+ self.exec_callback_yielding_self(:after_connection_interruption)
828
+ end
829
+
830
+
831
+
832
+ # @private
833
+ def run_before_recovery_callbacks
834
+ self.exec_callback_yielding_self(:before_recovery, @settings)
835
+
836
+ @channels.each { |n, ch| ch.run_before_recovery_callbacks }
837
+ end
838
+
839
+
840
+ # @private
841
+ def run_after_recovery_callbacks
842
+ self.exec_callback_yielding_self(:after_recovery, @settings)
843
+
844
+ @channels.each { |n, ch| ch.run_after_recovery_callbacks }
845
+ end
846
+
847
+
848
+ # Performs recovery of channels that are in the automatic recovery mode. "before recovery" callbacks
849
+ # are run immediately, "after recovery" callbacks are run after AMQP connection is re-established and
850
+ # auto recovery is performed (using #auto_recover).
851
+ #
852
+ # Use this method if you want to run automatic recovery process after handling a connection-level exception,
853
+ # for example, 320 CONNECTION_FORCED (used by RabbitMQ when it is shut down gracefully).
854
+ #
855
+ # @see Channel#auto_recover
856
+ # @see Queue#auto_recover
857
+ # @see Exchange#auto_recover
858
+ # @api plugin
859
+ def start_automatic_recovery
860
+ self.run_before_recovery_callbacks
861
+ self.register_connection_callback do
862
+ # always run automatic recovery, because it is per-channel
863
+ # and connection has to start it. Channels that did not opt-in for
864
+ # autorecovery won't be selected. MK.
865
+ self.auto_recover
866
+ self.run_after_recovery_callbacks
867
+ end
868
+ end # start_automatic_recovery
869
+
870
+
871
+ # Defines a callback that will be executed after time since last broker heartbeat is greater
872
+ # than or equal to the heartbeat interval (skipped heartbeat is detected).
873
+ # Only one callback can be defined (the one defined last replaces previously added ones).
874
+ #
875
+ # @api public
876
+ def on_skipped_heartbeats(&block)
877
+ self.redefine_callback(:skipped_heartbeats, &block)
878
+ end # on_skipped_heartbeats(&block)
879
+
880
+ # @private
881
+ def run_skipped_heartbeats_callbacks
882
+ self.exec_callback_yielding_self(:skipped_heartbeats, @settings)
883
+ end
884
+
885
+ # @endgroup
886
+
887
+
888
+
889
+
890
+ #
891
+ # Implementation
892
+ #
893
+
894
+ # Sends connection preamble to the broker.
895
+ # @api plugin
896
+ def handshake
897
+ @authenticating = true
898
+ self.send_preamble
899
+ end
900
+
901
+
902
+ # Sends connection.open to the server.
903
+ #
904
+ # @api plugin
905
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.7)
906
+ def open(vhost = "/")
907
+ self.send_frame(AMQ::Protocol::Connection::Open.encode(vhost))
908
+ end
909
+
910
+ # Resets connection state.
911
+ #
912
+ # @api plugin
913
+ def reset_state!
914
+ # no-op by default
915
+ end # reset_state!
916
+
917
+ # @api plugin
918
+ # @see http://tools.ietf.org/rfc/rfc2595.txt RFC 2595
919
+ def encode_credentials(username, password)
920
+ auth_mechanism_adapter.encode_credentials(username, password)
921
+ end # encode_credentials(username, password)
922
+
923
+ # Retrieves an AuthMechanismAdapter that will encode credentials for
924
+ # this Adapter.
925
+ #
926
+ # @api plugin
927
+ def auth_mechanism_adapter
928
+ @auth_mechanism_adapter ||= AuthMechanismAdapter.for_adapter(self)
929
+ end
930
+
931
+
932
+ # Processes a single frame.
933
+ #
934
+ # @param [AMQ::Protocol::Frame] frame
935
+ # @api plugin
936
+ def receive_frame(frame)
937
+ @frames[frame.channel] ||= Array.new
938
+ @frames[frame.channel] << frame
939
+
940
+ if frameset_complete?(@frames[frame.channel])
941
+ receive_frameset(@frames[frame.channel])
942
+ # for channel.close, frame.channel will be nil. MK.
943
+ clear_frames_on(frame.channel) if @frames[frame.channel]
944
+ end
945
+ end
946
+
947
+ # Processes a frameset by finding and invoking a suitable handler.
948
+ # Heartbeat frames are treated in a special way: they simply update @last_server_heartbeat
949
+ # value.
950
+ #
951
+ # @param [Array<AMQ::Protocol::Frame>] frames
952
+ # @api plugin
953
+ def receive_frameset(frames)
954
+ if self.heartbeats_enabled?
955
+ # treat incoming traffic as heartbeats.
956
+ # this operation is pretty expensive under heavy traffic but heartbeats can be disabled
957
+ # (and are also disabled by default). MK.
958
+ @last_server_heartbeat = Time.now
959
+ end
960
+ frame = frames.first
961
+
962
+ if AMQ::Protocol::HeartbeatFrame === frame
963
+ # no-op
964
+ else
965
+ if callable = AMQP::HandlersRegistry.find(frame.method_class)
966
+ f = frames.shift
967
+ callable.call(self, f, frames)
968
+ else
969
+ raise MissingHandlerError.new(frames.first)
970
+ end
971
+ end
972
+ end
973
+
974
+ # Clears frames that were received but not processed on given channel. Needs to be called
975
+ # when the channel is closed.
976
+ # @private
977
+ def clear_frames_on(channel_id)
978
+ raise ArgumentError, "channel id cannot be nil!" if channel_id.nil?
979
+
980
+ @frames[channel_id].clear
981
+ end
982
+
983
+ # Sends a heartbeat frame if connection is open.
984
+ # @api plugin
985
+ def send_heartbeat
986
+ if tcp_connection_established? && !@handling_skipped_hearbeats && @last_server_heartbeat
987
+ if @last_server_heartbeat < (Time.now - (self.heartbeat_interval * 2)) && !reconnecting?
988
+ logger.error "[amqp] Detected missing server heartbeats"
989
+ self.handle_skipped_hearbeats
990
+ end
991
+ send_frame(AMQ::Protocol::HeartbeatFrame)
992
+ end
993
+ end # send_heartbeat
994
+
995
+
996
+
997
+
998
+
999
+
1000
+ # Handles connection.start.
1001
+ #
1002
+ # @api plugin
1003
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.1.)
1004
+ def handle_start(connection_start)
1005
+ @server_properties = connection_start.server_properties
1006
+ @server_capabilities = @server_properties["capabilities"]
1007
+
1008
+ @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ")
1009
+ @server_locales = Array(connection_start.locales)
1010
+
1011
+ username = @settings[:user] || @settings[:username]
1012
+ password = @settings[:pass] || @settings[:password]
1013
+
1014
+ # It's not clear whether we should transition to :opening state here
1015
+ # or in #open but in case authentication fails, it would be strange to have
1016
+ # @status undefined. So lets do this. MK.
1017
+ opening!
1018
+
1019
+ self.send_frame(AMQ::Protocol::Connection::StartOk.encode(@client_properties, mechanism, self.encode_credentials(username, password), @locale))
1020
+ end
1021
+
1022
+
1023
+ # Handles Connection.Tune-Ok.
1024
+ #
1025
+ # @api plugin
1026
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.6)
1027
+ def handle_tune(connection_tune)
1028
+ @channel_max = connection_tune.channel_max.freeze
1029
+ @frame_max = connection_tune.frame_max.freeze
1030
+
1031
+ client_heartbeat = @settings[:heartbeat] || @settings[:heartbeat_interval] || 0
1032
+
1033
+ @heartbeat_interval = negotiate_heartbeat_value(client_heartbeat, connection_tune.heartbeat)
1034
+
1035
+ self.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, [settings[:frame_max], @frame_max].min, @heartbeat_interval))
1036
+ self.initialize_heartbeat_sender if heartbeats_enabled?
1037
+ end # handle_tune(method)
1038
+
1039
+
1040
+ # Handles Connection.Open-Ok.
1041
+ #
1042
+ # @api plugin
1043
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.8.)
1044
+ def handle_open_ok(open_ok)
1045
+ @known_hosts = open_ok.known_hosts.dup.freeze
1046
+
1047
+ opened!
1048
+ self.connection_successful if self.respond_to?(:connection_successful)
1049
+ end
1050
+
1051
+
1052
+ # Handles connection.close. When broker detects a connection level exception, this method is called.
1053
+ #
1054
+ # @api plugin
1055
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.5.2.9)
1056
+ def handle_close(conn_close)
1057
+ closed!
1058
+ self.exec_callback_yielding_self(:error, conn_close)
1059
+ end
1060
+
1061
+
1062
+ # Handles Connection.Close-Ok.
1063
+ #
1064
+ # @api plugin
1065
+ # @see http://bit.ly/amqp091reference AMQP 0.9.1 protocol reference (Section 1.4.2.10)
1066
+ def handle_close_ok(close_ok)
1067
+ closed!
1068
+ self.disconnection_successful
1069
+ end # handle_close_ok(close_ok)
1070
+
1071
+
1072
+
1073
+ protected
1074
+
1075
+ def negotiate_heartbeat_value(client_value, server_value)
1076
+ if client_value == 0 || server_value == 0
1077
+ [client_value, server_value].max
1078
+ else
1079
+ [client_value, server_value].min
1080
+ end
1081
+ end
1082
+
1083
+ # Returns next frame from buffer whenever possible
1084
+ #
1085
+ # @api private
1086
+ def get_next_frame
1087
+ return nil unless @chunk_buffer.size > 7 # otherwise, cannot read the length
1088
+ # octet + short
1089
+ offset = 3 # 1 + 2
1090
+ # length
1091
+ payload_length = @chunk_buffer[offset, 4].unpack(AMQ::Protocol::PACK_UINT32).first
1092
+ # 4 bytes for long payload length, 1 byte final octet
1093
+ frame_length = offset + payload_length + 5
1094
+ if frame_length <= @chunk_buffer.size
1095
+ @chunk_buffer.slice!(0, frame_length)
1096
+ else
1097
+ nil
1098
+ end
1099
+ end # def get_next_frame
1100
+
1101
+ # Utility methods
1102
+
1103
+ # Determines, whether the received frameset is ready to be further processed
1104
+ def frameset_complete?(frames)
1105
+ return false if frames.empty?
1106
+ first_frame = frames[0]
1107
+ first_frame.final? || (first_frame.method_class.has_content? && content_complete?(frames[1..-1]))
1108
+ end
1109
+
1110
+ # Determines, whether given frame array contains full content body
1111
+ def content_complete?(frames)
1112
+ return false if frames.empty?
1113
+ header = frames[0]
1114
+ raise "Not a content header frame first: #{header.inspect}" unless header.kind_of?(AMQ::Protocol::HeaderFrame)
1115
+ header.body_size == frames[1..-1].inject(0) {|sum, frame| sum + frame.payload.size }
1116
+ end
1117
+
1118
+
1119
+
1120
+ self.handle(AMQ::Protocol::Connection::Start) do |connection, frame|
1121
+ connection.handle_start(frame.decode_payload)
1122
+ end
1123
+
1124
+ self.handle(AMQ::Protocol::Connection::Tune) do |connection, frame|
1125
+ connection.handle_tune(frame.decode_payload)
1126
+
1127
+ connection.open(connection.vhost)
1128
+ end
1129
+
1130
+ self.handle(AMQ::Protocol::Connection::OpenOk) do |connection, frame|
1131
+ connection.handle_open_ok(frame.decode_payload)
1132
+ end
1133
+
1134
+ self.handle(AMQ::Protocol::Connection::Close) do |connection, frame|
1135
+ connection.handle_close(frame.decode_payload)
1136
+ end
1137
+
1138
+ self.handle(AMQ::Protocol::Connection::CloseOk) do |connection, frame|
1139
+ connection.handle_close_ok(frame.decode_payload)
1140
+ end
1141
+
1142
+
1143
+
1144
+
1145
+ protected
1146
+
1147
+
1148
+ def reset
1149
+ @size = 0
1150
+ @payload = ""
1151
+ @frames = Array.new
1152
+
1153
+ @chunk_buffer = ""
1154
+ @connection_deferrable = EventMachine::DefaultDeferrable.new
1155
+ @disconnection_deferrable = EventMachine::DefaultDeferrable.new
1156
+
1157
+ # used to track down whether authentication succeeded. AMQP 0.9.1 dictates
1158
+ # that on authentication failure broker must close TCP connection without sending
1159
+ # any more data. This is why we need to explicitly track whether we are past
1160
+ # authentication stage to signal possible authentication failures.
1161
+ @authenticating = false
1162
+ end
1163
+
1164
+ def upgrade_to_tls_if_necessary
1165
+ tls_options = @settings[:ssl]
1166
+
1167
+ if tls_options.is_a?(Hash)
1168
+ start_tls(tls_options)
1169
+ elsif tls_options
1170
+ start_tls
1171
+ end
1172
+ end # upgrade_to_tls_if_necessary
300
1173
  end # Session
301
1174
  end # AMQP