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,291 @@
1
+ # encoding: utf-8
2
+
3
+ # http://coolio.github.com
4
+
5
+ require "cool.io"
6
+ require "amq/client"
7
+ require "amq/client/framing/string/frame"
8
+
9
+ module AMQ
10
+ module Client
11
+ module Async
12
+ #
13
+ # CoolioClient is a drop-in replacement for EventMachineClient, if you prefer
14
+ # cool.io style.
15
+ #
16
+ class CoolioClient
17
+
18
+ #
19
+ # Behaviors
20
+ #
21
+
22
+ include AMQ::Client::Async::Adapter
23
+
24
+
25
+ #
26
+ # API
27
+ #
28
+
29
+
30
+ #
31
+ # Cool.io socket delegates most of its operations to the parent adapter.
32
+ # Thus, 99.9% of the time you don't need to deal with this class.
33
+ #
34
+ # @api private
35
+ # @private
36
+ class Socket < ::Coolio::TCPSocket
37
+ attr_accessor :adapter
38
+
39
+ # Connects to given host/port and sets parent adapter.
40
+ #
41
+ # @param [CoolioClient]
42
+ # @param [String]
43
+ # @param [Fixnum]
44
+ def self.connect(adapter, host, port)
45
+ socket = super(host, port)
46
+ socket.adapter = adapter
47
+ socket
48
+ end
49
+
50
+ # Triggers socket_connect callback
51
+ def on_connect
52
+ #puts "On connect"
53
+ adapter.socket_connected
54
+ end
55
+
56
+ # Triggers on_read callback
57
+ def on_read(data)
58
+ # puts "Received data"
59
+ # puts_data(data)
60
+ adapter.receive_data(data)
61
+ end
62
+
63
+ # Triggers socket_disconnect callback
64
+ def on_close
65
+ adapter.socket_disconnected
66
+ end
67
+
68
+ # Triggers tcp_connection_failed callback
69
+ def on_connect_failed
70
+ adapter.tcp_connection_failed
71
+ end
72
+
73
+ # Sends raw data through the socket
74
+ #
75
+ # param [String] Binary data
76
+ def send_raw(data)
77
+ # puts "Sending data"
78
+ # puts_data(data)
79
+ write(data)
80
+ end
81
+
82
+ protected
83
+ # Debugging routine
84
+ def puts_data(data)
85
+ puts " As string: #{data.inspect}"
86
+ puts " As byte array: #{data.bytes.to_a.inspect}"
87
+ end
88
+ end
89
+
90
+
91
+
92
+ # Cool.io socket for multiplexing et al.
93
+ #
94
+ # @private
95
+ attr_accessor :socket
96
+
97
+ # Hash with available callbacks
98
+ attr_accessor :callbacks
99
+
100
+ # Creates a socket and attaches it to cool.io default loop.
101
+ #
102
+ # Called from CoolioClient.connect
103
+ #
104
+ # @see AMQ::Client::Adapter::ClassMethods#connect
105
+ # @param [Hash] connection settings
106
+ # @api private
107
+ def establish_connection(settings)
108
+ @settings = Settings.configure(settings)
109
+
110
+ socket = Socket.connect(self, @settings[:host], @settings[:port])
111
+ socket.attach(Cool.io::Loop.default)
112
+ self.socket = socket
113
+
114
+
115
+ @on_tcp_connection_failure = @settings[:on_tcp_connection_failure] || Proc.new { |settings|
116
+ raise self.class.tcp_connection_failure_exception_class.new(settings)
117
+ }
118
+ @on_possible_authentication_failure = @settings[:on_possible_authentication_failure] || Proc.new { |settings|
119
+ raise self.class.authentication_failure_exception_class.new(settings)
120
+ }
121
+
122
+ @locale = @settings.fetch(:locale, "en_GB")
123
+ @client_properties = Settings.client_properties.merge(@settings.fetch(:client_properties, Hash.new))
124
+
125
+ socket
126
+ end
127
+
128
+ # Registers on_open callback
129
+ # @see #on_open
130
+ # @api private
131
+ def register_connection_callback(&block)
132
+ self.on_open(&block)
133
+ end
134
+
135
+ # Performs basic initialization. Do not use this method directly, use
136
+ # CoolioClient.connect instead
137
+ #
138
+ # @see AMQ::Client::Adapter::ClassMethods#connect
139
+ # @api private
140
+ def initialize
141
+ # Be careful with default values for #ruby hashes: h = Hash.new(Array.new); h[:key] ||= 1
142
+ # won't assign anything to :key. MK.
143
+ @callbacks = Hash.new
144
+
145
+ self.logger = self.class.logger
146
+
147
+ @frames = Array.new
148
+ @channels = Hash.new
149
+
150
+ @mechanism = "PLAIN"
151
+ end
152
+
153
+ # Sets a callback for successful connection (after we receive open-ok)
154
+ #
155
+ # @api public
156
+ def on_open(&block)
157
+ define_callback :connect, &block
158
+ end
159
+ alias on_connection on_open
160
+
161
+ # Sets a callback for disconnection (as in client-side disconnection)
162
+ #
163
+ # @api public
164
+ def on_closed(&block)
165
+ define_callback :disconnect, &block
166
+ end
167
+ alias on_disconnection on_closed
168
+
169
+ # Sets a callback for tcp connection failure (as in can't make initial connection)
170
+ def on_tcp_connection_failure(&block)
171
+ define_callback :tcp_connection_failure, &block
172
+ end
173
+
174
+
175
+ # Called by AMQ::Client::Connection after we receive connection.open-ok.
176
+ #
177
+ # @api private
178
+ def connection_successful
179
+ @authenticating = false
180
+ opened!
181
+
182
+ exec_callback_yielding_self(:connect)
183
+ end
184
+
185
+
186
+ # Called by AMQ::Client::Connection after we receive connection.close-ok.
187
+ #
188
+ # @api private
189
+ def disconnection_successful
190
+ exec_callback_yielding_self(:disconnect)
191
+ close_connection
192
+ closed!
193
+ end
194
+
195
+
196
+
197
+ # Called when socket is connected but before handshake is done
198
+ #
199
+ # @api private
200
+ def socket_connected
201
+ post_init
202
+ end
203
+
204
+ # Called after socket is closed
205
+ #
206
+ # @api private
207
+ def socket_disconnected
208
+ end
209
+
210
+ alias close disconnect
211
+
212
+
213
+ self.handle(Protocol::Connection::Start) do |connection, frame|
214
+ connection.handle_start(frame.decode_payload)
215
+ end
216
+
217
+ self.handle(Protocol::Connection::Tune) do |connection, frame|
218
+ connection.handle_tune(frame.decode_payload)
219
+
220
+ connection.open(connection.vhost)
221
+ end
222
+
223
+ self.handle(Protocol::Connection::OpenOk) do |connection, frame|
224
+ connection.handle_open_ok(frame.decode_payload)
225
+ end
226
+
227
+ self.handle(Protocol::Connection::Close) do |connection, frame|
228
+ connection.handle_close(frame.decode_payload)
229
+ end
230
+
231
+ self.handle(Protocol::Connection::CloseOk) do |connection, frame|
232
+ connection.handle_close_ok(frame.decode_payload)
233
+ end
234
+
235
+
236
+
237
+
238
+
239
+ # Sends raw data through the socket
240
+ #
241
+ # @param [String] binary data
242
+ # @api private
243
+ def send_raw(data)
244
+ socket.send_raw data
245
+ end
246
+
247
+
248
+ # The story about the buffering is kinda similar to EventMachine,
249
+ # you keep receiving more than one frame in a single packet.
250
+ #
251
+ # @param [String] chunk with binary data received. It could be one frame,
252
+ # more than one frame or less than one frame.
253
+ # @api private
254
+ def receive_data(chunk)
255
+ @chunk_buffer << chunk
256
+ while frame = get_next_frame
257
+ receive_frame(AMQ::Client::Framing::String::Frame.decode(frame))
258
+ end
259
+ end
260
+
261
+ # Closes the socket.
262
+ #
263
+ # @api private
264
+ def close_connection
265
+ @socket.close
266
+ end
267
+
268
+ # Returns class used for tcp connection failures.
269
+ #
270
+ # @api private
271
+ def self.tcp_connection_failure_exception_class
272
+ AMQ::Client::TCPConnectionFailed
273
+ end # self.tcp_connection_failure_exception_class
274
+
275
+ protected
276
+
277
+ # @api private
278
+ def post_init
279
+ self.reset
280
+ self.handshake
281
+ end
282
+
283
+ # @api private
284
+ def reset
285
+ @chunk_buffer = ""
286
+ @frames = Array.new
287
+ end
288
+ end # CoolioClient
289
+ end # Async
290
+ end # Client
291
+ end # AMQ
@@ -0,0 +1,392 @@
1
+ # encoding: utf-8
2
+
3
+ require "eventmachine"
4
+ require "amq/client"
5
+ require "amq/client/async/adapter"
6
+ require "amq/client/framing/string/frame"
7
+
8
+ module AMQ
9
+ module Client
10
+ module Async
11
+ class EventMachineClient < EM::Connection
12
+
13
+ #
14
+ # Behaviors
15
+ #
16
+
17
+ include AMQ::Client::Async::Adapter
18
+
19
+
20
+ #
21
+ # API
22
+ #
23
+
24
+ # Initiates connection to AMQP broker. If callback is given, runs it when (and if) AMQP connection
25
+ # succeeds.
26
+ #
27
+ # @option settings [String] :host ("127.0.0.1") Hostname AMQ broker runs on.
28
+ # @option settings [String] :port (5672) Port AMQ broker listens on.
29
+ # @option settings [String] :vhost ("/") Virtual host to use.
30
+ # @option settings [String] :user ("guest") Username to use for authentication.
31
+ # @option settings [String] :pass ("guest") Password to use for authentication.
32
+ # @option settings [String] :ssl (false) Should be use TLS (SSL) for connection?
33
+ # @option settings [String] :timeout (nil) Connection timeout.
34
+ # @option settings [String] :logging (false) Turns logging on or off.
35
+ # @option settings [String] :broker (nil) Broker name (use if you intend to use broker-specific features).
36
+ # @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.
37
+ #
38
+ # @param [Hash] settings
39
+ def self.connect(settings = {}, &block)
40
+ @settings = Settings.configure(settings)
41
+
42
+ instance = EventMachine.connect(@settings[:host], @settings[:port], self, @settings)
43
+ instance.register_connection_callback(&block)
44
+
45
+ instance
46
+ end
47
+
48
+ # Reconnect after a period of wait.
49
+ #
50
+ # @param [Fixnum] period Period of time, in seconds, to wait before reconnection attempt.
51
+ # @param [Boolean] force If true, enforces immediate reconnection.
52
+ # @api public
53
+ def reconnect(force = false, period = 5)
54
+ if @reconnecting and not force
55
+ EventMachine::Timer.new(period) {
56
+ reconnect(true, period)
57
+ }
58
+ return
59
+ end
60
+
61
+ if !@reconnecting
62
+ @reconnecting = true
63
+
64
+ self.handle_connection_interruption
65
+ self.reset
66
+ end
67
+
68
+ EventMachine.reconnect(@settings[:host], @settings[:port], self)
69
+ end
70
+
71
+
72
+
73
+
74
+
75
+ # Defines a callback that will be executed when AMQP connection is considered open:
76
+ # client and broker has agreed on max channel identifier and maximum allowed frame
77
+ # size and authentication succeeds. You can define more than one callback.
78
+ #
79
+ # @see on_possible_authentication_failure
80
+ # @api public
81
+ def on_open(&block)
82
+ @connection_deferrable.callback(&block)
83
+ end # on_open(&block)
84
+ alias on_connection on_open
85
+
86
+ # Defines a callback that will be run when broker confirms connection termination
87
+ # (client receives connection.close-ok). You can define more than one callback.
88
+ #
89
+ # @api public
90
+ def on_closed(&block)
91
+ @disconnection_deferrable.callback(&block)
92
+ end # on_closed(&block)
93
+ alias on_disconnection on_closed
94
+
95
+ # Defines a callback that will be run when initial TCP connection fails.
96
+ # You can define only one callback.
97
+ #
98
+ # @api public
99
+ def on_tcp_connection_failure(&block)
100
+ @on_tcp_connection_failure = block
101
+ end
102
+
103
+ # Defines a callback that will be run when TCP connection to AMQP broker is lost (interrupted).
104
+ # You can define only one callback.
105
+ #
106
+ # @api public
107
+ def on_tcp_connection_loss(&block)
108
+ @on_tcp_connection_loss = block
109
+ end
110
+
111
+ # Defines a callback that will be run when TCP connection is closed before authentication
112
+ # finishes. Usually this means authentication failure. You can define only one callback.
113
+ #
114
+ # @api public
115
+ def on_possible_authentication_failure(&block)
116
+ @on_possible_authentication_failure = block
117
+ end
118
+
119
+ # @see #on_open
120
+ # @private
121
+ def register_connection_callback(&block)
122
+ unless block.nil?
123
+ # delay calling block we were given till after we receive
124
+ # connection.open-ok. Connection will notify us when
125
+ # that happens.
126
+ self.on_open do
127
+ block.call(self)
128
+ end
129
+ end
130
+ end
131
+
132
+
133
+
134
+
135
+ def initialize(*args)
136
+ super(*args)
137
+
138
+ self.logger = self.class.logger
139
+
140
+ @frames = Array.new
141
+ @channels = Hash.new
142
+ @callbacks = Hash.new
143
+
144
+ opening!
145
+
146
+ # track TCP connection state, used to detect initial TCP connection failures.
147
+ @tcp_connection_established = false
148
+ @tcp_connection_failed = false
149
+ @intentionally_closing_connection = false
150
+
151
+ # EventMachine::Connection's and Adapter's constructors arity
152
+ # make it easier to use *args. MK.
153
+ @settings = Settings.configure(args.first)
154
+ @on_tcp_connection_failure = @settings[:on_tcp_connection_failure] || Proc.new { |settings|
155
+ raise self.class.tcp_connection_failure_exception_class.new(settings)
156
+ }
157
+ @on_possible_authentication_failure = @settings[:on_possible_authentication_failure] || Proc.new { |settings|
158
+ raise self.class.authentication_failure_exception_class.new(settings)
159
+ }
160
+
161
+ @mechanism = "PLAIN"
162
+ @locale = @settings.fetch(:locale, "en_GB")
163
+ @client_properties = Settings.client_properties.merge(@settings.fetch(:client_properties, Hash.new))
164
+
165
+ self.reset
166
+ self.set_pending_connect_timeout((@settings[:timeout] || 3).to_f) unless defined?(JRUBY_VERSION)
167
+
168
+ if self.heartbeat_interval > 0
169
+ @last_server_heartbeat = Time.now
170
+ EventMachine.add_periodic_timer(self.heartbeat_interval, &method(:send_heartbeat))
171
+ end
172
+ end # initialize(*args)
173
+
174
+
175
+
176
+ # For EventMachine adapter, this is a no-op.
177
+ # @api public
178
+ def establish_connection(settings)
179
+ # Unfortunately there doesn't seem to be any sane way
180
+ # how to get EventMachine connect to the instance level.
181
+ end
182
+
183
+ alias close disconnect
184
+
185
+
186
+
187
+ # Whether we are in authentication state (after TCP connection was estabilished
188
+ # but before broker authenticated us).
189
+ #
190
+ # @return [Boolean]
191
+ # @api public
192
+ def authenticating?
193
+ @authenticating
194
+ end # authenticating?
195
+
196
+ # IS TCP connection estabilished and currently active?
197
+ # @return [Boolean]
198
+ # @api public
199
+ def tcp_connection_established?
200
+ @tcp_connection_established
201
+ end # tcp_connection_established?
202
+
203
+
204
+
205
+
206
+
207
+
208
+ #
209
+ # Implementation
210
+ #
211
+
212
+ # Backwards compatibility with 0.7.0.a25. MK.
213
+ Deferrable = EventMachine::DefaultDeferrable
214
+
215
+
216
+ alias send_raw send_data
217
+
218
+
219
+ # EventMachine reactor callback. Is run when TCP connection is estabilished
220
+ # but before resumption of the network loop. Note that this includes cases
221
+ # when TCP connection has failed.
222
+ # @private
223
+ def post_init
224
+ reset
225
+
226
+ # note that upgrading to TLS in #connection_completed causes
227
+ # Erlang SSL app that RabbitMQ relies on to report
228
+ # error on TCP connection <0.1465.0>:{ssl_upgrade_error,"record overflow"}
229
+ # and close TCP connection down. Investigation of this issue is likely
230
+ # to take some time and to not be worth in as long as #post_init
231
+ # works fine. MK.
232
+ upgrade_to_tls_if_necessary
233
+ rescue Exception => error
234
+ raise error
235
+ end # post_init
236
+
237
+
238
+
239
+ # Called by EventMachine reactor once TCP connection is successfully estabilished.
240
+ # @private
241
+ def connection_completed
242
+ # we only can safely set this value here because EventMachine is a lovely piece of
243
+ # software that calls #post_init before #unbind even when TCP connection
244
+ # fails. MK.
245
+ @tcp_connection_established = true
246
+ # again, this is because #unbind is called in different situations
247
+ # and there is no easy way to tell initial connection failure
248
+ # from connection loss. Not in EventMachine 0.12.x, anyway. MK.
249
+ @had_successfull_connected_before = true
250
+
251
+ @reconnecting = false
252
+
253
+ self.handshake
254
+ end
255
+
256
+ # @private
257
+ def close_connection(*args)
258
+ @intentionally_closing_connection = true
259
+
260
+ super(*args)
261
+ end
262
+
263
+ # Called by EventMachine reactor when
264
+ #
265
+ # * We close TCP connection down
266
+ # * Our peer closes TCP connection down
267
+ # * There is a network connection issue
268
+ # * Initial TCP connection fails
269
+ # @private
270
+ def unbind(exception = nil)
271
+ if !@tcp_connection_established && !@had_successfull_connected_before && !@intentionally_closing_connection
272
+ @tcp_connection_failed = true
273
+ self.tcp_connection_failed
274
+ end
275
+
276
+ closing!
277
+ @tcp_connection_established = false
278
+
279
+ self.handle_connection_interruption
280
+ @disconnection_deferrable.succeed
281
+
282
+ closed!
283
+
284
+
285
+ self.tcp_connection_lost if !@intentionally_closing_connection && @had_successfull_connected_before
286
+
287
+ # since AMQP spec dictates that authentication failure is a protocol exception
288
+ # and protocol exceptions result in connection closure, check whether we are
289
+ # in the authentication stage. If so, it is likely to signal an authentication
290
+ # issue. Java client behaves the same way. MK.
291
+ if authenticating? && !@intentionally_closing_connection
292
+ @on_possible_authentication_failure.call(@settings) if @on_possible_authentication_failure
293
+ end
294
+ end # unbind
295
+
296
+
297
+ #
298
+ # EventMachine receives data in chunks, sometimes those chunks are smaller
299
+ # than the size of AMQP frame. That's why you need to add some kind of buffer.
300
+ #
301
+ # @private
302
+ def receive_data(chunk)
303
+ @chunk_buffer << chunk
304
+ while frame = get_next_frame
305
+ self.receive_frame(AMQ::Client::Framing::String::Frame.decode(frame))
306
+ end
307
+ end
308
+
309
+
310
+ # Called by AMQ::Client::Connection after we receive connection.open-ok.
311
+ # @api public
312
+ def connection_successful
313
+ @authenticating = false
314
+ opened!
315
+
316
+ @connection_deferrable.succeed
317
+ end # connection_successful
318
+
319
+
320
+ # Called by AMQ::Client::Connection after we receive connection.close-ok.
321
+ #
322
+ # @api public
323
+ def disconnection_successful
324
+ @disconnection_deferrable.succeed
325
+
326
+ # true for "after writing buffered data"
327
+ self.close_connection(true)
328
+ self.reset
329
+ closed!
330
+ end # disconnection_successful
331
+
332
+
333
+
334
+
335
+
336
+ self.handle(Protocol::Connection::Start) do |connection, frame|
337
+ connection.handle_start(frame.decode_payload)
338
+ end
339
+
340
+ self.handle(Protocol::Connection::Tune) do |connection, frame|
341
+ connection.handle_tune(frame.decode_payload)
342
+
343
+ connection.open(connection.vhost)
344
+ end
345
+
346
+ self.handle(Protocol::Connection::OpenOk) do |connection, frame|
347
+ connection.handle_open_ok(frame.decode_payload)
348
+ end
349
+
350
+ self.handle(Protocol::Connection::Close) do |connection, frame|
351
+ connection.handle_close(frame.decode_payload)
352
+ end
353
+
354
+ self.handle(Protocol::Connection::CloseOk) do |connection, frame|
355
+ connection.handle_close_ok(frame.decode_payload)
356
+ end
357
+
358
+
359
+
360
+
361
+ protected
362
+
363
+
364
+ def reset
365
+ @size = 0
366
+ @payload = ""
367
+ @frames = Array.new
368
+
369
+ @chunk_buffer = ""
370
+ @connection_deferrable = EventMachine::DefaultDeferrable.new
371
+ @disconnection_deferrable = EventMachine::DefaultDeferrable.new
372
+
373
+ # used to track down whether authentication succeeded. AMQP 0.9.1 dictates
374
+ # that on authentication failure broker must close TCP connection without sending
375
+ # any more data. This is why we need to explicitly track whether we are past
376
+ # authentication stage to signal possible authentication failures.
377
+ @authenticating = false
378
+ end
379
+
380
+ def upgrade_to_tls_if_necessary
381
+ tls_options = @settings[:ssl]
382
+
383
+ if tls_options.is_a?(Hash)
384
+ start_tls(tls_options)
385
+ elsif tls_options
386
+ start_tls
387
+ end
388
+ end # upgrade_to_tls_if_necessary
389
+ end # EventMachineClient
390
+ end # Async
391
+ end # Client
392
+ end # AMQ