amq-client 0.7.0.alpha34 → 0.7.0.alpha35

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.
Files changed (32) hide show
  1. data/.travis.yml +4 -0
  2. data/Gemfile +1 -1
  3. data/README.textile +1 -1
  4. data/bin/ci/before_build.sh +24 -0
  5. data/examples/eventmachine_adapter/extensions/rabbitmq/handling_confirm_select_ok.rb +2 -2
  6. data/examples/eventmachine_adapter/extensions/rabbitmq/publisher_confirmations_with_transient_messages.rb +1 -1
  7. data/examples/eventmachine_adapter/extensions/rabbitmq/publisher_confirmations_with_unroutable_message.rb +1 -1
  8. data/lib/amq/client.rb +29 -17
  9. data/lib/amq/client/adapter.rb +8 -504
  10. data/lib/amq/client/adapters/coolio.rb +4 -282
  11. data/lib/amq/client/adapters/event_machine.rb +4 -382
  12. data/lib/amq/client/async/adapter.rb +517 -0
  13. data/lib/amq/client/async/adapters/coolio.rb +291 -0
  14. data/lib/amq/client/async/adapters/event_machine.rb +392 -0
  15. data/lib/amq/client/async/adapters/eventmachine.rb +1 -0
  16. data/lib/amq/client/async/callbacks.rb +71 -0
  17. data/lib/amq/client/async/channel.rb +385 -0
  18. data/lib/amq/client/async/entity.rb +66 -0
  19. data/lib/amq/client/async/exchange.rb +157 -0
  20. data/lib/amq/client/async/extensions/rabbitmq/basic.rb +38 -0
  21. data/lib/amq/client/async/extensions/rabbitmq/confirm.rb +248 -0
  22. data/lib/amq/client/async/queue.rb +455 -0
  23. data/lib/amq/client/callbacks.rb +6 -65
  24. data/lib/amq/client/channel.rb +4 -376
  25. data/lib/amq/client/entity.rb +6 -57
  26. data/lib/amq/client/exchange.rb +4 -148
  27. data/lib/amq/client/extensions/rabbitmq/basic.rb +4 -28
  28. data/lib/amq/client/extensions/rabbitmq/confirm.rb +5 -240
  29. data/lib/amq/client/queue.rb +5 -450
  30. data/lib/amq/client/version.rb +1 -1
  31. data/spec/unit/client_spec.rb +10 -30
  32. metadata +16 -22
@@ -0,0 +1,517 @@
1
+ # encoding: utf-8
2
+
3
+ require "amq/client/logging"
4
+ require "amq/client/settings"
5
+ require "amq/client/async/entity"
6
+ require "amq/client/async/channel"
7
+
8
+ module AMQ
9
+ # For overview of AMQP client adapters API, see {AMQ::Client::Adapter}
10
+ module Client
11
+ module Async
12
+
13
+ # Base adapter class. Specific implementations (for example, EventMachine-based, Cool.io-based or
14
+ # sockets-based) subclass it and must implement Adapter API methods:
15
+ #
16
+ # * #send_raw(data)
17
+ # * #estabilish_connection(settings)
18
+ # * #close_connection
19
+ #
20
+ # @abstract
21
+ module Adapter
22
+
23
+ def self.included(host)
24
+ host.extend ClassMethods
25
+ host.extend ProtocolMethodHandlers
26
+
27
+ host.class_eval do
28
+
29
+ #
30
+ # API
31
+ #
32
+
33
+ attr_accessor :logger
34
+ attr_accessor :settings
35
+
36
+ # @return [Array<#call>]
37
+ attr_reader :callbacks
38
+
39
+
40
+ # The locale defines the language in which the server will send reply texts.
41
+ #
42
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.2)
43
+ attr_accessor :locale
44
+
45
+ # Client capabilities
46
+ #
47
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.2.1)
48
+ attr_accessor :client_properties
49
+
50
+ # Server properties
51
+ #
52
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.1.3)
53
+ attr_reader :server_properties
54
+
55
+ # Server capabilities
56
+ #
57
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.1.3)
58
+ attr_reader :server_capabilities
59
+
60
+ # Locales server supports
61
+ #
62
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.1.3)
63
+ attr_reader :server_locales
64
+
65
+ # Authentication mechanism used.
66
+ #
67
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.2)
68
+ attr_reader :mechanism
69
+
70
+ # Authentication mechanisms broker supports.
71
+ #
72
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.2)
73
+ attr_reader :server_authentication_mechanisms
74
+
75
+ # Channels within this connection.
76
+ #
77
+ # @see http://bit.ly/hw2ELX AMQP 0.9.1 specification (Section 2.2.5)
78
+ attr_reader :channels
79
+
80
+ # Maximum channel number that the server permits this connection to use.
81
+ # Usable channel numbers are in the range 1..channel_max.
82
+ # Zero indicates no specified limit.
83
+ #
84
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Sections 1.4.2.5.1 and 1.4.2.6.1)
85
+ attr_accessor :channel_max
86
+
87
+ # Maximum frame size that the server permits this connection to use.
88
+ #
89
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Sections 1.4.2.5.2 and 1.4.2.6.2)
90
+ attr_accessor :frame_max
91
+
92
+
93
+ attr_reader :known_hosts
94
+
95
+
96
+
97
+ # @api plugin
98
+ # @see #disconnect
99
+ # @note Adapters must implement this method but it is NOT supposed to be used directly.
100
+ # AMQ protocol defines two-step process of closing connection (send Connection.Close
101
+ # to the peer and wait for Connection.Close-Ok), implemented by {Adapter#disconnect}
102
+ def close_connection
103
+ raise MissingInterfaceMethodError.new("AMQ::Client.close_connection")
104
+ end unless defined?(:close_connection) # since it is a module, this method may already be defined
105
+ end
106
+ end # self.included(host)
107
+
108
+
109
+
110
+ module ClassMethods
111
+ # Settings
112
+ def settings
113
+ @settings ||= AMQ::Client::Settings.default
114
+ end
115
+
116
+ def logger
117
+ @logger ||= begin
118
+ require "logger"
119
+ Logger.new(STDERR)
120
+ end
121
+ end
122
+
123
+ def logger=(logger)
124
+ methods = AMQ::Client::Logging::REQUIRED_METHODS
125
+ unless methods.all? { |method| logger.respond_to?(method) }
126
+ raise AMQ::Client::Logging::IncompatibleLoggerError.new(methods)
127
+ end
128
+
129
+ @logger = logger
130
+ end
131
+
132
+ # @return [Boolean] Current value of logging flag.
133
+ def logging
134
+ settings[:logging]
135
+ end
136
+
137
+ # Turns loggin on or off.
138
+ def logging=(boolean)
139
+ settings[:logging] = boolean
140
+ end
141
+
142
+
143
+ # Establishes connection to AMQ broker and returns it. New connection object is yielded to
144
+ # the block if it is given.
145
+ #
146
+ # @example Specifying adapter via the :adapter option
147
+ # AMQ::Client::Adapter.connect(:adapter => "socket")
148
+ # @example Specifying using custom adapter class
149
+ # AMQ::Client::SocketClient.connect
150
+ # @param [Hash] Connection parameters, including :adapter to use.
151
+ # @api public
152
+ def connect(settings = nil, &block)
153
+ @settings = Settings.configure(settings)
154
+
155
+ instance = self.new
156
+ instance.establish_connection(settings)
157
+ instance.register_connection_callback(&block)
158
+
159
+ instance
160
+ end
161
+
162
+
163
+ # Can be overriden by higher-level libraries like amqp gem or bunny.
164
+ # Defaults to AMQ::Client::TCPConnectionFailed.
165
+ #
166
+ # @return [Class]
167
+ def tcp_connection_failure_exception_class
168
+ @tcp_connection_failure_exception_class ||= AMQ::Client::TCPConnectionFailed
169
+ end # tcp_connection_failure_exception_class
170
+
171
+ # Can be overriden by higher-level libraries like amqp gem or bunny.
172
+ # Defaults to AMQ::Client::PossibleAuthenticationFailure.
173
+ #
174
+ # @return [Class]
175
+ def authentication_failure_exception_class
176
+ @authentication_failure_exception_class ||= AMQ::Client::PossibleAuthenticationFailureError
177
+ end # authentication_failure_exception_class
178
+ end # ClassMethods
179
+
180
+
181
+ #
182
+ # Behaviors
183
+ #
184
+
185
+ include Openable
186
+ include Callbacks
187
+
188
+
189
+ extend RegisterEntityMixin
190
+
191
+ register_entity :channel, AMQ::Client::Async::Channel
192
+
193
+
194
+ #
195
+ # API
196
+ #
197
+
198
+
199
+ # Establish socket connection to the server.
200
+ #
201
+ # @api plugin
202
+ def establish_connection(settings)
203
+ raise MissingInterfaceMethodError.new("AMQ::Client#establish_connection(settings)")
204
+ end
205
+
206
+ # Properly close connection with AMQ broker, as described in
207
+ # section 2.2.4 of the {http://bit.ly/hw2ELX AMQP 0.9.1 specification}.
208
+ #
209
+ # @api plugin
210
+ # @see #close_connection
211
+ def disconnect(reply_code = 200, reply_text = "Goodbye", class_id = 0, method_id = 0, &block)
212
+ @intentionally_closing_connection = true
213
+ self.on_disconnection(&block)
214
+
215
+ # ruby-amqp/amqp#66, MK.
216
+ if self.open?
217
+ closing!
218
+ self.send_frame(Protocol::Connection::Close.encode(reply_code, reply_text, class_id, method_id))
219
+ elsif self.closing?
220
+ # no-op
221
+ else
222
+ self.disconnection_successful
223
+ end
224
+ end
225
+
226
+
227
+ # Sends AMQ protocol header (also known as preamble).
228
+ #
229
+ # @note This must be implemented by all AMQP clients.
230
+ # @api plugin
231
+ # @see http://bit.ly/hw2ELX AMQP 0.9.1 specification (Section 2.2)
232
+ def send_preamble
233
+ self.send_raw(AMQ::Protocol::PREAMBLE)
234
+ end
235
+
236
+ # Sends frame to the peer, checking that connection is open.
237
+ #
238
+ # @raise [ConnectionClosedError]
239
+ def send_frame(frame)
240
+ if closed?
241
+ raise ConnectionClosedError.new(frame)
242
+ else
243
+ self.send_raw(frame.encode)
244
+ end
245
+ end
246
+
247
+ # Sends multiple frames, one by one.
248
+ #
249
+ # @api public
250
+ def send_frameset(frames)
251
+ frames.each { |frame| self.send_frame(frame) }
252
+ end # send_frameset(frames)
253
+
254
+
255
+
256
+ # Returns heartbeat interval this client uses, in seconds.
257
+ # This value may or may not be used depending on broker capabilities.
258
+ # Zero means the server does not want a heartbeat.
259
+ #
260
+ # @return [Fixnum] Heartbeat interval this client uses, in seconds.
261
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.6)
262
+ def heartbeat_interval
263
+ @settings[:heartbeat] || @settings[:heartbeat_interval] || 0
264
+ end # heartbeat_interval
265
+
266
+
267
+ # vhost this connection uses. Default is "/", a historically estabilished convention
268
+ # of RabbitMQ and amqp gem.
269
+ #
270
+ # @return [String] vhost this connection uses
271
+ # @api public
272
+ def vhost
273
+ @settings.fetch(:vhost, "/")
274
+ end # vhost
275
+
276
+
277
+ # Called when previously established TCP connection fails.
278
+ # @api public
279
+ def tcp_connection_lost
280
+ @on_tcp_connection_loss.call(self, @settings) if @on_tcp_connection_loss
281
+ end
282
+
283
+ # Called when initial TCP connection fails.
284
+ # @api public
285
+ def tcp_connection_failed
286
+ @on_tcp_connection_failure.call(@settings) if @on_tcp_connection_failure
287
+ end
288
+
289
+
290
+
291
+ #
292
+ # Implementation
293
+ #
294
+
295
+
296
+ # Sends opaque data to AMQ broker over active connection.
297
+ #
298
+ # @note This must be implemented by all AMQP clients.
299
+ # @api plugin
300
+ def send_raw(data)
301
+ raise MissingInterfaceMethodError.new("AMQ::Client#send_raw(data)")
302
+ end
303
+
304
+ # Sends connection preamble to the broker.
305
+ # @api plugin
306
+ def handshake
307
+ @authenticating = true
308
+ self.send_preamble
309
+ end
310
+
311
+
312
+ # Sends connection.open to the server.
313
+ #
314
+ # @api plugin
315
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.7)
316
+ def open(vhost = "/")
317
+ self.send_frame(Protocol::Connection::Open.encode(vhost))
318
+ end
319
+
320
+ # Resets connection state.
321
+ #
322
+ # @api plugin
323
+ def reset_state!
324
+ # no-op by default
325
+ end # reset_state!
326
+
327
+ # @api plugin
328
+ # @see http://tools.ietf.org/rfc/rfc2595.txt RFC 2595
329
+ def encode_credentials(username, password)
330
+ "\0#{username}\0#{password}"
331
+ end # encode_credentials(username, password)
332
+
333
+
334
+ # Processes a single frame.
335
+ #
336
+ # @param [AMQ::Protocol::Frame] frame
337
+ # @api plugin
338
+ def receive_frame(frame)
339
+ @frames << frame
340
+ if frameset_complete?(@frames)
341
+ receive_frameset(@frames)
342
+ @frames.clear
343
+ else
344
+ # puts "#{frame.inspect} is NOT final"
345
+ end
346
+ end
347
+
348
+ # Processes a frameset by finding and invoking a suitable handler.
349
+ # Heartbeat frames are treated in a special way: they simply update @last_server_heartbeat
350
+ # value.
351
+ #
352
+ # @param [Array<AMQ::Protocol::Frame>] frames
353
+ # @api plugin
354
+ def receive_frameset(frames)
355
+ frame = frames.first
356
+
357
+ if Protocol::HeartbeatFrame === frame
358
+ @last_server_heartbeat = Time.now
359
+ else
360
+ if callable = AMQ::Client::HandlersRegistry.find(frame.method_class)
361
+ callable.call(self, frames.first, frames[1..-1])
362
+ else
363
+ raise MissingHandlerError.new(frames.first)
364
+ end
365
+ end
366
+ end
367
+
368
+ # Sends a heartbeat frame if connection is open.
369
+ # @api plugin
370
+ def send_heartbeat
371
+ if tcp_connection_established?
372
+ if @last_server_heartbeat < (Time.now - (self.heartbeat_interval * 2))
373
+ logger.error "Reconnecting due to missing server heartbeats"
374
+ # TODO: reconnect
375
+ end
376
+ send_frame(Protocol::HeartbeatFrame)
377
+ end
378
+ end # send_heartbeat
379
+
380
+
381
+
382
+
383
+ # @group Error handling
384
+
385
+ # Defines a callback that will be executed when channel is closed after
386
+ # channel-level exception. Only one callback can be defined (the one defined last
387
+ # replaces previously added ones).
388
+ #
389
+ # @api public
390
+ def on_error(&block)
391
+ self.redefine_callback(:error, &block)
392
+ end
393
+
394
+ # @endgroup
395
+
396
+
397
+
398
+
399
+ # Handles connection.start.
400
+ #
401
+ # @api plugin
402
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.1.)
403
+ def handle_start(connection_start)
404
+ @server_properties = connection_start.server_properties
405
+ @server_capabilities = @server_properties["capabilities"]
406
+
407
+ @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ")
408
+ @server_locales = Array(connection_start.locales)
409
+
410
+ username = @settings[:user] || @settings[:username]
411
+ password = @settings[:pass] || @settings[:password]
412
+
413
+ # It's not clear whether we should transition to :opening state here
414
+ # or in #open but in case authentication fails, it would be strange to have
415
+ # @status undefined. So lets do this. MK.
416
+ opening!
417
+
418
+ self.send_frame(Protocol::Connection::StartOk.encode(@client_properties, @mechanism, self.encode_credentials(username, password), @locale))
419
+ end
420
+
421
+
422
+ # Handles Connection.Tune-Ok.
423
+ #
424
+ # @api plugin
425
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.6)
426
+ def handle_tune(tune_ok)
427
+ @channel_max = tune_ok.channel_max.freeze
428
+ @frame_max = tune_ok.frame_max.freeze
429
+ @heartbeat_interval = self.heartbeat_interval || tune_ok.heartbeat
430
+
431
+ self.send_frame(Protocol::Connection::TuneOk.encode(@channel_max, [settings[:frame_max], @frame_max].min, @heartbeat_interval))
432
+ end # handle_tune(method)
433
+
434
+
435
+ # Handles Connection.Open-Ok.
436
+ #
437
+ # @api plugin
438
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.8.)
439
+ def handle_open_ok(open_ok)
440
+ @known_hosts = open_ok.known_hosts.dup.freeze
441
+
442
+ opened!
443
+ self.connection_successful if self.respond_to?(:connection_successful)
444
+ end
445
+
446
+
447
+ # Handles connection.close. When broker detects a connection level exception, this method is called.
448
+ #
449
+ # @api plugin
450
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.5.2.9)
451
+ def handle_close(conn_close)
452
+ self.handle_connection_interruption
453
+
454
+ closed!
455
+ # TODO: use proper exception class, provide protocol class (we know conn_close.class_id and conn_close.method_id) as well!
456
+ self.exec_callback_yielding_self(:error, conn_close)
457
+ end
458
+
459
+
460
+ # Handles Connection.Close-Ok.
461
+ #
462
+ # @api plugin
463
+ # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.4.2.10)
464
+ def handle_close_ok(close_ok)
465
+ closed!
466
+ self.disconnection_successful
467
+ end # handle_close_ok(close_ok)
468
+
469
+ # @api plugin
470
+ def handle_connection_interruption
471
+ @channels.each { |n, c| c.handle_connection_interruption }
472
+ end # handle_connection_interruption
473
+
474
+
475
+
476
+ protected
477
+
478
+ # Returns next frame from buffer whenever possible
479
+ #
480
+ # @api private
481
+ def get_next_frame
482
+ return nil unless @chunk_buffer.size > 7 # otherwise, cannot read the length
483
+ # octet + short
484
+ offset = 3 # 1 + 2
485
+ # length
486
+ payload_length = @chunk_buffer[offset, 4].unpack(AMQ::Protocol::PACK_UINT32).first
487
+ # 4 bytes for long payload length, 1 byte final octet
488
+ frame_length = offset + payload_length + 5
489
+ if frame_length <= @chunk_buffer.size
490
+ @chunk_buffer.slice!(0, frame_length)
491
+ else
492
+ nil
493
+ end
494
+ end # def get_next_frame
495
+
496
+ # Utility methods
497
+
498
+ # Determines, whether the received frameset is ready to be further processed
499
+ def frameset_complete?(frames)
500
+ return false if frames.empty?
501
+ first_frame = frames[0]
502
+ first_frame.final? || (first_frame.method_class.has_content? && content_complete?(frames[1..-1]))
503
+ end
504
+
505
+ # Determines, whether given frame array contains full content body
506
+ def content_complete?(frames)
507
+ return false if frames.empty?
508
+ header = frames[0]
509
+ raise "Not a content header frame first: #{header.inspect}" unless header.kind_of?(AMQ::Protocol::HeaderFrame)
510
+ header.body_size == frames[1..-1].inject(0) {|sum, frame| sum + frame.payload.size }
511
+ end
512
+
513
+ end # Adapter
514
+
515
+ end # Async
516
+ end # Client
517
+ end # AMQ