amq-client 0.7.0.alpha34 → 0.7.0.alpha35

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