brown 2.2.2 → 2.2.2.25.g85ddf08
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.
- 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
|