amq-client 0.5.0
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.
- data/.gitignore +8 -0
- data/.gitmodules +9 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/CONTRIBUTORS +3 -0
- data/Gemfile +27 -0
- data/LICENSE +20 -0
- data/README.textile +61 -0
- data/amq-client.gemspec +34 -0
- data/bin/jenkins.sh +23 -0
- data/bin/set_test_suite_realms_up.sh +24 -0
- data/examples/coolio_adapter/basic_consume.rb +49 -0
- data/examples/coolio_adapter/basic_consume_with_acknowledgements.rb +43 -0
- data/examples/coolio_adapter/basic_consume_with_rejections.rb +43 -0
- data/examples/coolio_adapter/basic_publish.rb +35 -0
- data/examples/coolio_adapter/channel_close.rb +24 -0
- data/examples/coolio_adapter/example_helper.rb +39 -0
- data/examples/coolio_adapter/exchange_declare.rb +28 -0
- data/examples/coolio_adapter/kitchen_sink1.rb +48 -0
- data/examples/coolio_adapter/queue_bind.rb +32 -0
- data/examples/coolio_adapter/queue_purge.rb +32 -0
- data/examples/coolio_adapter/queue_unbind.rb +37 -0
- data/examples/eventmachine_adapter/authentication/plain_password_with_custom_role_credentials.rb +36 -0
- data/examples/eventmachine_adapter/authentication/plain_password_with_default_role_credentials.rb +27 -0
- data/examples/eventmachine_adapter/authentication/plain_password_with_incorrect_credentials.rb +18 -0
- data/examples/eventmachine_adapter/basic_cancel.rb +49 -0
- data/examples/eventmachine_adapter/basic_consume.rb +51 -0
- data/examples/eventmachine_adapter/basic_consume_with_acknowledgements.rb +45 -0
- data/examples/eventmachine_adapter/basic_consume_with_rejections.rb +45 -0
- data/examples/eventmachine_adapter/basic_get.rb +57 -0
- data/examples/eventmachine_adapter/basic_get_with_empty_queue.rb +53 -0
- data/examples/eventmachine_adapter/basic_publish.rb +38 -0
- data/examples/eventmachine_adapter/basic_qos.rb +29 -0
- data/examples/eventmachine_adapter/basic_recover.rb +29 -0
- data/examples/eventmachine_adapter/basic_return.rb +34 -0
- data/examples/eventmachine_adapter/channel_close.rb +24 -0
- data/examples/eventmachine_adapter/channel_flow.rb +36 -0
- data/examples/eventmachine_adapter/channel_level_exception_handling.rb +44 -0
- data/examples/eventmachine_adapter/example_helper.rb +39 -0
- data/examples/eventmachine_adapter/exchange_declare.rb +54 -0
- data/examples/eventmachine_adapter/extensions/rabbitmq/handling_confirm_select_ok.rb +31 -0
- data/examples/eventmachine_adapter/extensions/rabbitmq/publisher_confirmations_with_transient_messages.rb +56 -0
- data/examples/eventmachine_adapter/extensions/rabbitmq/publisher_confirmations_with_unroutable_message.rb +46 -0
- data/examples/eventmachine_adapter/kitchen_sink1.rb +50 -0
- data/examples/eventmachine_adapter/queue_bind.rb +32 -0
- data/examples/eventmachine_adapter/queue_declare.rb +34 -0
- data/examples/eventmachine_adapter/queue_purge.rb +32 -0
- data/examples/eventmachine_adapter/queue_unbind.rb +37 -0
- data/examples/eventmachine_adapter/tx_commit.rb +29 -0
- data/examples/eventmachine_adapter/tx_rollback.rb +29 -0
- data/examples/eventmachine_adapter/tx_select.rb +27 -0
- data/examples/socket_adapter/basics.rb +19 -0
- data/examples/socket_adapter/connection.rb +53 -0
- data/examples/socket_adapter/multiple_connections.rb +17 -0
- data/irb.rb +66 -0
- data/lib/amq/client.rb +15 -0
- data/lib/amq/client/adapter.rb +356 -0
- data/lib/amq/client/adapters/coolio.rb +221 -0
- data/lib/amq/client/adapters/event_machine.rb +228 -0
- data/lib/amq/client/adapters/socket.rb +89 -0
- data/lib/amq/client/channel.rb +338 -0
- data/lib/amq/client/connection.rb +246 -0
- data/lib/amq/client/entity.rb +117 -0
- data/lib/amq/client/exceptions.rb +86 -0
- data/lib/amq/client/exchange.rb +163 -0
- data/lib/amq/client/extensions/rabbitmq.rb +5 -0
- data/lib/amq/client/extensions/rabbitmq/basic.rb +36 -0
- data/lib/amq/client/extensions/rabbitmq/confirm.rb +254 -0
- data/lib/amq/client/framing/io/frame.rb +32 -0
- data/lib/amq/client/framing/string/frame.rb +62 -0
- data/lib/amq/client/logging.rb +56 -0
- data/lib/amq/client/mixins/anonymous_entity.rb +21 -0
- data/lib/amq/client/mixins/status.rb +62 -0
- data/lib/amq/client/protocol/get_response.rb +55 -0
- data/lib/amq/client/queue.rb +450 -0
- data/lib/amq/client/settings.rb +83 -0
- data/lib/amq/client/version.rb +5 -0
- data/spec/benchmarks/adapters.rb +77 -0
- data/spec/client/framing/io_frame_spec.rb +57 -0
- data/spec/client/framing/string_frame_spec.rb +57 -0
- data/spec/client/protocol/get_response_spec.rb +79 -0
- data/spec/integration/coolio/basic_ack_spec.rb +41 -0
- data/spec/integration/coolio/basic_get_spec.rb +73 -0
- data/spec/integration/coolio/basic_return_spec.rb +33 -0
- data/spec/integration/coolio/channel_close_spec.rb +26 -0
- data/spec/integration/coolio/channel_flow_spec.rb +46 -0
- data/spec/integration/coolio/spec_helper.rb +31 -0
- data/spec/integration/coolio/tx_commit_spec.rb +40 -0
- data/spec/integration/coolio/tx_rollback_spec.rb +44 -0
- data/spec/integration/eventmachine/basic_ack_spec.rb +40 -0
- data/spec/integration/eventmachine/basic_get_spec.rb +73 -0
- data/spec/integration/eventmachine/basic_return_spec.rb +35 -0
- data/spec/integration/eventmachine/channel_close_spec.rb +26 -0
- data/spec/integration/eventmachine/channel_flow_spec.rb +32 -0
- data/spec/integration/eventmachine/spec_helper.rb +22 -0
- data/spec/integration/eventmachine/tx_commit_spec.rb +47 -0
- data/spec/integration/eventmachine/tx_rollback_spec.rb +35 -0
- data/spec/regression/bad_frame_slicing_in_adapters_spec.rb +59 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/unit/client/adapter_spec.rb +49 -0
- data/spec/unit/client/entity_spec.rb +49 -0
- data/spec/unit/client/logging_spec.rb +60 -0
- data/spec/unit/client/mixins/status_spec.rb +72 -0
- data/spec/unit/client/settings_spec.rb +27 -0
- data/spec/unit/client_spec.rb +11 -0
- data/tasks.rb +11 -0
- metadata +202 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require "amq/client/mixins/status"
|
|
4
|
+
|
|
5
|
+
module AMQ
|
|
6
|
+
module Client
|
|
7
|
+
# AMQ entities, as implemented by AMQ::Client, have callbacks and can run them
|
|
8
|
+
# when necessary.
|
|
9
|
+
#
|
|
10
|
+
# @note Exchanges and queues implementation is based on this class.
|
|
11
|
+
#
|
|
12
|
+
# @abstract
|
|
13
|
+
class Entity
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Behaviors
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
include StatusMixin
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# API
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
# @return [Array<#call>]
|
|
26
|
+
attr_reader :callbacks
|
|
27
|
+
|
|
28
|
+
@@handlers ||= Hash.new
|
|
29
|
+
|
|
30
|
+
def self.handle(klass, &block)
|
|
31
|
+
@@handlers[klass] = block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.handlers
|
|
35
|
+
@@handlers
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def initialize(client)
|
|
42
|
+
@client = client
|
|
43
|
+
# Be careful with default values for #ruby hashes: h = Hash.new(Array.new); h[:key] ||= 1
|
|
44
|
+
# won't assign anything to :key. MK.
|
|
45
|
+
@callbacks = Hash.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def redefine_callback(event, callable = nil, &block)
|
|
50
|
+
f = (callable || block)
|
|
51
|
+
# yes, re-assign!
|
|
52
|
+
@callbacks[event] = [f]
|
|
53
|
+
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def define_callback(event, callable = nil, &block)
|
|
58
|
+
f = (callable || block)
|
|
59
|
+
|
|
60
|
+
@callbacks[event] ||= []
|
|
61
|
+
@callbacks[event] << f if f
|
|
62
|
+
|
|
63
|
+
self
|
|
64
|
+
end # define_callback(event, &block)
|
|
65
|
+
alias append_callback define_callback
|
|
66
|
+
|
|
67
|
+
def prepend_callback(event, &block)
|
|
68
|
+
@callbacks[event] ||= []
|
|
69
|
+
@callbacks[event].unshift(block)
|
|
70
|
+
|
|
71
|
+
self
|
|
72
|
+
end # prepend_callback(event, &block)
|
|
73
|
+
|
|
74
|
+
def clear_callbacks(event)
|
|
75
|
+
@callbacks[event].clear if @callbacks[event]
|
|
76
|
+
end # clear_callbacks(event)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def exec_callback(name, *args, &block)
|
|
80
|
+
callbacks = Array(self.callbacks[name])
|
|
81
|
+
callbacks.map { |c| c.call(*args, &block) } if callbacks.any?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def exec_callback_once(name, *args, &block)
|
|
85
|
+
callbacks = Array(self.callbacks.delete(name))
|
|
86
|
+
callbacks.map { |c| c.call(*args, &block) } if callbacks.any?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def exec_callback_yielding_self(name, *args, &block)
|
|
90
|
+
callbacks = Array(self.callbacks[name])
|
|
91
|
+
callbacks.map { |c| c.call(self, *args, &block) } if callbacks.any?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def exec_callback_once_yielding_self(name, *args, &block)
|
|
95
|
+
callbacks = Array(self.callbacks.delete(name))
|
|
96
|
+
callbacks.map { |c| c.call(self, *args, &block) } if callbacks.any?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def error(exception)
|
|
102
|
+
if client.sync? # DO NOT DO THIS, just add a default errback to do exactly this, so if someone wants to use begin/rescue, he'll just ignore the errbacks.
|
|
103
|
+
# Synchronous error handling.
|
|
104
|
+
# Just use begin/rescue in the main loop.
|
|
105
|
+
raise exception
|
|
106
|
+
else
|
|
107
|
+
# Asynchronous error handling.
|
|
108
|
+
# Set callback for given class (Queue for example)
|
|
109
|
+
# or for the Connection class (or instance, of course).
|
|
110
|
+
callbacks = [self.callbacks[:close], self.client.connection.callbacks[:close]].flatten.compact
|
|
111
|
+
|
|
112
|
+
callbacks.map { |c| c.call(exception) } if callbacks.any?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module AMQ
|
|
4
|
+
module Client
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# Adapters
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
# Base exception class for data consistency and framing errors.
|
|
11
|
+
class InconsistentDataError < StandardError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Raised by adapters when frame does not end with {final octet AMQ::Protocol::Frame::FINAL_OCTET}.
|
|
15
|
+
# This suggest that there is a bug in adapter or AMQ broker implementation.
|
|
16
|
+
#
|
|
17
|
+
# @see http://bit.ly/hw2ELX AMQP 0.9.1 specification (Section 2.3)
|
|
18
|
+
class NoFinalOctetError < InconsistentDataError
|
|
19
|
+
def initialize
|
|
20
|
+
super("Frame doesn't end with #{AMQ::Protocol::Frame::FINAL_OCTET} as it must, which means the size is miscalculated.")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Raised by adapters when actual frame payload size in bytes is not equal
|
|
25
|
+
# to the size specified in that frame's header.
|
|
26
|
+
# This suggest that there is a bug in adapter or AMQ broker implementation.
|
|
27
|
+
#
|
|
28
|
+
# @see http://bit.ly/hw2ELX AMQP 0.9.1 specification (Section 2.3)
|
|
29
|
+
class BadLengthError < InconsistentDataError
|
|
30
|
+
def initialize(expected_length, actual_length)
|
|
31
|
+
super("Frame payload should be #{expected_length} long, but it's #{actual_length} long.")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# Client
|
|
37
|
+
#
|
|
38
|
+
|
|
39
|
+
class MissingInterfaceMethodError < NotImplementedError
|
|
40
|
+
def initialize(method_name)
|
|
41
|
+
super("Method #{method_name} is supposed to be overriden by adapter")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class MissingHandlerError < StandardError
|
|
46
|
+
def initialize(frame)
|
|
47
|
+
super("No callback registered for #{frame.method_class}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class ConnectionClosedError < StandardError
|
|
52
|
+
def initialize(frame)
|
|
53
|
+
if frame.respond_to?(:method_class)
|
|
54
|
+
super("Trying to send frame through a closed connection. Frame is #{frame.inspect}")
|
|
55
|
+
else
|
|
56
|
+
super("Trying to send frame through a closed connection. Frame is #{frame.inspect}, method class is #{frame.method_class}")
|
|
57
|
+
end
|
|
58
|
+
end # initialize
|
|
59
|
+
end # class ConnectionClosedError
|
|
60
|
+
|
|
61
|
+
module Logging
|
|
62
|
+
# Raised when logger object passed to {AMQ::Client::Adapter.logger=} does not
|
|
63
|
+
# provide API it supposed to provide.
|
|
64
|
+
#
|
|
65
|
+
# @see AMQ::Client::Adapter.logger=
|
|
66
|
+
class IncompatibleLoggerError < StandardError
|
|
67
|
+
def initialize(required_methods)
|
|
68
|
+
super("Logger has to respond to the following methods: #{required_methods.inspect}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end # Logging
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PossibleAuthenticationFailureError < StandardError
|
|
75
|
+
|
|
76
|
+
#
|
|
77
|
+
# API
|
|
78
|
+
#
|
|
79
|
+
|
|
80
|
+
def initialize(settings)
|
|
81
|
+
super("AMQP broker closed TCP connection before authentication succeeded: this usually means authentication failure due to misconfiguration. Settings are #{settings.inspect}")
|
|
82
|
+
end # initialize(settings)
|
|
83
|
+
end # PossibleAuthenticationFailureError
|
|
84
|
+
|
|
85
|
+
end # Client
|
|
86
|
+
end # AMQ
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require "amq/client/entity"
|
|
4
|
+
require "amq/client/mixins/anonymous_entity"
|
|
5
|
+
|
|
6
|
+
module AMQ
|
|
7
|
+
module Client
|
|
8
|
+
class Exchange < Entity
|
|
9
|
+
include AnonymousEntityMixin
|
|
10
|
+
|
|
11
|
+
TYPES = [:fanout, :direct, :topic, :headers].freeze
|
|
12
|
+
|
|
13
|
+
class IncompatibleExchangeTypeError < StandardError
|
|
14
|
+
def initialize(types, given)
|
|
15
|
+
super("Exchange types are #{TYPES.inspect}, #{given.inspect} given.")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# API
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
# Channel this exchange belongs to.
|
|
25
|
+
attr_reader :channel
|
|
26
|
+
|
|
27
|
+
# Exchange name. May be server-generated or assigned directly.
|
|
28
|
+
attr_reader :name
|
|
29
|
+
|
|
30
|
+
# @return [Symbol] One of :direct, :fanout, :topic, :headers
|
|
31
|
+
attr_reader :type
|
|
32
|
+
|
|
33
|
+
def initialize(client, channel, name, type = :fanout)
|
|
34
|
+
unless TYPES.include?(type.to_sym)
|
|
35
|
+
raise IncompatibleExchangeTypeError.new(TYPES, type)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@client = client
|
|
39
|
+
@channel = channel
|
|
40
|
+
@name = name
|
|
41
|
+
@type = type
|
|
42
|
+
|
|
43
|
+
# register pre-declared exchanges
|
|
44
|
+
if @name == AMQ::Protocol::EMPTY_STRING || @name =~ /^amq\.(fanout|topic)/
|
|
45
|
+
@channel.register_exchange(self)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
super(client)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fanout?
|
|
53
|
+
@type == :fanout
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def direct?
|
|
57
|
+
@type == :direct
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def topic?
|
|
61
|
+
@type == :topic
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def declare(passive = false, durable = false, exclusive = false, auto_delete = false, nowait = false, arguments = nil, &block)
|
|
67
|
+
@client.send(Protocol::Exchange::Declare.encode(@channel.id, @name, @type.to_s, passive, durable, auto_delete, false, nowait, arguments))
|
|
68
|
+
|
|
69
|
+
unless nowait
|
|
70
|
+
self.define_callback(:declare, &block)
|
|
71
|
+
@channel.exchanges_awaiting_declare_ok.push(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if @client.sync?
|
|
76
|
+
@client.read_until_receives(Protocol::Exchange::DeclareOk) unless nowait
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def delete(if_unused = false, nowait = false, &block)
|
|
84
|
+
@client.send(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, &block)
|
|
98
|
+
headers = { :priority => 0, :delivery_mode => 2, :content_type => "application/octet-stream" }.merge(user_headers)
|
|
99
|
+
@client.send_frameset(Protocol::Basic::Publish.encode(@channel.id, payload, headers, @name, routing_key, mandatory, immediate, (frame_size || @client.connection.frame_max)))
|
|
100
|
+
|
|
101
|
+
block.call if block
|
|
102
|
+
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def on_return(&block)
|
|
108
|
+
self.redefine_callback(:return, &block)
|
|
109
|
+
|
|
110
|
+
self
|
|
111
|
+
end # on_return(&block)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def handle_declare_ok(method)
|
|
117
|
+
@name = method.exchange if self.anonymous?
|
|
118
|
+
@channel.register_exchange(self)
|
|
119
|
+
|
|
120
|
+
self.exec_callback_once_yielding_self(:declare, method)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def handle_delete_ok(method)
|
|
124
|
+
self.exec_callback_once(:delete, method)
|
|
125
|
+
end # handle_delete_ok(method)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# === Handlers ===
|
|
130
|
+
# Get the first exchange which didn't receive Exchange.Declare-Ok yet and run its declare callback.
|
|
131
|
+
# The cache includes only exchanges with {nowait: false}.
|
|
132
|
+
self.handle(Protocol::Exchange::DeclareOk) do |client, frame|
|
|
133
|
+
method = frame.decode_payload
|
|
134
|
+
|
|
135
|
+
# We should have cache API, so it'll be easy to change caching behaviour easily.
|
|
136
|
+
# So in the amq-client we don't want to cache more than just the last instance per each channel,
|
|
137
|
+
# whereas more opinionated clients might want to have every single instance in the cache,
|
|
138
|
+
# so they can iterate over it etc.
|
|
139
|
+
channel = client.connection.channels[frame.channel]
|
|
140
|
+
exchange = channel.exchanges_awaiting_declare_ok.shift
|
|
141
|
+
|
|
142
|
+
exchange.handle_declare_ok(method)
|
|
143
|
+
end # handle
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
self.handle(Protocol::Exchange::DeleteOk) do |client, frame|
|
|
147
|
+
channel = client.connection.channels[frame.channel]
|
|
148
|
+
exchange = channel.exchanges_awaiting_delete_ok.shift
|
|
149
|
+
exchange.handle_delete_ok(frame.decode_payload)
|
|
150
|
+
end # handle
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
self.handle(Protocol::Basic::Return) do |client, frame|
|
|
154
|
+
channel = client.connection.channels[frame.channel]
|
|
155
|
+
method = frame.decode_payload
|
|
156
|
+
exchange = channel.find_exchange(method.exchange)
|
|
157
|
+
|
|
158
|
+
exchange.exec_callback(:return, method)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
end # Exchange
|
|
162
|
+
end # Client
|
|
163
|
+
end # AMQ
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require "amq/client/channel"
|
|
4
|
+
|
|
5
|
+
# Basic.Nack
|
|
6
|
+
module AMQ
|
|
7
|
+
module Client
|
|
8
|
+
module Extensions
|
|
9
|
+
module RabbitMQ
|
|
10
|
+
module Basic
|
|
11
|
+
module ChannelMixin
|
|
12
|
+
|
|
13
|
+
# Overrides {AMQ::Client::Channel#reject} behavior to use basic.nack.
|
|
14
|
+
#
|
|
15
|
+
# @api public
|
|
16
|
+
# @see http://www.rabbitmq.com/amqp-0-9-1-quickref.html#basic.nack
|
|
17
|
+
def reject(delivery_tag, requeue = true, multi = false)
|
|
18
|
+
if multi
|
|
19
|
+
@client.send(Protocol::Basic::Nack.encode(self.id, delivery_tag, multi, requeue))
|
|
20
|
+
else
|
|
21
|
+
super(delivery_tag, requeue)
|
|
22
|
+
end
|
|
23
|
+
end # reject
|
|
24
|
+
|
|
25
|
+
end # ChannelMixin
|
|
26
|
+
end # Basic
|
|
27
|
+
end # RabbitMQ
|
|
28
|
+
end # Extensions
|
|
29
|
+
|
|
30
|
+
class Channel
|
|
31
|
+
# use modules, a native Ruby way of extension of existing classes,
|
|
32
|
+
# instead of reckless monkey-patching. MK.
|
|
33
|
+
include Extensions::RabbitMQ::Basic::ChannelMixin
|
|
34
|
+
end
|
|
35
|
+
end # Client
|
|
36
|
+
end # AMQ
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
# === Purpose === #
|
|
4
|
+
# In case that the broker crashes, some messages can get lost.
|
|
5
|
+
# Thanks to this extension, broker sends Basic.Ack when the message
|
|
6
|
+
# is processed by the broker. In case of persistent messages, it must
|
|
7
|
+
# be written to disk or ack'd on all the queues it was delivered to.
|
|
8
|
+
# However it doesn't have to be necessarily 1:1, because the broker
|
|
9
|
+
# can send Basic.Ack with multi flag to acknowledge multiple messages.
|
|
10
|
+
#
|
|
11
|
+
# So it provides clients a lightweight way of keeping track of which
|
|
12
|
+
# messages have been processed by the broker and which would need
|
|
13
|
+
# re-publishing in case of broker shutdown or network failure.
|
|
14
|
+
#
|
|
15
|
+
# Transactions are solving the same problem, but they are very slow:
|
|
16
|
+
# confirmations are more than 100 times faster.
|
|
17
|
+
#
|
|
18
|
+
# === Workflow === #
|
|
19
|
+
# * Client asks broker to confirm messages on given channel (Confirm.Select).
|
|
20
|
+
# * Broker sends back Confirm.Select-Ok, unless we sent Confirm.Select with nowait=true.
|
|
21
|
+
# * After each published message, the client receives Basic.Ack from the broker.
|
|
22
|
+
# * If something bad happens inside the broker, it sends Basic.Nack.
|
|
23
|
+
#
|
|
24
|
+
# === Gotchas === #
|
|
25
|
+
# Note that we don't keep track of messages awaiting confirmation.
|
|
26
|
+
# It'd add a huge overhead and it's impossible to come up with one-suits-all solution.
|
|
27
|
+
# If you want to create such module, you'll probably want to redefine Channel#after_publish,
|
|
28
|
+
# so it will put messages into a queue and then handlers for Basic.Ack and Basic.Nack.
|
|
29
|
+
# This is the reason why we pass every argument from Exchange#publish to Channel#after_publish.
|
|
30
|
+
# You should not forget though, that both of these methods can have multi flag!
|
|
31
|
+
#
|
|
32
|
+
# Transactional channel cannot be put into confirm mode and a confirm
|
|
33
|
+
# mode channel cannot be made transactional.
|
|
34
|
+
#
|
|
35
|
+
# If the connection between the publisher and broker drops with outstanding
|
|
36
|
+
# confirms, it does not necessarily mean that the messages were lost, so
|
|
37
|
+
# republishing may result in duplicate messages.
|
|
38
|
+
|
|
39
|
+
# === Links === #
|
|
40
|
+
# http://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms
|
|
41
|
+
# http://www.rabbitmq.com/amqp-0-9-1-quickref.html#class.confirm
|
|
42
|
+
# http://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.ack
|
|
43
|
+
|
|
44
|
+
puts "in confirm.rb"
|
|
45
|
+
|
|
46
|
+
module AMQ
|
|
47
|
+
module Client
|
|
48
|
+
module Extensions
|
|
49
|
+
module RabbitMQ
|
|
50
|
+
module Confirm
|
|
51
|
+
module ChannelMixin
|
|
52
|
+
# Boolean value expressing whether confirmations are
|
|
53
|
+
# on or off, aka whether Confirm.Select was sent or not.
|
|
54
|
+
#
|
|
55
|
+
# @api public
|
|
56
|
+
# @return [Boolean] Whether confirmations are on or off.
|
|
57
|
+
attr_reader :confirmations
|
|
58
|
+
|
|
59
|
+
# Change publisher index. Publisher index is incremented
|
|
60
|
+
# by 1 after each Basic.Publish starting at 1. This is done
|
|
61
|
+
# on both client and server, hence this acknowledged messages
|
|
62
|
+
# can be matched via its delivery-tag.
|
|
63
|
+
#
|
|
64
|
+
# @api private
|
|
65
|
+
attr_writer :publisher_index
|
|
66
|
+
|
|
67
|
+
# Publisher index is an index of the last message since
|
|
68
|
+
# the confirmations were activated, started with 1. It's
|
|
69
|
+
# incremented by 1 after each Basic.Publish starting at 1.
|
|
70
|
+
# This is done on both client and server, hence this
|
|
71
|
+
# acknowledged messages can be matched via its delivery-tag.
|
|
72
|
+
#
|
|
73
|
+
# @return [Integer] Current publisher index.
|
|
74
|
+
# @api public
|
|
75
|
+
def publisher_index
|
|
76
|
+
@publisher_index ||= 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Resets publisher index to 0
|
|
80
|
+
#
|
|
81
|
+
# @api plugin
|
|
82
|
+
def reset_publisher_index!
|
|
83
|
+
@publisher_index = 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# This method is executed after publishing of each message via {Exchage#publish}.
|
|
88
|
+
# Currently it just increments publisher index by 1, so messages
|
|
89
|
+
# can be actually matched.
|
|
90
|
+
#
|
|
91
|
+
# @api plugin
|
|
92
|
+
def after_publish(*args)
|
|
93
|
+
self.publisher_index += 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Turn on confirmations for this channel and, if given,
|
|
97
|
+
# register callback for Confirm.Select-Ok.
|
|
98
|
+
#
|
|
99
|
+
# @raise [RuntimeError] Occurs when confirmations are already activated.
|
|
100
|
+
# @raise [RuntimeError] Occurs when nowait is true and block is given.
|
|
101
|
+
#
|
|
102
|
+
# @param [Boolean] nowait Whether we expect Confirm.Select-Ok to be returned by the broker or not.
|
|
103
|
+
# @yield [method] Callback which will be executed once we receive Confirm.Select-Ok.
|
|
104
|
+
# @yieldparam [AMQ::Protocol::Confirm::SelectOk] method Protocol method class instance.
|
|
105
|
+
#
|
|
106
|
+
# @return [self] self.
|
|
107
|
+
#
|
|
108
|
+
# @see #confirm
|
|
109
|
+
def confirmations(nowait = false, &block)
|
|
110
|
+
if @confirmations
|
|
111
|
+
raise "Confirmations are already activated!"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if nowait && block
|
|
115
|
+
raise "You can't use Confirm.Select with nowait=true and a callback at the same time."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@confirmations = true
|
|
119
|
+
self.redefine_callback(:confirmations, &block)
|
|
120
|
+
@client.send(Protocol::Confirm::Select.encode(@id, nowait))
|
|
121
|
+
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Turn on confirmations for this channel and, if given,
|
|
127
|
+
# register callback for basic.ack from the broker.
|
|
128
|
+
#
|
|
129
|
+
# @raise [RuntimeError] Occurs when confirmations are already activated.
|
|
130
|
+
# @raise [RuntimeError] Occurs when nowait is true and block is given.
|
|
131
|
+
# @param [Boolean] nowait Whether we expect Confirm.Select-Ok to be returned by the broker or not.
|
|
132
|
+
#
|
|
133
|
+
# @yield [basick_ack] Callback which will be executed every time we receive Basic.Ack from the broker.
|
|
134
|
+
# @yieldparam [AMQ::Protocol::Basic::Ack] basick_ack Protocol method class instance.
|
|
135
|
+
#
|
|
136
|
+
# @return [self] self.
|
|
137
|
+
def confirm(nowait = false, &block)
|
|
138
|
+
self.confirmations unless @confirmations
|
|
139
|
+
|
|
140
|
+
self.define_callback(:ack, &block) if block
|
|
141
|
+
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Register error callback for Basic.Nack. It's called
|
|
147
|
+
# when the broker reject given message(s).
|
|
148
|
+
#
|
|
149
|
+
# @return [self] self
|
|
150
|
+
def confirm_failed(&block)
|
|
151
|
+
self.define_callback(:nack, &block) if block
|
|
152
|
+
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Handler for Confirm.Select-Ok. By default, it just
|
|
160
|
+
# executes hook specified via the #confirmations method
|
|
161
|
+
# with a single argument, a protocol method class
|
|
162
|
+
# instance (an instance of AMQ::Protocol::Confirm::SelectOk)
|
|
163
|
+
# and then it deletes the callback, since Confirm.Select
|
|
164
|
+
# is supposed to be sent just once.
|
|
165
|
+
#
|
|
166
|
+
# @api plugin
|
|
167
|
+
def handle_select_ok(method)
|
|
168
|
+
self.exec_callback_once(:confirmations, method)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Handler for Basic.Ack. By default, it just
|
|
172
|
+
# executes hook specified via the #confirm method
|
|
173
|
+
# with a single argument, a protocol method class
|
|
174
|
+
# instance (an instance of AMQ::Protocol::Basic::Ack).
|
|
175
|
+
#
|
|
176
|
+
# @api plugin
|
|
177
|
+
def handle_basic_ack(method)
|
|
178
|
+
self.exec_callback(:ack, method)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Handler for Basic.Nack. By default, it just
|
|
183
|
+
# executes hook specified via the #confirm_failed method
|
|
184
|
+
# with a single argument, a protocol method class
|
|
185
|
+
# instance (an instance of AMQ::Protocol::Basic::Nack).
|
|
186
|
+
#
|
|
187
|
+
# @api plugin
|
|
188
|
+
def handle_basic_nack(method)
|
|
189
|
+
self.exec_callback(:nack, method)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def reset_state!
|
|
194
|
+
super
|
|
195
|
+
|
|
196
|
+
@confirmations = false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def self.included(host)
|
|
201
|
+
host.handle(Protocol::Confirm::SelectOk) do |client, frame|
|
|
202
|
+
method = frame.decode_payload
|
|
203
|
+
channel = client.connection.channels[frame.channel]
|
|
204
|
+
channel.handle_select_ok(method)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
host.handle(Protocol::Basic::Ack) do |client, frame|
|
|
208
|
+
method = frame.decode_payload
|
|
209
|
+
channel = client.connection.channels[frame.channel]
|
|
210
|
+
channel.handle_basic_ack(method)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
host.handle(Protocol::Basic::Nack) do |client, frame|
|
|
214
|
+
method = frame.decode_payload
|
|
215
|
+
channel = client.connection.channels[frame.channel]
|
|
216
|
+
channel.handle_basic_nack(method)
|
|
217
|
+
end
|
|
218
|
+
end # self.included(host)
|
|
219
|
+
end # ChannelMixin
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
module ExchangeMixin
|
|
223
|
+
# Publish message and then run #after_publish on channel belonging
|
|
224
|
+
# to the exchange. This is used for incrementing the publisher index.
|
|
225
|
+
#
|
|
226
|
+
# @api public
|
|
227
|
+
# @see AMQ::Client::Exchange#publish
|
|
228
|
+
# @see AMQ::Client::Extensions::RabbitMQ::Channel#publisher_index
|
|
229
|
+
# @return [self] self
|
|
230
|
+
def publish(*args)
|
|
231
|
+
super(*args)
|
|
232
|
+
@channel.after_publish(*args)
|
|
233
|
+
|
|
234
|
+
self
|
|
235
|
+
end # publish
|
|
236
|
+
end # ExchangeMixin
|
|
237
|
+
end # Confirm
|
|
238
|
+
end # RabbitMQ
|
|
239
|
+
end # Extensions
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Channel
|
|
243
|
+
# use modules, a native Ruby way of extension of existing classes,
|
|
244
|
+
# instead of reckless monkey-patching. MK.
|
|
245
|
+
include Extensions::RabbitMQ::Confirm::ChannelMixin
|
|
246
|
+
end # Channel
|
|
247
|
+
|
|
248
|
+
class Exchange
|
|
249
|
+
# use modules, a native Ruby way of extension of existing classes,
|
|
250
|
+
# instead of reckless monkey-patching. MK.
|
|
251
|
+
include Extensions::RabbitMQ::Confirm::ExchangeMixin
|
|
252
|
+
end # Exchange
|
|
253
|
+
end # Client
|
|
254
|
+
end # AMQ
|