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,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