brown 2.2.2 → 2.2.2.25.g85ddf08
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/Dockerfile +10 -0
- data/README.md +137 -99
- data/bin/brown +19 -82
- data/brown.gemspec +3 -3
- data/lib/brown.rb +2 -0
- data/lib/brown/agent.rb +41 -411
- data/lib/brown/agent/amqp.rb +20 -0
- data/lib/brown/agent/amqp/class_methods.rb +147 -0
- data/lib/brown/agent/amqp/initializer.rb +144 -0
- data/lib/brown/agent/amqp_message.rb +26 -4
- data/lib/brown/agent/amqp_publisher.rb +68 -27
- data/lib/brown/agent/class_methods.rb +166 -0
- data/lib/brown/agent/stimulus.rb +84 -77
- data/lib/brown/agent/stimulus/metrics.rb +17 -0
- data/lib/brown/rspec.rb +16 -5
- data/lib/brown/test.rb +16 -28
- metadata +18 -20
@@ -0,0 +1,20 @@
|
|
1
|
+
# AMQP support for Brown agents.
|
2
|
+
#
|
3
|
+
# Including this module in your agent provides support for publishing and
|
4
|
+
# receiving AMQP messages from an AMQP broker, such as RabbitMQ.
|
5
|
+
#
|
6
|
+
# The methods in this module itself aren't particularly interesting to end-users;
|
7
|
+
# the good stuff is in {Brown::Agent::AMQP::ClassMethods}.
|
8
|
+
#
|
9
|
+
module Brown::Agent::AMQP
|
10
|
+
private
|
11
|
+
|
12
|
+
def self.included(mod)
|
13
|
+
mod.url :AMQP_URL
|
14
|
+
mod.prepend(Brown::Agent::AMQP::Initializer)
|
15
|
+
mod.extend(Brown::Agent::AMQP::ClassMethods)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require_relative "amqp/initializer"
|
20
|
+
require_relative "amqp/class_methods"
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
# Class-level AMQP support for Brown agents.
|
5
|
+
#
|
6
|
+
# These methods are intended to be applied to a `Brown::Agent` subclass, so you
|
7
|
+
# can use them to define new AMQP listeners and publishers in your agent classes.
|
8
|
+
# You should not attempt to extend your classes directly with this module; the
|
9
|
+
# {Brown::Agent::AMQP} module should handle that for you automatically.
|
10
|
+
#
|
11
|
+
module Brown::Agent::AMQP::ClassMethods
|
12
|
+
attr_reader :amqp_publishers, :amqp_listeners
|
13
|
+
|
14
|
+
# Declare an AMQP publisher, and create an AMQP exchange to publish to.
|
15
|
+
#
|
16
|
+
# On the assumption that you already know [how exchanges
|
17
|
+
# work](http://www.rabbitmq.com/tutorials/amqp-concepts.html), lets
|
18
|
+
# just dive right in.
|
19
|
+
#
|
20
|
+
# This method creates an accessor method on your agent class, named after the
|
21
|
+
# symbol you pass in as `name`, which returns an instance of
|
22
|
+
# `Brown::Agent::AMQPPublisher`. This object, in turn, defines an
|
23
|
+
# AMQP exchange when it is created, and has a
|
24
|
+
# `publish` method on it (see {Brown::Agent::AMQPPublisher#publish}) which
|
25
|
+
# sends arbitrary messages to the exchange.
|
26
|
+
#
|
27
|
+
# @param name [Symbol] the name of the accessor method to call when
|
28
|
+
# you want to reference this publisher in your agent code.
|
29
|
+
#
|
30
|
+
# @param publisher_opts [Hash] options which are passed to
|
31
|
+
# {Brown::Agent::AMQPPublisher#initialize}.
|
32
|
+
#
|
33
|
+
# This method is a thin shim around {Brown::Agent::AMQPPublisher#initialize};
|
34
|
+
# you should read that method's documentation for details of what
|
35
|
+
# constitutes valid `publisher_opts`, and also what exceptions can be
|
36
|
+
# raised.
|
37
|
+
#
|
38
|
+
# @see Brown::Agent::AMQPPublisher#initialize
|
39
|
+
# @see Brown::Agent::AMQPPublisher#publish
|
40
|
+
#
|
41
|
+
def amqp_publisher(name, publisher_opts = {})
|
42
|
+
@amqp_publishers ||= []
|
43
|
+
@amqp_publishers << { name: name, opts: publisher_opts }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Declare a stimulus to listen for messages from an AMQP broker.
|
47
|
+
#
|
48
|
+
# When the agent is started, we will setup a queue, bound to the exchange
|
49
|
+
# specified by the `exchange_name` argument, and then proceed to hoover up
|
50
|
+
# all the messages we can.
|
51
|
+
#
|
52
|
+
# The name of the queue that is created by default is derived from the
|
53
|
+
# agent class name and the exchange name being bound to. This allows
|
54
|
+
# for multiple instances of the same agent, running in separate
|
55
|
+
# processes or machines, to share the same queue of messages to
|
56
|
+
# process, for throughput or redundancy reasons.
|
57
|
+
#
|
58
|
+
# @param exchange_name [#to_s, Array<#to_s>] the name of the exchange
|
59
|
+
# to bind to. You can also specify an array of exchange names, to
|
60
|
+
# have all of them put their messages into the one queue. This can
|
61
|
+
# be dangerous, because you need to make sure that your message
|
62
|
+
# handler can process the different types of messages that might be
|
63
|
+
# sent to the different exchangs.
|
64
|
+
#
|
65
|
+
# @param queue_name [#to_s] the name of the queue to create, if you
|
66
|
+
# don't want to use the class-derived default for some reason.
|
67
|
+
#
|
68
|
+
# @param concurrency [Integer] how many messages to process in parallel.
|
69
|
+
# The default, `1`, means that a message will need to be acknowledged
|
70
|
+
# (by calling `message.ack`) in your worker `blk` before the broker
|
71
|
+
# will consider sending another.
|
72
|
+
#
|
73
|
+
# If your agent is capable of processing more than one message in
|
74
|
+
# parallel (because the agent spends a lot of its time waiting for
|
75
|
+
# databases or HTTP requests, for example, or perhaps you're running
|
76
|
+
# your agents in a Ruby VM which has no GIL) you should increase
|
77
|
+
# this value to improve performance. Alternately, if you want/need
|
78
|
+
# to batch processing (say, you insert 100 records into a database
|
79
|
+
# in a single query) you'll need to increase this to get multiple
|
80
|
+
# records at once.
|
81
|
+
#
|
82
|
+
# Setting this to `0` is only for the adventurous. It tells the
|
83
|
+
# broker to send your agent messages as fast as it can. You still
|
84
|
+
# need to acknowledge the messages as you finish processing them
|
85
|
+
# (otherwise the broker will not consider them "delivered") but you
|
86
|
+
# will always be sent more messages if there are more to send, even
|
87
|
+
# if you never acknowledge any of them. This *can* get you into an
|
88
|
+
# awful lot of trouble if you're not careful, so don't do it just
|
89
|
+
# because you can.
|
90
|
+
#
|
91
|
+
# @param autoparse [Boolean] if your messages are all of a single
|
92
|
+
# content type, and the first thing you do on receiving a message is to
|
93
|
+
# call `JSON.parse(msg.payload)`, then you can turn on `autoparse`, and
|
94
|
+
# *as long as the message content-type is set correctly*, your messages
|
95
|
+
# will be parsed into native data structures before being turned into the
|
96
|
+
# `payload`. This also enables automatic deserialization of object
|
97
|
+
# messages.
|
98
|
+
#
|
99
|
+
# @param allowed_classes [Array<Class>] a list of classes which are
|
100
|
+
# permitted to be instantiated when a message's `content_type` attribute
|
101
|
+
# indicates it is a serialized object. If a message is received that
|
102
|
+
# contains a class not in the list (even if that object is embedded inside
|
103
|
+
# the message itself), the message will be rejected.
|
104
|
+
#
|
105
|
+
# @param routing_key [#to_s] if specified, then the queue that is created
|
106
|
+
# will be bound to the exchange with a defined routing key.
|
107
|
+
#
|
108
|
+
# @param predeclared [Boolean] indicates that the necessary queues and
|
109
|
+
# bindings are already in place, and thus no declarations should be
|
110
|
+
# made to the AMQP broker.
|
111
|
+
#
|
112
|
+
# @param blk [Proc] is called every time a message is received from
|
113
|
+
# the queue, and an instance of {Brown::Agent::AMQPMessage} will
|
114
|
+
# be passed as the sole argument.
|
115
|
+
#
|
116
|
+
# @yieldparam message [Brown::Agent::AMQPMessage] is passed to `blk`
|
117
|
+
# each time a message is received from the queue.
|
118
|
+
#
|
119
|
+
def amqp_listener(exchange_name = "",
|
120
|
+
queue_name: nil,
|
121
|
+
concurrency: 1,
|
122
|
+
autoparse: false,
|
123
|
+
allowed_classes: nil,
|
124
|
+
routing_key: nil,
|
125
|
+
predeclared: false,
|
126
|
+
&blk
|
127
|
+
)
|
128
|
+
exchange_list = (Array === exchange_name ? exchange_name : [exchange_name]).map(&:to_s)
|
129
|
+
|
130
|
+
if queue_name.nil?
|
131
|
+
munged_exchange_list = exchange_list.map { |n| n.to_s == "" ? "" : "-#{n.to_s}" }.join
|
132
|
+
queue_name = self.name.to_s + munged_exchange_list + (routing_key ? ":#{routing_key}" : "")
|
133
|
+
end
|
134
|
+
|
135
|
+
@amqp_listeners ||= []
|
136
|
+
@amqp_listeners << {
|
137
|
+
exchange_list: exchange_list,
|
138
|
+
queue_name: queue_name,
|
139
|
+
concurrency: concurrency,
|
140
|
+
autoparse: autoparse,
|
141
|
+
allowed_classes: allowed_classes,
|
142
|
+
routing_key: routing_key,
|
143
|
+
callback: blk,
|
144
|
+
predeclared: predeclared,
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "json"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
# Methods that have to be prepended in order to work properly.
|
6
|
+
#
|
7
|
+
module Brown::Agent::AMQP::Initializer
|
8
|
+
def initialize(*_)
|
9
|
+
begin
|
10
|
+
super
|
11
|
+
rescue ArgumentError => ex
|
12
|
+
if ex.message =~ /wrong number of arguments.*expected 0/
|
13
|
+
super()
|
14
|
+
else
|
15
|
+
raise
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
initialize_publishers
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
initialize_listeners
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def shutdown
|
29
|
+
amqp_session.close
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def amqp_session
|
37
|
+
@amqp_session ||= begin
|
38
|
+
logger.debug(logloc) { "Initializing AMQP session" }
|
39
|
+
Bunny.new(config.amqp_url, recover_from_connection_close: true, logger: config.logger).tap do |session|
|
40
|
+
session.on_blocked { |blocked| logger.warn(logloc) { "AMQP connection has become blocked: #{blocked.reason}" } }
|
41
|
+
session.on_unblocked { logger.info(logloc) { "AMQP connection has unblocked" } }
|
42
|
+
session.start
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize_publishers
|
48
|
+
(self.class.amqp_publishers || []).each do |publisher|
|
49
|
+
logger.debug(logloc) { "Initializing AMQP publisher #{publisher}" }
|
50
|
+
opts = { exchange_name: publisher[:name] }.merge(publisher[:opts])
|
51
|
+
|
52
|
+
amqp_publisher = Brown::Agent::AMQPPublisher.new(amqp_session: amqp_session, **opts)
|
53
|
+
|
54
|
+
define_singleton_method(publisher[:name]) { amqp_publisher }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize_listeners
|
59
|
+
(self.class.amqp_listeners || []).each do |listener|
|
60
|
+
logger.debug(logloc) { "Initializing AMQP listener #{listener}" }
|
61
|
+
worker_method = "amqp_listener_worker_#{SecureRandom.uuid}".to_sym
|
62
|
+
define_singleton_method(worker_method, listener[:callback])
|
63
|
+
|
64
|
+
@stimuli ||= []
|
65
|
+
@stimuli << {
|
66
|
+
name: "amqp_listener_#{listener[:exchange_list].join("_").gsub(/[^A-Za-z0-9_]/, '_').gsub(/__+/, "_")}",
|
67
|
+
method: method(worker_method),
|
68
|
+
stimuli_proc: proc do |worker|
|
69
|
+
consumer = queue(listener).subscribe(manual_ack: true) do |di, prop, payload|
|
70
|
+
if listener[:autoparse]
|
71
|
+
logger.debug(logloc) { "Attempting to autoparse against Content-Type: #{prop.content_type.inspect}" }
|
72
|
+
case prop.content_type
|
73
|
+
when "application/json"
|
74
|
+
logger.debug(logloc) { "Parsing as JSON" }
|
75
|
+
payload = JSON.parse(payload)
|
76
|
+
when "application/x.yaml"
|
77
|
+
logger.debug(logloc) { "Parsing as YAML" }
|
78
|
+
payload = YAML.load(payload)
|
79
|
+
when "application/vnd.brown.object.v1"
|
80
|
+
logger.debug(logloc) { "Parsing as Brown object, allowed classes: #{listener[:allowed_classes]}" }
|
81
|
+
begin
|
82
|
+
payload = YAML.safe_load(payload, listener[:allowed_classes])
|
83
|
+
rescue Psych::DisallowedClass => ex
|
84
|
+
logger.error(logloc) { "message rejected: #{ex.message}" }
|
85
|
+
di.channel.nack(di.delivery_tag, false, false)
|
86
|
+
next
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
worker.call Brown::Agent::AMQPMessage.new(di, prop, payload)
|
92
|
+
end
|
93
|
+
|
94
|
+
while consumer&.channel&.status == :open do
|
95
|
+
logger.debug(logloc) { "stimuli_proc for #{listener[:queue_name]} having a snooze" }
|
96
|
+
sleep
|
97
|
+
end
|
98
|
+
end
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def queue(listener)
|
104
|
+
@queue_cache ||= {}
|
105
|
+
@queue_cache[listener] ||= begin
|
106
|
+
bind_queue(
|
107
|
+
queue_name: listener[:queue_name],
|
108
|
+
exchange_list: listener[:exchange_list].map(&:to_s),
|
109
|
+
concurrency: listener[:concurrency],
|
110
|
+
routing_key: listener[:routing_key],
|
111
|
+
predeclared: listener[:predeclared],
|
112
|
+
)
|
113
|
+
rescue StandardError => ex
|
114
|
+
log_exception(ex) { "Unknown error while binding queue #{listener[:queue_name].inspect} to exchange list #{listener[:exchange_list].inspect}" }
|
115
|
+
sleep 5
|
116
|
+
retry
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def bind_queue(queue_name:, exchange_list:, concurrency:, routing_key: nil, predeclared: false)
|
121
|
+
ch = amqp_session.create_channel
|
122
|
+
ch.prefetch(concurrency)
|
123
|
+
|
124
|
+
ch.queue(queue_name, durable: true, no_declare: predeclared).tap do |q|
|
125
|
+
next if predeclared
|
126
|
+
exchange_list.each do |exchange_name|
|
127
|
+
if exchange_name != ""
|
128
|
+
begin
|
129
|
+
q.bind(exchange_name, routing_key: routing_key)
|
130
|
+
rescue Bunny::NotFound => ex
|
131
|
+
logger.error { "bind failed: #{ex.message}" }
|
132
|
+
sleep 5
|
133
|
+
return bind_queue(
|
134
|
+
queue_name: queue_name,
|
135
|
+
exchange_list: exchange_list,
|
136
|
+
concurrency: concurrency,
|
137
|
+
routing_key: routing_key,
|
138
|
+
)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -7,11 +7,14 @@
|
|
7
7
|
# "delivered".
|
8
8
|
#
|
9
9
|
class Brown::Agent::AMQPMessage
|
10
|
-
# The
|
11
|
-
# so any serialisation that might have been applied to the message will
|
12
|
-
# have to be undone manually.
|
10
|
+
# The body of the message.
|
13
11
|
#
|
14
|
-
#
|
12
|
+
# Depending on the listener configuration, this may either be a string
|
13
|
+
# representing the raw message, or else a deserialized object of some
|
14
|
+
# sort. See the documentation for the amqp_listener method's `autoparse`
|
15
|
+
# and `allowed_classes` parameters for more details.
|
16
|
+
#
|
17
|
+
# @return [Object]
|
15
18
|
#
|
16
19
|
attr_reader :payload
|
17
20
|
|
@@ -39,4 +42,23 @@ class Brown::Agent::AMQPMessage
|
|
39
42
|
def ack
|
40
43
|
@delivery_info.channel.ack(@delivery_info.delivery_tag)
|
41
44
|
end
|
45
|
+
|
46
|
+
# Decline to handle this message, and requeue for later
|
47
|
+
#
|
48
|
+
# If, for some reason, the agent can't successfully process this message
|
49
|
+
# right *now*, but you'd like it to be processed again later, then you
|
50
|
+
# can ask for it to be requeued using this handy-dandy method.
|
51
|
+
#
|
52
|
+
def requeue
|
53
|
+
@delivery_info.channel.nack(@delivery_info.delivery_tag, false, true)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Decline to handle this message, now and forever
|
57
|
+
#
|
58
|
+
# If you want to dead-letter (or just disappear) a message, call this
|
59
|
+
# method, and it will cease to exist.
|
60
|
+
#
|
61
|
+
def reject
|
62
|
+
@delivery_info.channel.nack(@delivery_info.delivery_tag, false, false)
|
63
|
+
end
|
42
64
|
end
|
@@ -1,8 +1,13 @@
|
|
1
1
|
require 'bunny'
|
2
|
+
require 'json'
|
3
|
+
require 'service_skeleton/logging_helpers'
|
4
|
+
require 'yaml'
|
2
5
|
|
3
6
|
# Publish messages to an AMQP exchange.
|
4
7
|
#
|
5
8
|
class Brown::Agent::AMQPPublisher
|
9
|
+
include ServiceSkeleton::LoggingHelpers
|
10
|
+
|
6
11
|
#:nodoc:
|
7
12
|
# Sentinel to detect that we've been sent the "default" value,
|
8
13
|
# since `nil` can, sometimes, be a valid value.
|
@@ -34,21 +39,14 @@ class Brown::Agent::AMQPPublisher
|
|
34
39
|
# Setup an exchange in the AMQP broker, and allow the publishing of
|
35
40
|
# messages to that exchange.
|
36
41
|
#
|
37
|
-
# @param
|
38
|
-
#
|
39
|
-
# given in the standard fashion (`amqp://<user>:<pass>@<host>`).
|
40
|
-
#
|
41
|
-
# The path portion of AMQP URLs is totes spesh; if you want to
|
42
|
-
# connect to the default vhost (`/`) you either need to specify *no*
|
43
|
-
# trailing slash (ie `amqp://hostname`) or percent-encode the `/`
|
44
|
-
# vhost name (ie `amqp://hostname/%2F`). Yes, this drives me nuts,
|
45
|
-
# too.
|
42
|
+
# @param amqp_session [Bunny::Session] an active session with the AMQP
|
43
|
+
# server. Typically this will be the agent's `@amqp_session` variable.
|
46
44
|
#
|
47
45
|
# @param exchange_type [Symbol] the type of exchange to create or publish
|
48
46
|
# to. By default, the exchange is created as a *direct* exchange; this
|
49
47
|
# routes messages to their destination queue(s) based on the
|
50
48
|
# `routing_key` (set per-publisher or per-queue). Other valid values
|
51
|
-
# for this option are `:
|
49
|
+
# for this option are `:fanout`, `:topic`, and `:headers`.
|
52
50
|
#
|
53
51
|
# @param exchange_name [#to_s] the name of the exchange to create or
|
54
52
|
# publish to. If not specified, then the "default" exchange is used,
|
@@ -88,7 +86,7 @@ class Brown::Agent::AMQPPublisher
|
|
88
86
|
# create the exchange fails for some reason (such as the exchange
|
89
87
|
# already existing with a different configuration).
|
90
88
|
#
|
91
|
-
def initialize(
|
89
|
+
def initialize(amqp_session:,
|
92
90
|
exchange_type: :direct,
|
93
91
|
exchange_name: "",
|
94
92
|
routing_key: nil,
|
@@ -96,23 +94,11 @@ class Brown::Agent::AMQPPublisher
|
|
96
94
|
logger: Logger.new("/dev/null"),
|
97
95
|
**amqp_opts
|
98
96
|
)
|
99
|
-
|
100
|
-
|
101
|
-
@amqp_session.start
|
102
|
-
rescue Bunny::TCPConnectionFailed
|
103
|
-
raise BrokerError,
|
104
|
-
"Failed to connect to #{amqp_url}"
|
105
|
-
rescue Bunny::PossibleAuthenticationFailureError
|
106
|
-
raise BrokerError,
|
107
|
-
"Authentication failed for #{amqp_url}"
|
108
|
-
rescue StandardError => ex
|
109
|
-
raise Error,
|
110
|
-
"Unknown error occured: #{ex.message} (#{ex.class})"
|
111
|
-
end
|
112
|
-
|
113
|
-
@amqp_channel = @amqp_session.create_channel
|
97
|
+
@logger = logger
|
98
|
+
@amqp_channel = amqp_session.create_channel
|
114
99
|
|
115
100
|
begin
|
101
|
+
@logger.debug(logloc) { "Initializing exchange #{exchange_name.inspect}" }
|
116
102
|
@amqp_exchange = @amqp_channel.exchange(
|
117
103
|
exchange_name,
|
118
104
|
type: exchange_type,
|
@@ -133,7 +119,35 @@ class Brown::Agent::AMQPPublisher
|
|
133
119
|
|
134
120
|
# Publish a message to the exchange.
|
135
121
|
#
|
136
|
-
#
|
122
|
+
# A message can be provided in the `payload` parameter in one of several
|
123
|
+
# ways:
|
124
|
+
#
|
125
|
+
# * as a string, in which case the provided string is used as the message
|
126
|
+
# payload as-is, with no further processing or added metadata.
|
127
|
+
#
|
128
|
+
# * as a single-element hash, in which case the key is used as the identifier
|
129
|
+
# of the serialization format (either `:yaml` or `:json`), while the value
|
130
|
+
# is serialized by calling the appropriate conversion method (`.to_yaml` or
|
131
|
+
# `.to_json`, as appropriate) and the resulting string is used as the message
|
132
|
+
# payload. The message's `content_type` attribute is set appropriately, also,
|
133
|
+
# allowing for automatic deserialization at the receiver.
|
134
|
+
#
|
135
|
+
# This payload format is best used where you have complex structured data to
|
136
|
+
# pass around between system components with very diverse implementation
|
137
|
+
# environments, or where very loose coupling is desired.
|
138
|
+
#
|
139
|
+
# * as an arbitrary object, in which case the object is serialized in a form
|
140
|
+
# which allows it to be transparently deserialized again into a native
|
141
|
+
# Ruby object at the other end.
|
142
|
+
#
|
143
|
+
# This payload format is most appropriate when you're deploying a
|
144
|
+
# tightly-coupled system where all the consumers are Brown agents, or at
|
145
|
+
# least all Ruby-based.
|
146
|
+
#
|
147
|
+
# @param payload [String, Hash<Symbol, Object>, Object] the content of the
|
148
|
+
# message to send. There are several possibilities for what can be passed
|
149
|
+
# here, and how it is encoded into an AMQP message, as per the description
|
150
|
+
# above.
|
137
151
|
#
|
138
152
|
# @param type [#to_s] override the default message type set in the
|
139
153
|
# publisher, just for this one message.
|
@@ -150,6 +164,8 @@ class Brown::Agent::AMQPPublisher
|
|
150
164
|
# for full details of every possible permutation.
|
151
165
|
#
|
152
166
|
def publish(payload, type: NoValue, routing_key: NoValue, **amqp_opts)
|
167
|
+
@logger.debug(logloc) { "Publishing message #{payload.inspect}, type: #{type.inspect}, routing_key: #{routing_key.inspect}, options: #{amqp_opts.inspect}" }
|
168
|
+
|
153
169
|
opts = @message_defaults.merge(
|
154
170
|
{
|
155
171
|
type: type,
|
@@ -157,6 +173,30 @@ class Brown::Agent::AMQPPublisher
|
|
157
173
|
}.delete_if { |_,v| v == NoValue }
|
158
174
|
).delete_if { |_,v| v.nil? }.merge(amqp_opts)
|
159
175
|
|
176
|
+
if payload.is_a?(Hash)
|
177
|
+
if payload.length != 1
|
178
|
+
raise ArgumentError,
|
179
|
+
"Payload hash must have exactly one element"
|
180
|
+
end
|
181
|
+
|
182
|
+
case payload.keys.first
|
183
|
+
when :json
|
184
|
+
@logger.debug(logloc) { "JSON serialisation activated" }
|
185
|
+
opts[:content_type] = "application/json"
|
186
|
+
payload = payload.values.first.to_json
|
187
|
+
when :yaml
|
188
|
+
@logger.debug(logloc) { "YAML serialisation activated" }
|
189
|
+
opts[:content_type] = "application/x.yaml"
|
190
|
+
payload = payload.values.first.to_yaml
|
191
|
+
else
|
192
|
+
raise ArgumentError,
|
193
|
+
"Unknown format type: #{payload.keys.first.inspect} (must be :json or :yaml)"
|
194
|
+
end
|
195
|
+
elsif !payload.is_a?(String)
|
196
|
+
payload = payload.to_yaml
|
197
|
+
opts[:content_type] = "application/vnd.brown.object.v1"
|
198
|
+
end
|
199
|
+
|
160
200
|
if @amqp_exchange.name == "" and opts[:routing_key].nil?
|
161
201
|
raise ExchangeError,
|
162
202
|
"Cannot send a message to the default exchange without a routing key"
|
@@ -164,6 +204,7 @@ class Brown::Agent::AMQPPublisher
|
|
164
204
|
|
165
205
|
@channel_mutex.synchronize do
|
166
206
|
@amqp_exchange.publish(payload, opts)
|
207
|
+
@logger.debug(logloc) { "... and it's gone" }
|
167
208
|
end
|
168
209
|
end
|
169
210
|
end
|