gorgon 0.5.0.rc1 → 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/Gemfile.lock +2 -4
  2. data/gorgon.gemspec +0 -1
  3. data/lib/gorgon/amqp_service.rb +5 -5
  4. data/lib/gorgon/gem_command_handler.rb +2 -1
  5. data/lib/gorgon/listener.rb +7 -5
  6. data/lib/gorgon/originator_protocol.rb +1 -0
  7. data/lib/gorgon/version.rb +1 -1
  8. data/lib/gorgon/worker_manager.rb +5 -2
  9. data/lib/gorgon_amq-protocol/.gitignore +15 -0
  10. data/lib/gorgon_amq-protocol/.gitmodules +3 -0
  11. data/lib/gorgon_amq-protocol/.rspec +3 -0
  12. data/lib/gorgon_amq-protocol/.travis.yml +19 -0
  13. data/lib/gorgon_amq-protocol/lib/gorgon_amq/bit_set.rb +82 -0
  14. data/lib/gorgon_amq-protocol/lib/gorgon_amq/endianness.rb +15 -0
  15. data/lib/gorgon_amq-protocol/lib/gorgon_amq/int_allocator.rb +96 -0
  16. data/lib/gorgon_amq-protocol/lib/gorgon_amq/pack.rb +53 -0
  17. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol.rb +4 -0
  18. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/client.rb +2322 -0
  19. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/constants.rb +22 -0
  20. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/exceptions.rb +60 -0
  21. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/float_32bit.rb +14 -0
  22. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/frame.rb +210 -0
  23. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table.rb +142 -0
  24. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_decoder.rb +190 -0
  25. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_encoder.rb +123 -0
  26. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/type_constants.rb +26 -0
  27. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/version.rb +5 -0
  28. data/lib/gorgon_amq-protocol/lib/gorgon_amq/settings.rb +114 -0
  29. data/lib/gorgon_amq-protocol/lib/gorgon_amq/uri.rb +37 -0
  30. data/lib/gorgon_bunny/lib/gorgon_amq/protocol/extensions.rb +16 -0
  31. data/lib/gorgon_bunny/lib/gorgon_bunny.rb +89 -0
  32. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/credentials_encoder.rb +55 -0
  33. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/external_mechanism_encoder.rb +27 -0
  34. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/plain_mechanism_encoder.rb +19 -0
  35. data/lib/gorgon_bunny/lib/gorgon_bunny/channel.rb +1875 -0
  36. data/lib/gorgon_bunny/lib/gorgon_bunny/channel_id_allocator.rb +80 -0
  37. data/lib/gorgon_bunny/lib/gorgon_bunny/compatibility.rb +24 -0
  38. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/atomic_fixnum.rb +74 -0
  39. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/condition.rb +66 -0
  40. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/continuation_queue.rb +41 -0
  41. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/linked_continuation_queue.rb +61 -0
  42. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/synchronized_sorted_set.rb +56 -0
  43. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer.rb +123 -0
  44. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_tag_generator.rb +23 -0
  45. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_work_pool.rb +94 -0
  46. data/lib/gorgon_bunny/lib/gorgon_bunny/delivery_info.rb +93 -0
  47. data/lib/gorgon_bunny/lib/gorgon_bunny/exceptions.rb +236 -0
  48. data/lib/gorgon_bunny/lib/gorgon_bunny/exchange.rb +271 -0
  49. data/lib/gorgon_bunny/lib/gorgon_bunny/framing.rb +56 -0
  50. data/lib/gorgon_bunny/lib/gorgon_bunny/heartbeat_sender.rb +70 -0
  51. data/lib/gorgon_bunny/lib/gorgon_bunny/message_properties.rb +119 -0
  52. data/lib/gorgon_bunny/lib/gorgon_bunny/queue.rb +387 -0
  53. data/lib/gorgon_bunny/lib/gorgon_bunny/reader_loop.rb +116 -0
  54. data/lib/gorgon_bunny/lib/gorgon_bunny/return_info.rb +74 -0
  55. data/lib/gorgon_bunny/lib/gorgon_bunny/session.rb +1044 -0
  56. data/lib/gorgon_bunny/lib/gorgon_bunny/socket.rb +83 -0
  57. data/lib/gorgon_bunny/lib/gorgon_bunny/ssl_socket.rb +57 -0
  58. data/lib/gorgon_bunny/lib/gorgon_bunny/system_timer.rb +20 -0
  59. data/lib/gorgon_bunny/lib/gorgon_bunny/test_kit.rb +27 -0
  60. data/lib/gorgon_bunny/lib/gorgon_bunny/timeout.rb +18 -0
  61. data/lib/gorgon_bunny/lib/gorgon_bunny/transport.rb +398 -0
  62. data/lib/gorgon_bunny/lib/gorgon_bunny/version.rb +6 -0
  63. data/lib/gorgon_bunny/lib/gorgon_bunny/versioned_delivery_tag.rb +28 -0
  64. data/spec/crash_reporter_spec.rb +1 -1
  65. data/spec/gem_command_handler_spec.rb +2 -2
  66. data/spec/listener_spec.rb +5 -5
  67. data/spec/worker_manager_spec.rb +3 -3
  68. metadata +56 -17
@@ -0,0 +1,74 @@
1
+ module GorgonBunny
2
+ # Wraps GorgonAMQ::Protocol::Basic::Return to
3
+ # provide access to the delivery properties as immutable hash as
4
+ # well as methods.
5
+ class ReturnInfo
6
+
7
+ #
8
+ # Behaviors
9
+ #
10
+
11
+ include Enumerable
12
+
13
+ #
14
+ # API
15
+ #
16
+
17
+ def initialize(basic_return)
18
+ @basic_return = basic_return
19
+ @hash = {
20
+ :reply_code => basic_return.reply_code,
21
+ :reply_text => basic_return.reply_text,
22
+ :exchange => basic_return.exchange,
23
+ :routing_key => basic_return.routing_key
24
+ }
25
+ end
26
+
27
+ # Iterates over the returned delivery properties
28
+ # @see Enumerable#each
29
+ def each(*args, &block)
30
+ @hash.each(*args, &block)
31
+ end
32
+
33
+ # Accesses returned delivery properties by key
34
+ # @see Hash#[]
35
+ def [](k)
36
+ @hash[k]
37
+ end
38
+
39
+ # @return [Hash] Hash representation of this returned delivery info
40
+ def to_hash
41
+ @hash
42
+ end
43
+
44
+ # @private
45
+ def to_s
46
+ to_hash.to_s
47
+ end
48
+
49
+ # @private
50
+ def inspect
51
+ to_hash.inspect
52
+ end
53
+
54
+ # @return [Integer] Reply (status) code of the cause
55
+ def reply_code
56
+ @basic_return.reply_code
57
+ end
58
+
59
+ # @return [Integer] Reply (status) text of the cause, explaining why the message was returned
60
+ def reply_text
61
+ @basic_return.reply_text
62
+ end
63
+
64
+ # @return [String] Exchange the message was published to
65
+ def exchange
66
+ @basic_return.exchange
67
+ end
68
+
69
+ # @return [String] Routing key the message has
70
+ def routing_key
71
+ @basic_return.routing_key
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,1044 @@
1
+ require "socket"
2
+ require "thread"
3
+ require "monitor"
4
+
5
+ require "gorgon_bunny/transport"
6
+ require "gorgon_bunny/channel_id_allocator"
7
+ require "gorgon_bunny/heartbeat_sender"
8
+ require "gorgon_bunny/reader_loop"
9
+ require "gorgon_bunny/authentication/credentials_encoder"
10
+ require "gorgon_bunny/authentication/plain_mechanism_encoder"
11
+ require "gorgon_bunny/authentication/external_mechanism_encoder"
12
+
13
+ if defined?(JRUBY_VERSION)
14
+ require "gorgon_bunny/concurrent/linked_continuation_queue"
15
+ else
16
+ require "gorgon_bunny/concurrent/continuation_queue"
17
+ end
18
+
19
+ require "gorgon_amq/protocol/client"
20
+ require "gorgon_amq/settings"
21
+
22
+ module GorgonBunny
23
+ # Represents AMQP 0.9.1 connection (to a RabbitMQ node).
24
+ # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
25
+ class Session
26
+
27
+ # Default host used for connection
28
+ DEFAULT_HOST = "127.0.0.1"
29
+ # Default virtual host used for connection
30
+ DEFAULT_VHOST = "/"
31
+ # Default username used for connection
32
+ DEFAULT_USER = "guest"
33
+ # Default password used for connection
34
+ DEFAULT_PASSWORD = "guest"
35
+ # Default heartbeat interval, the same value as RabbitMQ 3.0 uses.
36
+ DEFAULT_HEARTBEAT = :server
37
+ # @private
38
+ DEFAULT_FRAME_MAX = 131072
39
+
40
+ # backwards compatibility
41
+ # @private
42
+ CONNECT_TIMEOUT = Transport::DEFAULT_CONNECTION_TIMEOUT
43
+
44
+ # @private
45
+ DEFAULT_CONTINUATION_TIMEOUT = if RUBY_VERSION.to_f < 1.9
46
+ 8000
47
+ else
48
+ 4000
49
+ end
50
+
51
+ # RabbitMQ client metadata
52
+ DEFAULT_CLIENT_PROPERTIES = {
53
+ :capabilities => {
54
+ :publisher_confirms => true,
55
+ :consumer_cancel_notify => true,
56
+ :exchange_exchange_bindings => true,
57
+ :"basic.nack" => true,
58
+ :"connection.blocked" => true,
59
+ # See http://www.rabbitmq.com/auth-notification.html
60
+ :authentication_failure_close => true
61
+ },
62
+ :product => "GorgonBunny",
63
+ :platform => ::RUBY_DESCRIPTION,
64
+ :version => GorgonBunny::VERSION,
65
+ :information => "http://rubybunny.info",
66
+ }
67
+
68
+ # @private
69
+ DEFAULT_LOCALE = "en_GB"
70
+
71
+ # Default reconnection interval for TCP connection failures
72
+ DEFAULT_NETWORK_RECOVERY_INTERVAL = 5.0
73
+
74
+
75
+ #
76
+ # API
77
+ #
78
+
79
+ # @return [GorgonBunny::Transport]
80
+ attr_reader :transport
81
+ attr_reader :status, :host, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :threaded
82
+ attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
83
+ attr_reader :default_channel
84
+ attr_reader :channel_id_allocator
85
+ # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
86
+ # @return [String]
87
+ attr_reader :mechanism
88
+ # @return [Logger]
89
+ attr_reader :logger
90
+ # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 4000.
91
+ attr_reader :continuation_timeout
92
+
93
+
94
+ # @param [String, Hash] connection_string_or_opts Connection string or a hash of connection options
95
+ # @param [Hash] optz Extra options not related to connection
96
+ #
97
+ # @option connection_string_or_opts [String] :host ("127.0.0.1") Hostname or IP address to connect to
98
+ # @option connection_string_or_opts [Integer] :port (5672) Port RabbitMQ listens on
99
+ # @option connection_string_or_opts [String] :username ("guest") Username
100
+ # @option connection_string_or_opts [String] :password ("guest") Password
101
+ # @option connection_string_or_opts [String] :vhost ("/") Virtual host to use
102
+ # @option connection_string_or_opts [Integer] :heartbeat (600) Heartbeat interval. 0 means no heartbeat.
103
+ # @option connection_string_or_opts [Integer] :network_recovery_interval (4) Recovery interval periodic network recovery will use. This includes initial pause after network failure.
104
+ # @option connection_string_or_opts [Boolean] :tls (false) Should TLS/SSL be used?
105
+ # @option connection_string_or_opts [String] :tls_cert (nil) Path to client TLS/SSL certificate file (.pem)
106
+ # @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
107
+ # @option connection_string_or_opts [Array<String>] :tls_ca_certificates Array of paths to TLS/SSL CA files (.pem), by default detected from OpenSSL configuration
108
+ # @option connection_string_or_opts [Integer] :continuation_timeout (4000) Timeout for client operations that expect a response (e.g. {GorgonBunny::Queue#get}), in milliseconds.
109
+ #
110
+ # @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
111
+ # @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
112
+ #
113
+ # @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
114
+ # @see http://rubybunny.info/articles/tls.html TLS/SSL guide
115
+ # @api public
116
+ def initialize(connection_string_or_opts = Hash.new, optz = Hash.new)
117
+ opts = case (ENV["RABBITMQ_URL"] || connection_string_or_opts)
118
+ when nil then
119
+ Hash.new
120
+ when String then
121
+ self.class.parse_uri(ENV["RABBITMQ_URL"] || connection_string_or_opts)
122
+ when Hash then
123
+ connection_string_or_opts
124
+ end.merge(optz)
125
+
126
+ @opts = opts
127
+ @host = self.hostname_from(opts)
128
+ @port = self.port_from(opts)
129
+ @user = self.username_from(opts)
130
+ @pass = self.password_from(opts)
131
+ @vhost = self.vhost_from(opts)
132
+ @logfile = opts[:log_file] || opts[:logfile] || STDOUT
133
+ @threaded = opts.fetch(:threaded, true)
134
+
135
+ self.init_logger(opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN)
136
+
137
+ # should automatic recovery from network failures be used?
138
+ @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
139
+ true
140
+ else
141
+ opts[:automatically_recover] || opts[:automatic_recovery]
142
+ end
143
+ @network_recovery_interval = opts.fetch(:network_recovery_interval, DEFAULT_NETWORK_RECOVERY_INTERVAL)
144
+ # in ms
145
+ @continuation_timeout = opts.fetch(:continuation_timeout, DEFAULT_CONTINUATION_TIMEOUT)
146
+
147
+ @status = :not_connected
148
+ @blocked = false
149
+
150
+ # these are negotiated with the broker during the connection tuning phase
151
+ @client_frame_max = opts.fetch(:frame_max, DEFAULT_FRAME_MAX)
152
+ @client_channel_max = opts.fetch(:channel_max, 65536)
153
+ @client_heartbeat = self.heartbeat_from(opts)
154
+
155
+ @client_properties = opts[:properties] || DEFAULT_CLIENT_PROPERTIES
156
+ @mechanism = opts.fetch(:auth_mechanism, "PLAIN")
157
+ @credentials_encoder = credentials_encoder_for(@mechanism)
158
+ @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
159
+
160
+ @mutex_impl = @opts.fetch(:mutex_impl, Monitor)
161
+
162
+ # mutex for the channel id => channel hash
163
+ @channel_mutex = @mutex_impl.new
164
+ # transport operations/continuations mutex. A workaround for
165
+ # the non-reentrant Ruby mutexes. MK.
166
+ @transport_mutex = @mutex_impl.new
167
+ @channels = Hash.new
168
+
169
+ @origin_thread = Thread.current
170
+
171
+ self.reset_continuations
172
+ self.initialize_transport
173
+ end
174
+
175
+ # @return [String] RabbitMQ hostname (or IP address) used
176
+ def hostname; self.host; end
177
+ # @return [String] Username used
178
+ def username; self.user; end
179
+ # @return [String] Password used
180
+ def password; self.pass; end
181
+ # @return [String] Virtual host used
182
+ def virtual_host; self.vhost; end
183
+
184
+ # @return [Integer] Heartbeat interval used
185
+ def heartbeat_interval; self.heartbeat; end
186
+
187
+ # @return [Boolean] true if this connection uses TLS (SSL)
188
+ def uses_tls?
189
+ @transport.uses_tls?
190
+ end
191
+ alias tls? uses_tls?
192
+
193
+ # @return [Boolean] true if this connection uses TLS (SSL)
194
+ def uses_ssl?
195
+ @transport.uses_ssl?
196
+ end
197
+ alias ssl? uses_ssl?
198
+
199
+ # @return [Boolean] true if this connection uses a separate thread for I/O activity
200
+ def threaded?
201
+ @threaded
202
+ end
203
+
204
+ # @private
205
+ attr_reader :mutex_impl
206
+
207
+ # Provides a way to fine tune the socket used by connection.
208
+ # Accepts a block that the socket will be yielded to.
209
+ def configure_socket(&block)
210
+ raise ArgumentError, "No block provided!" if block.nil?
211
+
212
+ @transport.configure_socket(&block)
213
+ end
214
+
215
+ # Starts the connection process.
216
+ #
217
+ # @see http://rubybunny.info/articles/getting_started.html
218
+ # @see http://rubybunny.info/articles/connecting.html
219
+ # @api public
220
+ def start
221
+ return self if connected?
222
+
223
+ @status = :connecting
224
+ # reset here for cases when automatic network recovery kicks in
225
+ # when we were blocked. MK.
226
+ @blocked = false
227
+ self.reset_continuations
228
+
229
+ begin
230
+ # close existing transport if we have one,
231
+ # to not leak sockets
232
+ @transport.maybe_initialize_socket
233
+
234
+ @transport.post_initialize_socket
235
+ @transport.connect
236
+
237
+ if @socket_configurator
238
+ @transport.configure_socket(&@socket_configurator)
239
+ end
240
+
241
+ self.init_connection
242
+ self.open_connection
243
+
244
+ @reader_loop = nil
245
+ self.start_reader_loop if threaded?
246
+
247
+ @default_channel = self.create_channel
248
+ rescue Exception => e
249
+ @status = :not_connected
250
+ raise e
251
+ end
252
+
253
+ self
254
+ end
255
+
256
+ # Socket operation timeout used by this connection
257
+ # @return [Integer]
258
+ # @private
259
+ def read_write_timeout
260
+ @transport.read_write_timeout
261
+ end
262
+
263
+ # Opens a new channel and returns it. This method will block the calling
264
+ # thread until the response is received and the channel is guaranteed to be
265
+ # opened (this operation is very fast and inexpensive).
266
+ #
267
+ # @return [GorgonBunny::Channel] Newly opened channel
268
+ def create_channel(n = nil, consumer_pool_size = 1)
269
+ if n && (ch = @channels[n])
270
+ ch
271
+ else
272
+ ch = GorgonBunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1))
273
+ ch.open
274
+ ch
275
+ end
276
+ end
277
+ alias channel create_channel
278
+
279
+ # Closes the connection. This involves closing all of its channels.
280
+ def close
281
+ if @transport.open?
282
+ close_all_channels
283
+
284
+ GorgonBunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) do
285
+ self.close_connection(true)
286
+ end
287
+
288
+ maybe_shutdown_reader_loop
289
+ close_transport
290
+
291
+ @status = :closed
292
+ end
293
+ end
294
+ alias stop close
295
+
296
+ # Creates a temporary channel, yields it to the block given to this
297
+ # method and closes it.
298
+ #
299
+ # @return [GorgonBunny::Session] self
300
+ def with_channel(n = nil)
301
+ ch = create_channel(n)
302
+ yield ch
303
+ ch.close if ch.open?
304
+
305
+ self
306
+ end
307
+
308
+ # @return [Boolean] true if this connection is still not fully open
309
+ def connecting?
310
+ status == :connecting
311
+ end
312
+
313
+ # @return [Boolean] true if this AMQP 0.9.1 connection is closed
314
+ def closed?
315
+ status == :closed
316
+ end
317
+
318
+ # @return [Boolean] true if this AMQP 0.9.1 connection is open
319
+ def open?
320
+ (status == :open || status == :connected || status == :connecting) && @transport.open?
321
+ end
322
+ alias connected? open?
323
+
324
+ # @return [Boolean] true if this connection has automatic recovery from network failure enabled
325
+ def automatically_recover?
326
+ @automatically_recover
327
+ end
328
+
329
+ #
330
+ # Backwards compatibility
331
+ #
332
+
333
+ # @private
334
+ def queue(*args)
335
+ @default_channel.queue(*args)
336
+ end
337
+
338
+ # @private
339
+ def direct(*args)
340
+ @default_channel.direct(*args)
341
+ end
342
+
343
+ # @private
344
+ def fanout(*args)
345
+ @default_channel.fanout(*args)
346
+ end
347
+
348
+ # @private
349
+ def topic(*args)
350
+ @default_channel.topic(*args)
351
+ end
352
+
353
+ # @private
354
+ def headers(*args)
355
+ @default_channel.headers(*args)
356
+ end
357
+
358
+ # @private
359
+ def exchange(*args)
360
+ @default_channel.exchange(*args)
361
+ end
362
+
363
+ # Defines a callback that will be executed when RabbitMQ blocks the connection
364
+ # because it is running low on memory or disk space (as configured via config file
365
+ # and/or rabbitmqctl).
366
+ #
367
+ # @yield [GorgonAMQ::Protocol::Connection::Blocked] connection.blocked method which provides a reason for blocking
368
+ #
369
+ # @api public
370
+ def on_blocked(&block)
371
+ @block_callback = block
372
+ end
373
+
374
+ # Defines a callback that will be executed when RabbitMQ unblocks the connection
375
+ # that was previously blocked, e.g. because the memory or disk space alarm has cleared.
376
+ #
377
+ # @see #on_blocked
378
+ # @api public
379
+ def on_unblocked(&block)
380
+ @unblock_callback = block
381
+ end
382
+
383
+ # @return [Boolean] true if the connection is currently blocked by RabbitMQ because it's running low on
384
+ # RAM, disk space, or other resource; false otherwise
385
+ # @see #on_blocked
386
+ # @see #on_unblocked
387
+ def blocked?
388
+ @blocked
389
+ end
390
+
391
+ # Parses an amqp[s] URI into a hash that {GorgonBunny::Session#initialize} accepts.
392
+ #
393
+ # @param [String] uri amqp or amqps URI to parse
394
+ # @return [Hash] Parsed URI as a hash
395
+ def self.parse_uri(uri)
396
+ GorgonAMQ::Settings.parse_amqp_url(uri)
397
+ end
398
+
399
+ # Checks if a queue with given name exists.
400
+ #
401
+ # Implemented using queue.declare
402
+ # with passive set to true and a one-off (short lived) channel
403
+ # under the hood.
404
+ #
405
+ # @param [String] name Queue name
406
+ # @return [Boolean] true if queue exists
407
+ def queue_exists?(name)
408
+ ch = create_channel
409
+ begin
410
+ ch.queue(name, :passive => true)
411
+ true
412
+ rescue GorgonBunny::NotFound => _
413
+ false
414
+ ensure
415
+ ch.close if ch.open?
416
+ end
417
+ end
418
+
419
+ # Checks if a exchange with given name exists.
420
+ #
421
+ # Implemented using exchange.declare
422
+ # with passive set to true and a one-off (short lived) channel
423
+ # under the hood.
424
+ #
425
+ # @param [String] name Exchange name
426
+ # @return [Boolean] true if exchange exists
427
+ def exchange_exists?(name)
428
+ ch = create_channel
429
+ begin
430
+ ch.exchange(name, :passive => true)
431
+ true
432
+ rescue GorgonBunny::NotFound => _
433
+ false
434
+ ensure
435
+ ch.close if ch.open?
436
+ end
437
+ end
438
+
439
+
440
+ #
441
+ # Implementation
442
+ #
443
+
444
+ # @private
445
+ def open_channel(ch)
446
+ n = ch.number
447
+ self.register_channel(ch)
448
+
449
+ @transport_mutex.synchronize do
450
+ @transport.send_frame(GorgonAMQ::Protocol::Channel::Open.encode(n, GorgonAMQ::Protocol::EMPTY_STRING))
451
+ end
452
+ @last_channel_open_ok = wait_on_continuations
453
+ raise_if_continuation_resulted_in_a_connection_error!
454
+
455
+ @last_channel_open_ok
456
+ end
457
+
458
+ # @private
459
+ def close_channel(ch)
460
+ n = ch.number
461
+
462
+ @transport.send_frame(GorgonAMQ::Protocol::Channel::Close.encode(n, 200, "Goodbye", 0, 0))
463
+ @last_channel_close_ok = wait_on_continuations
464
+ raise_if_continuation_resulted_in_a_connection_error!
465
+
466
+ self.unregister_channel(ch)
467
+ @last_channel_close_ok
468
+ end
469
+
470
+ # @private
471
+ def close_all_channels
472
+ @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
473
+ GorgonBunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) { ch.close }
474
+ end
475
+ end
476
+
477
+ # @private
478
+ def close_connection(sync = true)
479
+ if @transport.open?
480
+ @transport.send_frame(GorgonAMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
481
+
482
+ maybe_shutdown_heartbeat_sender
483
+ @status = :not_connected
484
+
485
+ if sync
486
+ @last_connection_close_ok = wait_on_continuations
487
+ end
488
+ end
489
+ end
490
+
491
+ # Handles incoming frames and dispatches them.
492
+ #
493
+ # Channel methods (`channel.open-ok`, `channel.close-ok`) are
494
+ # handled by the session itself.
495
+ # Connection level errors result in exceptions being raised.
496
+ # Deliveries and other methods are passed on to channels to dispatch.
497
+ #
498
+ # @private
499
+ def handle_frame(ch_number, method)
500
+ @logger.debug "Session#handle_frame on #{ch_number}: #{method.inspect}"
501
+ case method
502
+ when GorgonAMQ::Protocol::Channel::OpenOk then
503
+ @continuations.push(method)
504
+ when GorgonAMQ::Protocol::Channel::CloseOk then
505
+ @continuations.push(method)
506
+ when GorgonAMQ::Protocol::Connection::Close then
507
+ @last_connection_error = instantiate_connection_level_exception(method)
508
+ @continuations.push(method)
509
+
510
+ @origin_thread.raise(@last_connection_error)
511
+ when GorgonAMQ::Protocol::Connection::CloseOk then
512
+ @last_connection_close_ok = method
513
+ begin
514
+ @continuations.clear
515
+ rescue StandardError => e
516
+ @logger.error e.class.name
517
+ @logger.error e.message
518
+ @logger.error e.backtrace
519
+ ensure
520
+ @continuations.push(:__unblock__)
521
+ end
522
+ when GorgonAMQ::Protocol::Connection::Blocked then
523
+ @blocked = true
524
+ @block_callback.call(method) if @block_callback
525
+ when GorgonAMQ::Protocol::Connection::Unblocked then
526
+ @blocked = false
527
+ @unblock_callback.call(method) if @unblock_callback
528
+ when GorgonAMQ::Protocol::Channel::Close then
529
+ begin
530
+ ch = @channels[ch_number]
531
+ ch.handle_method(method)
532
+ ensure
533
+ self.unregister_channel(ch)
534
+ end
535
+ when GorgonAMQ::Protocol::Basic::GetEmpty then
536
+ @channels[ch_number].handle_basic_get_empty(method)
537
+ else
538
+ if ch = @channels[ch_number]
539
+ ch.handle_method(method)
540
+ else
541
+ @logger.warn "Channel #{ch_number} is not open on this connection!"
542
+ end
543
+ end
544
+ end
545
+
546
+ # @private
547
+ def raise_if_continuation_resulted_in_a_connection_error!
548
+ raise @last_connection_error if @last_connection_error
549
+ end
550
+
551
+ # @private
552
+ def handle_frameset(ch_number, frames)
553
+ method = frames.first
554
+
555
+ case method
556
+ when GorgonAMQ::Protocol::Basic::GetOk then
557
+ @channels[ch_number].handle_basic_get_ok(*frames)
558
+ when GorgonAMQ::Protocol::Basic::GetEmpty then
559
+ @channels[ch_number].handle_basic_get_empty(*frames)
560
+ when GorgonAMQ::Protocol::Basic::Return then
561
+ @channels[ch_number].handle_basic_return(*frames)
562
+ else
563
+ @channels[ch_number].handle_frameset(*frames)
564
+ end
565
+ end
566
+
567
+ # @private
568
+ def handle_network_failure(exception)
569
+ raise NetworkErrorWrapper.new(exception) unless @threaded
570
+
571
+ @status = :disconnected
572
+
573
+ if !recovering_from_network_failure?
574
+ @recovering_from_network_failure = true
575
+ if recoverable_network_failure?(exception)
576
+ @logger.warn "Recovering from a network failure..."
577
+ @channels.each do |n, ch|
578
+ ch.maybe_kill_consumer_work_pool!
579
+ end
580
+ maybe_shutdown_heartbeat_sender
581
+
582
+ recover_from_network_failure
583
+ else
584
+ # TODO: investigate if we can be a bit smarter here. MK.
585
+ end
586
+ end
587
+ end
588
+
589
+ # @private
590
+ def recoverable_network_failure?(exception)
591
+ # TODO: investigate if we can be a bit smarter here. MK.
592
+ true
593
+ end
594
+
595
+ # @private
596
+ def recovering_from_network_failure?
597
+ @recovering_from_network_failure
598
+ end
599
+
600
+ # @private
601
+ def recover_from_network_failure
602
+ begin
603
+ sleep @network_recovery_interval
604
+ @logger.debug "About to start connection recovery..."
605
+ self.initialize_transport
606
+ self.start
607
+
608
+ if open?
609
+ @recovering_from_network_failure = false
610
+
611
+ recover_channels
612
+ end
613
+ rescue TCPConnectionFailed, GorgonAMQ::Protocol::EmptyResponseError => e
614
+ @logger.warn "TCP connection failed, reconnecting in 5 seconds"
615
+ sleep @network_recovery_interval
616
+ retry if recoverable_network_failure?(e)
617
+ end
618
+ end
619
+
620
+ # @private
621
+ def recover_channels
622
+ # default channel is reopened right after connection
623
+ # negotiation is completed, so make sure we do not try to open
624
+ # it twice. MK.
625
+ @channels.reject { |n, ch| ch == @default_channel }.each do |n, ch|
626
+ ch.open
627
+
628
+ ch.recover_from_network_failure
629
+ end
630
+ end
631
+
632
+ # @private
633
+ def instantiate_connection_level_exception(frame)
634
+ case frame
635
+ when GorgonAMQ::Protocol::Connection::Close then
636
+ klass = case frame.reply_code
637
+ when 320 then
638
+ ConnectionForced
639
+ when 501 then
640
+ FrameError
641
+ when 503 then
642
+ CommandInvalid
643
+ when 504 then
644
+ ChannelError
645
+ when 505 then
646
+ UnexpectedFrame
647
+ when 506 then
648
+ ResourceError
649
+ when 541 then
650
+ InternalError
651
+ else
652
+ raise "Unknown reply code: #{frame.reply_code}, text: #{frame.reply_text}"
653
+ end
654
+
655
+ klass.new("Connection-level error: #{frame.reply_text}", self, frame)
656
+ end
657
+ end
658
+
659
+ # @private
660
+ def hostname_from(options)
661
+ options[:host] || options[:hostname] || DEFAULT_HOST
662
+ end
663
+
664
+ # @private
665
+ def port_from(options)
666
+ fallback = if options[:tls] || options[:ssl]
667
+ GorgonAMQ::Protocol::TLS_PORT
668
+ else
669
+ GorgonAMQ::Protocol::DEFAULT_PORT
670
+ end
671
+
672
+ options.fetch(:port, fallback)
673
+ end
674
+
675
+ # @private
676
+ def vhost_from(options)
677
+ options[:virtual_host] || options[:vhost] || DEFAULT_VHOST
678
+ end
679
+
680
+ # @private
681
+ def username_from(options)
682
+ options[:username] || options[:user] || DEFAULT_USER
683
+ end
684
+
685
+ # @private
686
+ def password_from(options)
687
+ options[:password] || options[:pass] || options[:pwd] || DEFAULT_PASSWORD
688
+ end
689
+
690
+ # @private
691
+ def heartbeat_from(options)
692
+ options[:heartbeat] || options[:heartbeat_interval] || options[:requested_heartbeat] || DEFAULT_HEARTBEAT
693
+ end
694
+
695
+ # @private
696
+ def next_channel_id
697
+ @channel_id_allocator.next_channel_id
698
+ end
699
+
700
+ # @private
701
+ def release_channel_id(i)
702
+ @channel_id_allocator.release_channel_id(i)
703
+ end
704
+
705
+ # @private
706
+ def register_channel(ch)
707
+ @channel_mutex.synchronize do
708
+ @channels[ch.number] = ch
709
+ end
710
+ end
711
+
712
+ # @private
713
+ def unregister_channel(ch)
714
+ @channel_mutex.synchronize do
715
+ n = ch.number
716
+
717
+ self.release_channel_id(n)
718
+ @channels.delete(ch.number)
719
+ end
720
+ end
721
+
722
+ # @private
723
+ def start_reader_loop
724
+ reader_loop.start
725
+ end
726
+
727
+ # @private
728
+ def reader_loop
729
+ @reader_loop ||= ReaderLoop.new(@transport, self, Thread.current)
730
+ end
731
+
732
+ # @private
733
+ def maybe_shutdown_reader_loop
734
+ if @reader_loop
735
+ @reader_loop.stop
736
+ if threaded?
737
+ # this is the easiest way to wait until the loop
738
+ # is guaranteed to have terminated
739
+ @reader_loop.raise(ShutdownSignal)
740
+ # joining the thread here may take forever
741
+ # on JRuby because sun.nio.ch.KQueueArrayWrapper#kevent0 is
742
+ # a native method that cannot be (easily) interrupted.
743
+ # So we use this ugly hack or else our test suite takes forever
744
+ # to run on JRuby (a new connection is opened/closed per example). MK.
745
+ if defined?(JRUBY_VERSION)
746
+ sleep 0.075
747
+ else
748
+ @reader_loop.join
749
+ end
750
+ else
751
+ # single threaded mode, nothing to do. MK.
752
+ end
753
+ end
754
+
755
+ @reader_loop = nil
756
+ end
757
+
758
+ # @private
759
+ def close_transport
760
+ begin
761
+ @transport.close
762
+ rescue StandardError => e
763
+ @logger.error "Exception when closing transport:"
764
+ @logger.error e.class.name
765
+ @logger.error e.message
766
+ @logger.error e.backtrace
767
+ end
768
+ end
769
+
770
+ # @private
771
+ def signal_activity!
772
+ @heartbeat_sender.signal_activity! if @heartbeat_sender
773
+ end
774
+
775
+
776
+ # Sends frame to the peer, checking that connection is open.
777
+ # Exposed primarily for GorgonBunny::Channel
778
+ #
779
+ # @raise [ConnectionClosedError]
780
+ # @private
781
+ def send_frame(frame, signal_activity = true)
782
+ if open?
783
+ @transport.write(frame.encode)
784
+ signal_activity! if signal_activity
785
+ else
786
+ raise ConnectionClosedError.new(frame)
787
+ end
788
+ end
789
+
790
+ # Sends frame to the peer, checking that connection is open.
791
+ # Uses transport implementation that does not perform
792
+ # timeout control. Exposed primarily for GorgonBunny::Channel.
793
+ #
794
+ # @raise [ConnectionClosedError]
795
+ # @private
796
+ def send_frame_without_timeout(frame, signal_activity = true)
797
+ if open?
798
+ @transport.write_without_timeout(frame.encode)
799
+ signal_activity! if signal_activity
800
+ else
801
+ raise ConnectionClosedError.new(frame)
802
+ end
803
+ end
804
+
805
+ # Sends multiple frames, one by one. For thread safety this method takes a channel
806
+ # object and synchronizes on it.
807
+ #
808
+ # @private
809
+ def send_frameset(frames, channel)
810
+ # some developers end up sharing channels between threads and when multiple
811
+ # threads publish on the same channel aggressively, at some point frames will be
812
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
813
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
814
+ # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
815
+ channel.synchronize do
816
+ frames.each { |frame| self.send_frame(frame, false) }
817
+ signal_activity!
818
+ end
819
+ end # send_frameset(frames)
820
+
821
+ # Sends multiple frames, one by one. For thread safety this method takes a channel
822
+ # object and synchronizes on it. Uses transport implementation that does not perform
823
+ # timeout control.
824
+ #
825
+ # @private
826
+ def send_frameset_without_timeout(frames, channel)
827
+ # some developers end up sharing channels between threads and when multiple
828
+ # threads publish on the same channel aggressively, at some point frames will be
829
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
830
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
831
+ # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
832
+ channel.synchronize do
833
+ frames.each { |frame| self.send_frame_without_timeout(frame, false) }
834
+ signal_activity!
835
+ end
836
+ end # send_frameset_without_timeout(frames)
837
+
838
+ # @private
839
+ def send_raw_without_timeout(data, channel)
840
+ # some developers end up sharing channels between threads and when multiple
841
+ # threads publish on the same channel aggressively, at some point frames will be
842
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
843
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
844
+ # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
845
+ channel.synchronize do
846
+ @transport.write(data)
847
+ signal_activity!
848
+ end
849
+ end # send_frameset_without_timeout(frames)
850
+
851
+ # @return [String]
852
+ # @api public
853
+ def to_s
854
+ "#<#{self.class.name}:#{object_id} #{@user}@#{@host}:#{@port}, vhost=#{@vhost}>"
855
+ end
856
+
857
+ protected
858
+
859
+ # @private
860
+ def init_connection
861
+ self.send_preamble
862
+
863
+ connection_start = @transport.read_next_frame.decode_payload
864
+
865
+ @server_properties = connection_start.server_properties
866
+ @server_capabilities = @server_properties["capabilities"]
867
+
868
+ @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ")
869
+ @server_locales = Array(connection_start.locales)
870
+
871
+ @status = :connected
872
+ end
873
+
874
+ # @private
875
+ def open_connection
876
+ @transport.send_frame(GorgonAMQ::Protocol::Connection::StartOk.encode(@client_properties, @mechanism, self.encode_credentials(username, password), @locale))
877
+ @logger.debug "Sent connection.start-ok"
878
+
879
+ frame = begin
880
+ @transport.read_next_frame
881
+ # frame timeout means the broker has closed the TCP connection, which it
882
+ # does per 0.9.1 spec.
883
+ rescue Errno::ECONNRESET, ClientTimeout, GorgonAMQ::Protocol::EmptyResponseError, EOFError, IOError => e
884
+ nil
885
+ end
886
+ if frame.nil?
887
+ @state = :closed
888
+ @logger.error "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
889
+ raise GorgonBunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
890
+ end
891
+
892
+ response = frame.decode_payload
893
+ if response.is_a?(GorgonAMQ::Protocol::Connection::Close)
894
+ @state = :closed
895
+ @logger.error "Authentication with RabbitMQ failed: #{response.reply_code} #{response.reply_text}"
896
+ raise GorgonBunny::AuthenticationFailureError.new(self.user, self.vhost, self.password.size)
897
+ end
898
+
899
+
900
+
901
+ connection_tune = response
902
+
903
+ @frame_max = negotiate_value(@client_frame_max, connection_tune.frame_max)
904
+ @channel_max = negotiate_value(@client_channel_max, connection_tune.channel_max)
905
+ # this allows for disabled heartbeats. MK.
906
+ @heartbeat = if heartbeat_disabled?(@client_heartbeat)
907
+ 0
908
+ else
909
+ negotiate_value(@client_heartbeat, connection_tune.heartbeat)
910
+ end
911
+ @logger.debug "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}"
912
+ @logger.info "Heartbeat interval used (in seconds): #{@heartbeat}"
913
+
914
+ @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
915
+
916
+ @transport.send_frame(GorgonAMQ::Protocol::Connection::TuneOk.encode(@channel_max, @frame_max, @heartbeat))
917
+ @logger.debug "Sent connection.tune-ok with heartbeat interval = #{@heartbeat}, frame_max = #{@frame_max}, channel_max = #{@channel_max}"
918
+ @transport.send_frame(GorgonAMQ::Protocol::Connection::Open.encode(self.vhost))
919
+ @logger.debug "Sent connection.open with vhost = #{self.vhost}"
920
+
921
+ frame2 = begin
922
+ @transport.read_next_frame
923
+ # frame timeout means the broker has closed the TCP connection, which it
924
+ # does per 0.9.1 spec.
925
+ rescue Errno::ECONNRESET, ClientTimeout, GorgonAMQ::Protocol::EmptyResponseError, EOFError => e
926
+ nil
927
+ end
928
+ if frame2.nil?
929
+ @state = :closed
930
+ @logger.warn "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
931
+ raise GorgonBunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
932
+ end
933
+ connection_open_ok = frame2.decode_payload
934
+
935
+ @status = :open
936
+ if @heartbeat && @heartbeat > 0
937
+ initialize_heartbeat_sender
938
+ end
939
+
940
+ raise "could not open connection: server did not respond with connection.open-ok" unless connection_open_ok.is_a?(GorgonAMQ::Protocol::Connection::OpenOk)
941
+ end
942
+
943
+ def heartbeat_disabled?(val)
944
+ 0 == val || val.nil?
945
+ end
946
+
947
+ # @private
948
+ def negotiate_value(client_value, server_value)
949
+ return server_value if client_value == :server
950
+
951
+ if client_value == 0 || server_value == 0
952
+ [client_value, server_value].max
953
+ else
954
+ [client_value, server_value].min
955
+ end
956
+ end
957
+
958
+ # @private
959
+ def initialize_heartbeat_sender
960
+ @logger.debug "Initializing heartbeat sender..."
961
+ @heartbeat_sender = HeartbeatSender.new(@transport, @logger)
962
+ @heartbeat_sender.start(@heartbeat)
963
+ end
964
+
965
+ # @private
966
+ def maybe_shutdown_heartbeat_sender
967
+ @heartbeat_sender.stop if @heartbeat_sender
968
+ end
969
+
970
+ # @private
971
+ def initialize_transport
972
+ @transport = Transport.new(self, @host, @port, @opts.merge(:session_thread => @origin_thread))
973
+ end
974
+
975
+ # @private
976
+ def maybe_close_transport
977
+ @transport.close if @transport
978
+ end
979
+
980
+ # Sends AMQ protocol header (also known as preamble).
981
+ # @private
982
+ def send_preamble
983
+ @transport.write(GorgonAMQ::Protocol::PREAMBLE)
984
+ @logger.debug "Sent protocol preamble"
985
+ end
986
+
987
+
988
+ # @private
989
+ def encode_credentials(username, password)
990
+ @credentials_encoder.encode_credentials(username, password)
991
+ end # encode_credentials(username, password)
992
+
993
+ # @private
994
+ def credentials_encoder_for(mechanism)
995
+ Authentication::CredentialsEncoder.for_session(self)
996
+ end
997
+
998
+ if defined?(JRUBY_VERSION)
999
+ # @private
1000
+ def reset_continuations
1001
+ @continuations = Concurrent::LinkedContinuationQueue.new
1002
+ end
1003
+ else
1004
+ # @private
1005
+ def reset_continuations
1006
+ @continuations = Concurrent::ContinuationQueue.new
1007
+ end
1008
+ end
1009
+
1010
+ # @private
1011
+ def wait_on_continuations
1012
+ unless @threaded
1013
+ reader_loop.run_once until @continuations.length > 0
1014
+ end
1015
+
1016
+ @continuations.poll(@continuation_timeout)
1017
+ end
1018
+
1019
+ # @private
1020
+ def init_logger(level)
1021
+ @logger = ::Logger.new(@logfile)
1022
+ @logger.level = normalize_log_level(level)
1023
+ @logger.progname = self.to_s
1024
+
1025
+ @logger
1026
+ end
1027
+
1028
+ # @private
1029
+ def normalize_log_level(level)
1030
+ case level
1031
+ when :debug, Logger::DEBUG, "debug" then Logger::DEBUG
1032
+ when :info, Logger::INFO, "info" then Logger::INFO
1033
+ when :warn, Logger::WARN, "warn" then Logger::WARN
1034
+ when :error, Logger::ERROR, "error" then Logger::ERROR
1035
+ when :fatal, Logger::FATAL, "fatal" then Logger::FATAL
1036
+ else
1037
+ Logger::WARN
1038
+ end
1039
+ end
1040
+ end # Session
1041
+
1042
+ # backwards compatibility
1043
+ Client = Session
1044
+ end