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,66 @@
1
+ # encoding: utf-8
2
+
3
+ require "amq/client/openable"
4
+ require "amq/client/async/callbacks"
5
+
6
+ module AMQ
7
+ module Client
8
+ module Async
9
+ module RegisterEntityMixin
10
+ # @example Registering Channel implementation
11
+ # Adapter.register_entity(:channel, Channel)
12
+ # # ... so then I can do:
13
+ # channel = client.channel(1)
14
+ # # instead of:
15
+ # channel = Channel.new(client, 1)
16
+ def register_entity(name, klass)
17
+ define_method(name) do |*args, &block|
18
+ klass.new(self, *args, &block)
19
+ end # define_method
20
+ end # register_entity
21
+ end # RegisterEntityMixin
22
+
23
+ module ProtocolMethodHandlers
24
+ def handle(klass, &block)
25
+ AMQ::Client::HandlersRegistry.register(klass, &block)
26
+ end
27
+
28
+ def handlers
29
+ AMQ::Client::HandlersRegistry.handlers
30
+ end
31
+ end # ProtocolMethodHandlers
32
+
33
+
34
+ # AMQ entities, as implemented by AMQ::Client, have callbacks and can run them
35
+ # when necessary.
36
+ #
37
+ # @note Exchanges and queues implementation is based on this class.
38
+ #
39
+ # @abstract
40
+ module Entity
41
+
42
+ #
43
+ # Behaviors
44
+ #
45
+
46
+ include Openable
47
+ include Async::Callbacks
48
+
49
+ #
50
+ # API
51
+ #
52
+
53
+ # @return [Array<#call>]
54
+ attr_reader :callbacks
55
+
56
+
57
+ def initialize(connection)
58
+ @connection = connection
59
+ # Be careful with default values for #ruby hashes: h = Hash.new(Array.new); h[:key] ||= 1
60
+ # won't assign anything to :key. MK.
61
+ @callbacks = Hash.new
62
+ end # initialize
63
+ end # Entity
64
+ end # Async
65
+ end # Client
66
+ end # AMQ
@@ -0,0 +1,157 @@
1
+ # encoding: utf-8
2
+
3
+ require "amq/client/entity"
4
+ require "amq/client/server_named_entity"
5
+
6
+ module AMQ
7
+ module Client
8
+ module Async
9
+ class Exchange
10
+
11
+
12
+ include Entity
13
+ include ServerNamedEntity
14
+ extend ProtocolMethodHandlers
15
+
16
+ TYPES = [:fanout, :direct, :topic, :headers].freeze
17
+
18
+ class IncompatibleExchangeTypeError < StandardError
19
+ def initialize(types, given)
20
+ super("#{given.inspect} exchange type is unknown. Standard types are #{TYPES.inspect}, custom exchange types must begin with x-, for example: x-recent-history")
21
+ end
22
+ end
23
+
24
+
25
+ #
26
+ # API
27
+ #
28
+
29
+ # Channel this exchange belongs to.
30
+ attr_reader :channel
31
+
32
+ # Exchange name. May be server-generated or assigned directly.
33
+ attr_reader :name
34
+
35
+ # @return [Symbol] One of :direct, :fanout, :topic, :headers
36
+ attr_reader :type
37
+
38
+ def initialize(connection, channel, name, type = :fanout)
39
+ if !(TYPES.include?(type.to_sym) || type.to_s =~ /^x-.+/i)
40
+ raise IncompatibleExchangeTypeError.new(TYPES, type)
41
+ end
42
+
43
+ @connection = connection
44
+ @channel = channel
45
+ @name = name
46
+ @type = type
47
+
48
+ # register pre-declared exchanges
49
+ if @name == AMQ::Protocol::EMPTY_STRING || @name =~ /^amq\.(fanout|topic)/
50
+ @channel.register_exchange(self)
51
+ end
52
+
53
+ super(connection)
54
+ end
55
+
56
+
57
+ def fanout?
58
+ @type == :fanout
59
+ end
60
+
61
+ def direct?
62
+ @type == :direct
63
+ end
64
+
65
+ def topic?
66
+ @type == :topic
67
+ end
68
+
69
+
70
+
71
+ def declare(passive = false, durable = false, auto_delete = false, nowait = false, arguments = nil, &block)
72
+ @connection.send_frame(Protocol::Exchange::Declare.encode(@channel.id, @name, @type.to_s, passive, durable, auto_delete, false, nowait, arguments))
73
+
74
+ unless nowait
75
+ self.define_callback(:declare, &block)
76
+ @channel.exchanges_awaiting_declare_ok.push(self)
77
+ end
78
+
79
+ self
80
+ end
81
+
82
+
83
+ def delete(if_unused = false, nowait = false, &block)
84
+ @connection.send_frame(Protocol::Exchange::Delete.encode(@channel.id, @name, if_unused, nowait))
85
+
86
+ unless nowait
87
+ self.define_callback(:delete, &block)
88
+
89
+ # TODO: delete itself from exchanges cache
90
+ @channel.exchanges_awaiting_delete_ok.push(self)
91
+ end
92
+
93
+ self
94
+ end # delete(if_unused = false, nowait = false)
95
+
96
+
97
+ def publish(payload, routing_key = AMQ::Protocol::EMPTY_STRING, user_headers = {}, mandatory = false, immediate = false, frame_size = nil)
98
+ headers = { :priority => 0, :delivery_mode => 2, :content_type => "application/octet-stream" }.merge(user_headers)
99
+ @connection.send_frameset(Protocol::Basic::Publish.encode(@channel.id, payload, headers, @name, routing_key, mandatory, immediate, (frame_size || @connection.frame_max)))
100
+
101
+ self
102
+ end
103
+
104
+
105
+ def on_return(&block)
106
+ self.redefine_callback(:return, &block)
107
+
108
+ self
109
+ end # on_return(&block)
110
+
111
+
112
+
113
+
114
+ def handle_declare_ok(method)
115
+ @name = method.exchange if self.anonymous?
116
+ @channel.register_exchange(self)
117
+
118
+ self.exec_callback_once_yielding_self(:declare, method)
119
+ end
120
+
121
+ def handle_delete_ok(method)
122
+ self.exec_callback_once(:delete, method)
123
+ end # handle_delete_ok(method)
124
+
125
+
126
+
127
+ self.handle(Protocol::Exchange::DeclareOk) do |connection, frame|
128
+ method = frame.decode_payload
129
+ channel = connection.channels[frame.channel]
130
+ exchange = channel.exchanges_awaiting_declare_ok.shift
131
+
132
+ exchange.handle_declare_ok(method)
133
+ end # handle
134
+
135
+
136
+ self.handle(Protocol::Exchange::DeleteOk) do |connection, frame|
137
+ channel = connection.channels[frame.channel]
138
+ exchange = channel.exchanges_awaiting_delete_ok.shift
139
+ exchange.handle_delete_ok(frame.decode_payload)
140
+ end # handle
141
+
142
+
143
+ self.handle(Protocol::Basic::Return) do |connection, frame, content_frames|
144
+ channel = connection.channels[frame.channel]
145
+ method = frame.decode_payload
146
+ exchange = channel.find_exchange(method.exchange)
147
+
148
+ header = content_frames.shift
149
+ body = content_frames.map { |frame| frame.payload }.join
150
+
151
+ exchange.exec_callback(:return, method, header, body)
152
+ end
153
+
154
+ end # Exchange
155
+ end # Async
156
+ end # Client
157
+ end # AMQ
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require "amq/client/async/channel"
4
+
5
+ # Basic.Nack
6
+ module AMQ
7
+ module Client
8
+ module Async
9
+ module Extensions
10
+ module RabbitMQ
11
+ module Basic
12
+ module ChannelMixin
13
+
14
+ # Overrides {AMQ::Client::Channel#reject} behavior to use basic.nack.
15
+ #
16
+ # @api public
17
+ # @see http://www.rabbitmq.com/amqp-0-9-1-quickref.html#basic.nack
18
+ def reject(delivery_tag, requeue = true, multi = false)
19
+ if multi
20
+ @connection.send_frame(Protocol::Basic::Nack.encode(self.id, delivery_tag, multi, requeue))
21
+ else
22
+ super(delivery_tag, requeue)
23
+ end
24
+ end # reject
25
+
26
+ end # ChannelMixin
27
+ end # Basic
28
+ end # RabbitMQ
29
+ end # Extensions
30
+
31
+ class Channel
32
+ # use modules, the native Ruby way of extension of existing classes,
33
+ # instead of reckless monkey-patching. MK.
34
+ include Extensions::RabbitMQ::Basic::ChannelMixin
35
+ end # Channel
36
+ end # Async
37
+ end # Client
38
+ end # AMQ
@@ -0,0 +1,248 @@
1
+ # encoding: utf-8
2
+
3
+ module AMQ
4
+ module Client
5
+ module Async
6
+ module Extensions
7
+ module RabbitMQ
8
+ # h2. Purpose
9
+ # In case that the broker crashes, some messages can get lost.
10
+ # Thanks to this extension, broker sends Basic.Ack when the message
11
+ # is processed by the broker. In case of persistent messages, it must
12
+ # be written to disk or ack'd on all the queues it was delivered to.
13
+ # However it doesn't have to be necessarily 1:1, because the broker
14
+ # can send Basic.Ack with multi flag to acknowledge multiple messages.
15
+ #
16
+ # So it provides clients a lightweight way of keeping track of which
17
+ # messages have been processed by the broker and which would need
18
+ # re-publishing in case of broker shutdown or network failure.
19
+ #
20
+ # Transactions are solving the same problem, but they are very slow:
21
+ # confirmations are more than 100 times faster.
22
+ #
23
+ # h2. Workflow
24
+ # * Client asks broker to confirm messages on given channel (Confirm.Select).
25
+ # * Broker sends back Confirm.Select-Ok, unless we sent Confirm.Select with nowait=true.
26
+ # * After each published message, the client receives Basic.Ack from the broker.
27
+ # * If something bad happens inside the broker, it sends Basic.Nack.
28
+ #
29
+ # h2. Gotchas
30
+ # Note that we don't keep track of messages awaiting confirmation.
31
+ # It'd add a huge overhead and it's impossible to come up with one-suits-all solution.
32
+ # If you want to create such module, you'll probably want to redefine Channel#after_publish,
33
+ # so it will put messages into a queue and then handlers for Basic.Ack and Basic.Nack.
34
+ # This is the reason why we pass every argument from Exchange#publish to Channel#after_publish.
35
+ # You should not forget though, that both of these methods can have multi flag!
36
+ #
37
+ # Transactional channel cannot be put into confirm mode and a confirm
38
+ # mode channel cannot be made transactional.
39
+ #
40
+ # If the connection between the publisher and broker drops with outstanding
41
+ # confirms, it does not necessarily mean that the messages were lost, so
42
+ # republishing may result in duplicate messages.
43
+
44
+ # h2. Learn more
45
+ # @see http://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms
46
+ # @see http://www.rabbitmq.com/amqp-0-9-1-quickref.html#class.confirm
47
+ # @see http://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.ack
48
+ module Confirm
49
+ module ChannelMixin
50
+
51
+ # Change publisher index. Publisher index is incremented
52
+ # by 1 after each Basic.Publish starting at 1. This is done
53
+ # on both client and server, hence this acknowledged messages
54
+ # can be matched via its delivery-tag.
55
+ #
56
+ # @api private
57
+ attr_writer :publisher_index
58
+
59
+ # Publisher index is an index of the last message since
60
+ # the confirmations were activated, started with 1. It's
61
+ # incremented by 1 after each Basic.Publish starting at 1.
62
+ # This is done on both client and server, hence this
63
+ # acknowledged messages can be matched via its delivery-tag.
64
+ #
65
+ # @return [Integer] Current publisher index.
66
+ # @api public
67
+ def publisher_index
68
+ @publisher_index ||= 1
69
+ end
70
+
71
+ # Resets publisher index to 0
72
+ #
73
+ # @api plugin
74
+ def reset_publisher_index!
75
+ @publisher_index = 0
76
+ end
77
+
78
+
79
+ # This method is executed after publishing of each message via {Exchage#publish}.
80
+ # Currently it just increments publisher index by 1, so messages
81
+ # can be actually matched.
82
+ #
83
+ # @api plugin
84
+ def after_publish(*args)
85
+ self.publisher_index += 1
86
+ end
87
+
88
+ # Turn on confirmations for this channel and, if given,
89
+ # register callback for Confirm.Select-Ok.
90
+ #
91
+ # @raise [RuntimeError] Occurs when confirmations are already activated.
92
+ # @raise [RuntimeError] Occurs when nowait is true and block is given.
93
+ #
94
+ # @param [Boolean] nowait Whether we expect Confirm.Select-Ok to be returned by the broker or not.
95
+ # @yield [method] Callback which will be executed once we receive Confirm.Select-Ok.
96
+ # @yieldparam [AMQ::Protocol::Confirm::SelectOk] method Protocol method class instance.
97
+ #
98
+ # @return [self] self.
99
+ #
100
+ # @see #confirm
101
+ def confirm_select(nowait = false, &block)
102
+ if nowait && block
103
+ raise "You can't use Confirm.Select with nowait=true and a callback at the same time."
104
+ end
105
+
106
+ @uses_publisher_confirmations = true
107
+ self.redefine_callback(:confirm_select, &block)
108
+ @connection.send_frame(Protocol::Confirm::Select.encode(@id, nowait))
109
+
110
+ self
111
+ end
112
+
113
+ # @return [Boolean]
114
+ def uses_publisher_confirmations?
115
+ @uses_publisher_confirmations
116
+ end # uses_publisher_confirmations?
117
+
118
+
119
+ # Turn on confirmations for this channel and, if given,
120
+ # register callback for basic.ack from the broker.
121
+ #
122
+ # @raise [RuntimeError] Occurs when confirmations are already activated.
123
+ # @raise [RuntimeError] Occurs when nowait is true and block is given.
124
+ # @param [Boolean] nowait Whether we expect Confirm.Select-Ok to be returned by the broker or not.
125
+ #
126
+ # @yield [basick_ack] Callback which will be executed every time we receive Basic.Ack from the broker.
127
+ # @yieldparam [AMQ::Protocol::Basic::Ack] basick_ack Protocol method class instance.
128
+ #
129
+ # @return [self] self.
130
+ def on_ack(nowait = false, &block)
131
+ self.use_publisher_confirmations! unless self.uses_publisher_confirmations?
132
+
133
+ self.define_callback(:ack, &block) if block
134
+
135
+ self
136
+ end
137
+
138
+
139
+ # Register error callback for Basic.Nack. It's called
140
+ # when message(s) is rejected.
141
+ #
142
+ # @return [self] self
143
+ def on_nack(&block)
144
+ self.define_callback(:nack, &block) if block
145
+
146
+ self
147
+ end
148
+
149
+
150
+
151
+
152
+ # Handler for Confirm.Select-Ok. By default, it just
153
+ # executes hook specified via the #confirmations method
154
+ # with a single argument, a protocol method class
155
+ # instance (an instance of AMQ::Protocol::Confirm::SelectOk)
156
+ # and then it deletes the callback, since Confirm.Select
157
+ # is supposed to be sent just once.
158
+ #
159
+ # @api plugin
160
+ def handle_select_ok(method)
161
+ self.exec_callback_once(:confirm_select, method)
162
+ end
163
+
164
+ # Handler for Basic.Ack. By default, it just
165
+ # executes hook specified via the #confirm method
166
+ # with a single argument, a protocol method class
167
+ # instance (an instance of AMQ::Protocol::Basic::Ack).
168
+ #
169
+ # @api plugin
170
+ def handle_basic_ack(method)
171
+ self.exec_callback(:ack, method)
172
+ end
173
+
174
+
175
+ # Handler for Basic.Nack. By default, it just
176
+ # executes hook specified via the #confirm_failed method
177
+ # with a single argument, a protocol method class
178
+ # instance (an instance of AMQ::Protocol::Basic::Nack).
179
+ #
180
+ # @api plugin
181
+ def handle_basic_nack(method)
182
+ self.exec_callback(:nack, method)
183
+ end
184
+
185
+
186
+ def reset_state!
187
+ super
188
+
189
+ @uses_publisher_confirmations = false
190
+ end
191
+
192
+
193
+ def self.included(host)
194
+ host.handle(Protocol::Confirm::SelectOk) do |connection, frame|
195
+ method = frame.decode_payload
196
+ channel = connection.channels[frame.channel]
197
+ channel.handle_select_ok(method)
198
+ end
199
+
200
+ host.handle(Protocol::Basic::Ack) do |connection, frame|
201
+ method = frame.decode_payload
202
+ channel = connection.channels[frame.channel]
203
+ channel.handle_basic_ack(method)
204
+ end
205
+
206
+ host.handle(Protocol::Basic::Nack) do |connection, frame|
207
+ method = frame.decode_payload
208
+ channel = connection.channels[frame.channel]
209
+ channel.handle_basic_nack(method)
210
+ end
211
+ end # self.included(host)
212
+ end # ChannelMixin
213
+
214
+
215
+ module ExchangeMixin
216
+ # Publish message and then run #after_publish on channel belonging
217
+ # to the exchange. This is used for incrementing the publisher index.
218
+ #
219
+ # @api public
220
+ # @see AMQ::Client::Exchange#publish
221
+ # @see AMQ::Client::Extensions::RabbitMQ::Channel#publisher_index
222
+ # @return [self] self
223
+ def publish(*args)
224
+ super(*args)
225
+ @channel.after_publish(*args)
226
+
227
+ self
228
+ end # publish
229
+ end # ExchangeMixin
230
+ end # Confirm
231
+ end # RabbitMQ
232
+ end # Extensions
233
+
234
+
235
+ class Channel
236
+ # use modules, a native Ruby way of extension of existing classes,
237
+ # instead of reckless monkey-patching. MK.
238
+ include Extensions::RabbitMQ::Confirm::ChannelMixin
239
+ end # Channel
240
+
241
+ class Exchange
242
+ # use modules, a native Ruby way of extension of existing classes,
243
+ # instead of reckless monkey-patching. MK.
244
+ include Extensions::RabbitMQ::Confirm::ExchangeMixin
245
+ end # Exchange
246
+ end # Async
247
+ end # Client
248
+ end # AMQ