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