Fabio Pitino
Sr Software Engineer
fabio_pitino@symantec.com
(with Ruby examples)
I asked Wikipedia...
A message broker is an architectural pattern for message validation, transformation, and routing. It mediates communication among applications, minimising the mutual awareness that applications should have of each other in order to be able to exchange messages, effectively implementing decoupling.
# producer.rb
require 'bunny'
connection = Bunny.new
connection.start
channel = connection.create_channel
exchange = channel.default_exchange
queue = channel.queue('tasks')
10.times do |n|
exchange.publish("task-#{n}", routing_key: queue.name)
end
connection.close
# consumer.rb
require 'bunny'
connection = Bunny.new
connection.start
channel = connection.create_channel
queue = channel.queue('tasks')
begin
queue.subscribe(block: true) do |_delivery_info, _properties, payload|
puts "received #{payload}"
# do some work with it...
end
rescue => error
puts error.inspect
connection.close
end
# worker.rb
# setup omitted...
queue = channel.queue('tasks')
# using "block: true" is like using "channel.prefetch(1)"
queue.subscribe(manual_ack: true, block: true) do |delivery_info, _, payload|
puts "received #{payload}"
# do some work with it...
channel.ack(delivery_info.delivery_tag)
end
# worker.rb
# setup omitted...
channel.prefetch(1)
queue = channel.queue('tasks')
queue.subscribe(manual_ack: true, block: true) do |delivery_info, _, payload|
puts "received #{payload}"
# do some work with it...
channel.ack(delivery_info.delivery_tag)
end
# worker.rb
POOL_SIZE = 10
# setup omitted...
channel.prefetch(POOL_SIZE)
queue = channel.queue('tasks')
queue.subscribe(manual_ack: true, block: false) do |delivery_info, _, payload|
puts "received #{payload}"
# do some work with it...
channel.ack(delivery_info.delivery_tag)
end
# queue does not survive broker reboot
channel.queue('tasks')
# queue is persisted after broker reboot but not the messages in it
channel.queue('tasks', durable: true)
# queue and messages persisted after broker reboot
queue = channel.queue('tasks', durable: true)
queue.publish(message, persistent: true)
slow
fast
# so far we have been using this:
channel.queue('tasks').publish(message)
# ...which is equivalent to this:
channel.default_exchange.publish(message, routing_key: 'tasks')
# publisher.rb
exchange = channel.fanout('logs')
exchange.publish(message)
# subscriber.rb
# declare a temporary queue owned by the subscriber
# with auto-generated name: amq.gen-JzTY20BRgKO-HjmUJj0wLg
queue = channel.queue('', exclusive: true)
# bind queue to the specific fanout exchange
queue.bind('logs')
# at this point messages published to the "logs" exchange
# will be copied to each queue bound to it
queue.subscribe(block: true) do |_delivery_info, _, payload|
puts "received: #{payload}"
end
# publisher.rb
exchange = channel.direct('logs')
exchange.publish(message, routing_key: severity)
# consumer.rb [info] [warn] [error]
exchange = channel.direct('logs')
queue = channel.queue('', exclusive: true)
ARGV.each do |severity|
queue.bind(exchange, routing_key: severity
end
queue.subscribe(block: true) do |_delivery_info, _, payload|
puts "received #{payload}"
end
Imagine that for our logging system we have different consumers interested in different severity of messages
Direct exchange cannot route message based on multiple criteria.
Enter the Topic Exchange.
Messages must contain a routing_key that is a list of words delimited by "dots":
Routing key can contain special characters:
A weather forecast server streams real-time weather information to various backend services around the world.
Format of the routing key: <continent>.<country>.<city>
consumers can use various forms of routing keys to get data:
Well! turns out that...
# publisher.rb
exchange = channel.topic('logs')
exchange.publish(message, routing_key: "#{source}.#{severity}")
# consumer.rb [key]
# e.g: consumer "rails.info"
# e.g: consumer "syslog.*" "kern.error"
# e.g: consumer "#"
exchange = channel.topic('logs')
queue = channel.queue('', exclusive: true)
ARGV.each do |key|
queue.bind(exchange, routing_key: key
end
queue.subscribe(block: true) do |delivery_info, _, payload|
puts "received #{delivery_info.routing_key}: #{payload}"
end
Let's extend the logging system with a more flexible approach
# server.rb
exchange = channel.default_exchange
queue = channel.queue('fibonacci_server')
queue.subscribe(block: true) do |_delivery, properties, payload|
result = fibonacci(payload.to_i)
exchange.publish(
result.to_s, routing_key: properties.reply_to, correlation_id: properties.correlation_id
)
end
# client.rb
call_id = SecureRandom.hex(16)
reply_queue = channel.queue('', exclusive: true)
reply_queue.subscribe do |_delivery, properties, payload|
if properties[:correlation_id] == call_id
puts "response: #{payload}"
end
end
exchange = channel.default_exchange
exchange.publish(
'30', routing_key: 'fibonacci_server', correlation_id: call_id, reply_to: reply_queue.name
)
Advanced use case