amqp 1.1.0.pre1 → 1.1.0.pre2

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