blinkbox-common_messaging 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/CHANGELOG.md +72 -0
- data/README.md +1 -0
- data/VERSION +1 -0
- data/lib/blinkbox/common_messaging.rb +239 -0
- data/lib/blinkbox/common_messaging/exchange.rb +84 -0
- data/lib/blinkbox/common_messaging/header_detectors.rb +20 -0
- data/lib/blinkbox/common_messaging/header_detectors/detect_remote_uris.rb +41 -0
- data/lib/blinkbox/common_messaging/queue.rb +149 -0
- data/lib/blinkbox/common_messaging/version.rb +9 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
N2MwMWMwOWQzM2ZkMTMzN2FlNzRhMTBkZjA0MWQ0Y2JjMWFmYmY3Mw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MGJkMjdlZWRlYzgwNThmYmMwYjYzOGM0MjI2MGRkNzZhMDJiZTIzNw==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZDEwMTY2MThmZjdlZDBmMWY1YWQ5ODdjMjQyYjk3ZDdhOTVmYjlmODgzMTJh
|
10
|
+
OThiZWU3ZjEyMjJkZWQ1YjBjYjkzOGVmNGNhOGNlMjQxYjRiMjM1MTA3ZDdh
|
11
|
+
Nzc4ZTk3NzgxN2E2OWU2YTQ5ZDJhYzMyYmIxZjNiYTdkZTEzZjE=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZGNmZTQ0YTJiN2IxZGVkMjEzY2MwMDE2ZjU4YzFlNGNmMzFjNjQ3MmNhODlk
|
14
|
+
NDVlZWQ2MjY2ZDhjZTYwMThjNzM2ZGQzZDg5OTU3NDhhZDU4OGQ0Mjc5NWY1
|
15
|
+
YjIxNTdlOGM0MmJhYzQ1MzhkYWNmOTM1OWY5ZWJiODQ3OTNmOGY=
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Change log
|
2
|
+
|
3
|
+
## Open Source release (2015-01-28 14:11:21)
|
4
|
+
|
5
|
+
Today we have decided to publish parts of our codebase on Github under the [MIT licence](LICENCE). The licence is very permissive, but we will always appreciate hearing if and where our work is used!
|
6
|
+
|
7
|
+
## 0.5.2 ([#9](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/9) 2014-12-22 15:03:21)
|
8
|
+
|
9
|
+
Ensure Bunny logging to Graylog
|
10
|
+
|
11
|
+
### Improvement
|
12
|
+
|
13
|
+
- Ensure the logger is set for bunny at the low level. [Bunny Docs](http://rubybunny.info/articles/connecting.html)
|
14
|
+
|
15
|
+
## 0.5.1 ([#8](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/8) 2014-11-25 17:44:39)
|
16
|
+
|
17
|
+
Only stringify if it can be stringified
|
18
|
+
|
19
|
+
### Bug fix
|
20
|
+
|
21
|
+
- Mapping Updates are arrays, not objects, which fail when sent into the initialiser.
|
22
|
+
|
23
|
+
## 0.5.0 ([#7](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/7) 2014-11-10 13:05:42)
|
24
|
+
|
25
|
+
Temporary queues
|
26
|
+
|
27
|
+
### New feature
|
28
|
+
|
29
|
+
- Added support for temporary and exclusive queues. Needed for `common_mapping`.
|
30
|
+
|
31
|
+
## 0.4.1 ([#5](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/5) 2014-11-06 16:31:05)
|
32
|
+
|
33
|
+
Refactor
|
34
|
+
|
35
|
+
### Improvements
|
36
|
+
|
37
|
+
- Just shift code between two files. No code alterations whatsoever.
|
38
|
+
|
39
|
+
## 0.4.0 ([#4](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/4) 2014-10-24 10:11:02)
|
40
|
+
|
41
|
+
Validation of messages
|
42
|
+
|
43
|
+
### New feature
|
44
|
+
|
45
|
+
- Payloads can be validated or not, allowing 'catch all' queues.
|
46
|
+
|
47
|
+
## 0.3.0 ([#3](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/3) 2014-10-09 10:46:10)
|
48
|
+
|
49
|
+
Prefetch
|
50
|
+
|
51
|
+
### New feature
|
52
|
+
|
53
|
+
- Add prefetch capability
|
54
|
+
|
55
|
+
## 0.2.0 ([#2](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/2) 2014-10-03 13:30:51)
|
56
|
+
|
57
|
+
Remote uris
|
58
|
+
|
59
|
+
### New feature
|
60
|
+
|
61
|
+
- Any message sent with this library that contains a hash with the key `"type" => "remote"` will have its deep key placed into the `remote_uris` header for Marvin 2.0's resource fetcher to process.
|
62
|
+
- Consolidated `VERSION` retrieval.
|
63
|
+
|
64
|
+
## 0.1.0 ([#1](https://git.mobcastdev.com/Platform/common_messaging.rb/pull/1) 2014-09-01 08:37:37)
|
65
|
+
|
66
|
+
Basic needs of the messaging library
|
67
|
+
|
68
|
+
### New Features
|
69
|
+
|
70
|
+
- Allows blinkbox Books specific message message publishing and subscription
|
71
|
+
- Automatically validates incoming and outbound message structure against the json schema.
|
72
|
+
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Blinkbox::CommonMessaging
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.2
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require "blinkbox/common_messaging/version"
|
2
|
+
require "bunny"
|
3
|
+
require "uri"
|
4
|
+
require "active_support/core_ext/hash/keys"
|
5
|
+
require "active_support/core_ext/hash/deep_merge"
|
6
|
+
require "active_support/core_ext/string/inflections"
|
7
|
+
require "ruby_units"
|
8
|
+
require "forwardable"
|
9
|
+
require "json-schema"
|
10
|
+
require "securerandom"
|
11
|
+
require "logger"
|
12
|
+
require "blinkbox/common_messaging/header_detectors"
|
13
|
+
require "blinkbox/common_messaging/queue"
|
14
|
+
require "blinkbox/common_messaging/exchange"
|
15
|
+
|
16
|
+
module Blinkbox
|
17
|
+
# A group of methods and classes which enable the delivery of messages through the
|
18
|
+
# blinkbox Books ecosystem via AMQP.
|
19
|
+
#
|
20
|
+
# `CommonMessaging.configure!` should be used to set up connection details first, then
|
21
|
+
# every subsequent call to `CommonMessaging::Queue.new` will create a `Bunny::Queue` object
|
22
|
+
# using the connection details that were present at the time.
|
23
|
+
module CommonMessaging
|
24
|
+
# The default RabbitMQ connection details, in the format that Bunny needs them.
|
25
|
+
DEFAULT_CONFIG = {
|
26
|
+
bunny: {
|
27
|
+
host: "localhost",
|
28
|
+
port: 5672,
|
29
|
+
user: "guest",
|
30
|
+
pass: "guest",
|
31
|
+
vhost: "/",
|
32
|
+
log_level: Logger::WARN,
|
33
|
+
automatically_recover: true,
|
34
|
+
threaded: true,
|
35
|
+
continuation_timeout: 4000
|
36
|
+
},
|
37
|
+
retry_interval: {
|
38
|
+
initial: Unit("5 seconds"),
|
39
|
+
max: Unit("5 seconds")
|
40
|
+
},
|
41
|
+
logger: Logger.new(nil)
|
42
|
+
}
|
43
|
+
|
44
|
+
# This method only stores connection details for calls to `CommonMessaging::Queue.new`.
|
45
|
+
# Any queues already created will not be affected by subsequent calls to this method.
|
46
|
+
#
|
47
|
+
# This method converts the given options from the blinkbox Books common config format
|
48
|
+
# to the format required for Bunny so that calls like the following are possible:
|
49
|
+
#
|
50
|
+
# @example Using with CommonConfig
|
51
|
+
# require "blinkbox/common_config"
|
52
|
+
# require "blinkbox/common_messaging"
|
53
|
+
#
|
54
|
+
# config = Blinkbox::CommonConfig.new
|
55
|
+
# Blinkbox::CommonMessaging.configure!(config.tree(:rabbitmq))
|
56
|
+
#
|
57
|
+
# @param [Hash] config The configuration options needed for an MQ connection.
|
58
|
+
# @option config [String] :url The URL to the RabbitMQ server, eg. amqp://user:pass@host.name:1234/virtual_host
|
59
|
+
# @option config [Unit] :initialRetryInterval The interval at which re-connection attempts should be made when a RabbitMQ failure first occurs.
|
60
|
+
# @option config [Unit] :maxRetryInterval The maximum interval at which RabbitMQ reconnection attempts should back off to.
|
61
|
+
# @param [#debug, #info, #warn, #error, #fatal] logger The logger instance which should be used by Bunny
|
62
|
+
def self.configure!(config, logger = nil)
|
63
|
+
@@config = DEFAULT_CONFIG
|
64
|
+
|
65
|
+
unless config[:url].nil?
|
66
|
+
uri = URI.parse(config[:url])
|
67
|
+
@@config.deep_merge!(
|
68
|
+
bunny: {
|
69
|
+
host: uri.host,
|
70
|
+
port: uri.port,
|
71
|
+
user: uri.user,
|
72
|
+
pass: uri.password,
|
73
|
+
vhost: uri.path
|
74
|
+
}
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
%i{initialRetryInterval maxRetryInterval}.each do |unit_key|
|
79
|
+
if config[unit_key]
|
80
|
+
config[unit_key] = Unit(config[unit_key]) unless config[unit_key].is_a?(Unit)
|
81
|
+
|
82
|
+
@@config.deep_merge!(
|
83
|
+
retry_interval: {
|
84
|
+
unit_key.to_s.sub('RetryInterval', '').to_sym => config[unit_key]
|
85
|
+
}
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
self.logger = logger unless logger.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the current config being used (as used by Bunny)
|
94
|
+
#
|
95
|
+
# @return [Hash]
|
96
|
+
def self.config
|
97
|
+
@@config rescue DEFAULT_CONFIG
|
98
|
+
end
|
99
|
+
|
100
|
+
# Sets the logger delivered to Bunny when new connections are made
|
101
|
+
#
|
102
|
+
# @param [] logger The object to which log messages should be sent.
|
103
|
+
def self.logger=(logger)
|
104
|
+
%i{debug info warn error fatal level= level}.each do |m|
|
105
|
+
raise ArgumentError, "The logger did not respond to '#{m}'" unless logger.respond_to?(m)
|
106
|
+
end
|
107
|
+
@@config[:logger] = logger
|
108
|
+
@@config[:bunny][:logger] = logger
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns (and starts if necessary) the connection to the RabbitMQ server as specified by the current
|
112
|
+
# config. Will keep only one connection per configuration at any time and will return or create a new connection
|
113
|
+
# as necessary. Channels are created with publisher confirmations.
|
114
|
+
#
|
115
|
+
# Application code should not need to use this method.
|
116
|
+
#
|
117
|
+
# @return [Bunny::Session]
|
118
|
+
def self.connection
|
119
|
+
@@connections ||= {}
|
120
|
+
@@connections[config] ||= Bunny.new(config[:bunny])
|
121
|
+
@@connections[config].start
|
122
|
+
@@connections[config]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Blocks until all the open connections have been closed, calling the block with any message_ids which haven't been delivered
|
126
|
+
#
|
127
|
+
# @param [Boolean] block_until_confirms Force the method to block until all messages have been acked or nacked.
|
128
|
+
# @yield [message_id] Calls the given block for any message that was undeliverable (if block_until_confirms was `true`)
|
129
|
+
# @yieldparam [String] message_id The message_id of the message which could not be delivered
|
130
|
+
def self.close_connections(block_until_confirms: true)
|
131
|
+
@@connections.each do |k, c|
|
132
|
+
if block_until_confirms && !c.wait_for_confirms
|
133
|
+
c.nacked_set.each do |message_id|
|
134
|
+
yield message_id if block_given?
|
135
|
+
end
|
136
|
+
end
|
137
|
+
c.close
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
module JsonSchemaPowered
|
142
|
+
extend Forwardable
|
143
|
+
def_delegators :@data, :responds_to?, :to_json, :[]
|
144
|
+
|
145
|
+
def method_missing(m, *args, &block)
|
146
|
+
@data.send(m, *args, &block)
|
147
|
+
end
|
148
|
+
|
149
|
+
def to_hash
|
150
|
+
@data
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_s
|
154
|
+
@data.to_json
|
155
|
+
end
|
156
|
+
|
157
|
+
def ==(other)
|
158
|
+
self.to_hash == other.to_hash
|
159
|
+
rescue
|
160
|
+
# Any errors would be because the other isn't a hash, so the answer must be false
|
161
|
+
false
|
162
|
+
end
|
163
|
+
|
164
|
+
def inspect
|
165
|
+
classification_string = @data["classification"].map do |cl|
|
166
|
+
"#{cl["realm"]}:#{cl["id"]}"
|
167
|
+
end.join(", ")
|
168
|
+
"<#{self.class.name.split("::").last}: #{classification_string}>"
|
169
|
+
rescue
|
170
|
+
to_s
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class UndeliverableMessageError < RuntimeError; end
|
175
|
+
|
176
|
+
# Generates ruby classes representing blinkbox Books messages from the schema files at the
|
177
|
+
# given path.
|
178
|
+
#
|
179
|
+
# @example Initialising CommonMessaging for sending
|
180
|
+
# Blinkbox::CommonMessaging.init_from_schema_at("ingestion.book.metatdata.v2.schema.json")
|
181
|
+
# msg = Blinkbox::CommonMessaging::IngestionBookMetadataV2.new(title: "A title")
|
182
|
+
# exchange.publish(msg)
|
183
|
+
#
|
184
|
+
# @example Using the root path
|
185
|
+
# Blinkbox::CommonMessaging.init_from_schema_at("./schema/ingestion/book/metatdata/v2.schema.json")
|
186
|
+
# # => [Blinkbox::CommonMessaging::SchemaIngestionBookMetadataV2]
|
187
|
+
#
|
188
|
+
# Blinkbox::CommonMessaging.init_from_schema_at("./schema/ingestion/book/metatdata/v2.schema.json", "./schema")
|
189
|
+
# # => [Blinkbox::CommonMessaging::IngestionBookMetadataV2]
|
190
|
+
#
|
191
|
+
# @param [String] path The path to a (or a folder of) json-schema file(s) in the blinkbox Books format.
|
192
|
+
# @param [String] root The root path from which namespaces will be calculated.
|
193
|
+
# @return Array of class names generated
|
194
|
+
def self.init_from_schema_at(path, root = path)
|
195
|
+
fail "The path #{path} does not exist" unless File.exist?(path)
|
196
|
+
return Dir[File.join(path, "**/*.schema.json")].map { |file| init_from_schema_at(file, root) }.flatten if File.directory?(path)
|
197
|
+
|
198
|
+
root = File.dirname(root) if root =~ /\.schema\.json$/
|
199
|
+
schema_name = path.sub(%r{^(?:\./)?#{root}/?(.+)\.schema\.json$}, "\\1").tr("/",".")
|
200
|
+
class_name = class_name_from_schema_name(schema_name)
|
201
|
+
|
202
|
+
# We will re-declare these classes if required, rather than raise an error.
|
203
|
+
remove_const(class_name) if constants.include?(class_name.to_sym)
|
204
|
+
|
205
|
+
const_set(class_name, Class.new {
|
206
|
+
include JsonSchemaPowered
|
207
|
+
|
208
|
+
def initialize(data = {})
|
209
|
+
@data = data
|
210
|
+
@data = @data.stringify_keys if data.respond_to?(:stringify_keys)
|
211
|
+
JSON::Validator.validate!(self.class.const_get("SCHEMA_FILE"), @data, insert_defaults: true)
|
212
|
+
end
|
213
|
+
|
214
|
+
def content_type
|
215
|
+
self.class.const_get("CONTENT_TYPE")
|
216
|
+
end
|
217
|
+
})
|
218
|
+
|
219
|
+
klass = const_get(class_name)
|
220
|
+
klass.const_set('CONTENT_TYPE', "application/vnd.blinkbox.books.#{schema_name}+json")
|
221
|
+
klass.const_set('SCHEMA_FILE', path)
|
222
|
+
klass
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.class_from_content_type(content_type)
|
226
|
+
fail "No content type was given" if content_type.nil? || content_type.empty?
|
227
|
+
begin
|
228
|
+
schema_name = content_type.sub(%r{^application/vnd\.blinkbox\.books\.(.+)\+json$}, '\1')
|
229
|
+
const_get(class_name_from_schema_name(schema_name))
|
230
|
+
rescue
|
231
|
+
raise "The schema for the #{content_type} content type has not been loaded"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.class_name_from_schema_name(schema_name)
|
236
|
+
schema_name.tr("./", "_").camelcase
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Blinkbox
|
2
|
+
module CommonMessaging
|
3
|
+
class Exchange
|
4
|
+
extend Forwardable
|
5
|
+
def_delegators :@exchange, :on_return
|
6
|
+
|
7
|
+
# A wrapped class for Bunny::Exchange. Wrapped so we can take care of message validation and header
|
8
|
+
# conventions in the blinkbox Books format.
|
9
|
+
#
|
10
|
+
# @param [String] exchange_name The name of the Exchange to connect to.
|
11
|
+
# @param [String] facility The name of the app or service (we've adopted the GELF naming term across ruby)
|
12
|
+
# @param [String] facility_version The version of the app or service which sent the message.
|
13
|
+
# @raise [Bunny::NotFound] If the exchange does not exist.
|
14
|
+
def initialize(exchange_name, facility: File.basename($0, '.rb'), facility_version: "0.0.0-unknown")
|
15
|
+
@app_id = "#{facility}:v#{facility_version}"
|
16
|
+
connection = CommonMessaging.connection
|
17
|
+
channel = connection.create_channel
|
18
|
+
channel.confirm_select
|
19
|
+
@exchange = channel.headers(
|
20
|
+
exchange_name,
|
21
|
+
durable: true,
|
22
|
+
auto_delete: false,
|
23
|
+
passive: true
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Publishes a message to the exchange with blinkbox Books default message headers and properties.
|
28
|
+
#
|
29
|
+
# Worth noting that because of a quirk of the RabbitMQ Headers Exchange you cannot route on properties
|
30
|
+
# so, in order to facilitate routing on content-type, that key is written to the headers by default as
|
31
|
+
# well as to the properties.
|
32
|
+
#
|
33
|
+
# @param [Blinkbox::CommonMessaging::JsonSchemaPowered, String] data The information which will be sent as the payload of the message. An instance of any class generated by Blinkbox::CommonMessaging.init_from_schema_at while :validate is true, or a String if false.
|
34
|
+
# @param [Hash] headers A hash of string keys and string values which will be sent as headers with the message. Used for matching.
|
35
|
+
# @param [Array<String>] message_id_chain Optional. The message_id_chain of the message which was received in order to prompt this one.
|
36
|
+
# @param [Boolean] confirm Will block this method until the MQ server has confirmed the message has been persisted and routed.
|
37
|
+
# @param [Boolean] validate if false will relax the constraint that the inbound data must be a JsonSchemaPowered object.
|
38
|
+
# @return [String] The correlation_id of the message which was delivered.
|
39
|
+
def publish(data, headers: {}, message_id_chain: [], confirm: true, validate: true)
|
40
|
+
raise ArgumentError, "All published messages must be validated. Please see Blinkbox::CommonMessaging.init_from_schema_at for details." if validate && !data.class.included_modules.include?(JsonSchemaPowered)
|
41
|
+
raise ArgumentError, "message_id_chain must be an array of strings" unless message_id_chain.is_a?(Array)
|
42
|
+
|
43
|
+
message_id = generate_message_id
|
44
|
+
new_message_id_chain = message_id_chain.dup << message_id
|
45
|
+
correlation_id = new_message_id_chain.first
|
46
|
+
|
47
|
+
headers = headers.merge!("message_id_chain" => new_message_id_chain)
|
48
|
+
options = {}
|
49
|
+
|
50
|
+
if data.respond_to?(:content_type)
|
51
|
+
hd = Blinkbox::CommonMessaging::HeaderDetectors.new(data)
|
52
|
+
headers = hd.modified_headers(headers)
|
53
|
+
# We have to do both of these because of RabbitMQ's weird header exchange protocol
|
54
|
+
headers["content-type"] = data.content_type
|
55
|
+
options[:content_type] = data.content_type
|
56
|
+
data = data.to_json
|
57
|
+
end
|
58
|
+
|
59
|
+
options.merge!(
|
60
|
+
persistent: true,
|
61
|
+
correlation_id: correlation_id,
|
62
|
+
message_id: message_id,
|
63
|
+
app_id: @app_id,
|
64
|
+
timestamp: Time.now.to_i,
|
65
|
+
headers: headers
|
66
|
+
)
|
67
|
+
@exchange.publish(data, options)
|
68
|
+
|
69
|
+
if confirm && !@exchange.channel.wait_for_confirms
|
70
|
+
message_id = @exchange.channel.nacked_set.first
|
71
|
+
raise UndeliverableMessageError, "Message #{message_id} was returned as undeliverable by RabbitMQ."
|
72
|
+
end
|
73
|
+
|
74
|
+
message_id
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def generate_message_id
|
80
|
+
SecureRandom.hex(8) # 8 generates a 16 byte string
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Blinkbox::CommonMessaging::HeaderDetectors
|
2
|
+
@@header_detectors = []
|
3
|
+
|
4
|
+
def initialize(obj)
|
5
|
+
@obj = obj
|
6
|
+
end
|
7
|
+
|
8
|
+
def modified_headers(original_headers = {})
|
9
|
+
@@header_detectors.each do |m|
|
10
|
+
original_headers = send(m, original_headers)
|
11
|
+
end
|
12
|
+
original_headers
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.register(method_name)
|
16
|
+
@@header_detectors << method_name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Dir.glob(File.join(__dir__, "header_detectors/*.rb")) { |hd| require hd }
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Blinkbox::CommonMessaging::HeaderDetectors
|
2
|
+
def detect_remote_uris(original_headers)
|
3
|
+
hash = @obj.to_hash.extend(ExtraHashMethods)
|
4
|
+
deep_keys = hash.deep_key_select do |h|
|
5
|
+
h["type"] == "remote"
|
6
|
+
end
|
7
|
+
if !deep_keys.empty?
|
8
|
+
original_headers.merge!(
|
9
|
+
"has_remote_uris" => true,
|
10
|
+
"remote_uris" => deep_keys
|
11
|
+
)
|
12
|
+
end
|
13
|
+
original_headers
|
14
|
+
end
|
15
|
+
|
16
|
+
register :detect_remote_uris
|
17
|
+
end
|
18
|
+
|
19
|
+
module ExtraHashMethods
|
20
|
+
def deep_key_select(parent_key: "", &block)
|
21
|
+
keys = []
|
22
|
+
keys.push parent_key if block.call(self)
|
23
|
+
self.each do |k, v|
|
24
|
+
case v
|
25
|
+
when Hash
|
26
|
+
v.extend(ExtraHashMethods).deep_key_select(parent_key: k, &block).each do |hit|
|
27
|
+
keys.push [parent_key, hit].join(".").sub(/^\./,"")
|
28
|
+
end
|
29
|
+
when Array
|
30
|
+
v.each_with_index do |item, i|
|
31
|
+
if item.is_a? Hash
|
32
|
+
item.extend(ExtraHashMethods).deep_key_select(parent_key: "#{k}[#{i}]", &block).each do |hit|
|
33
|
+
keys.push [parent_key, hit].join(".").sub(/^\./,"")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
keys
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
module Blinkbox
|
4
|
+
module CommonMessaging
|
5
|
+
# A proxy class for generating queues and binding them to exchanges using Bunny. In the
|
6
|
+
# format expected from blinkbox Books services.
|
7
|
+
class Queue
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@queue, :status
|
10
|
+
|
11
|
+
# Create a queue object for subscribing to messages with.
|
12
|
+
#
|
13
|
+
# NB. There is no way to know what bindings have already been made for a queue, so all code
|
14
|
+
# subscribing to a queue should cope with receiving messages it's not expecting.
|
15
|
+
#
|
16
|
+
# @param [String] queue_name The name of the queue which should be used and (if necessary) created.
|
17
|
+
# @param [String] exchange The name of the Exchange to bind to. The default value should be avoided for production uses.
|
18
|
+
# @param [String, nil] dlx The name of the Dead Letter Exchange to send nacked messages to.
|
19
|
+
# @param [Array,Hash] bindings An array of hashes, each on detailing the parameters for a new binding.
|
20
|
+
# @param [Integer] prefetch The number of messages to collect at a time when subscribing.
|
21
|
+
# @raise [Bunny::NotFound] If the exchange does not exist.
|
22
|
+
# @return [Bunny::Queue] A blinkbox managed Bunny Queue object
|
23
|
+
def initialize(queue_name, exchange: "amq.headers", dlx: "#{exchange}.DLX", bindings: [], prefetch: 10, exclusive: false, temporary: false)
|
24
|
+
raise ArgumentError, "Prefetch must be a positive integer" unless prefetch.is_a?(Integer) && prefetch > 0
|
25
|
+
connection = CommonMessaging.connection
|
26
|
+
@logger = CommonMessaging.config[:logger]
|
27
|
+
# We create one channel per queue because it means that any issues are isolated
|
28
|
+
# and we can start a new channel and resume efforts in a segregated manner.
|
29
|
+
@channel = connection.create_channel
|
30
|
+
@channel.prefetch(prefetch)
|
31
|
+
args = {}
|
32
|
+
args["x-dead-letter-exchange"] = dlx unless dlx.nil?
|
33
|
+
@queue = @channel.queue(
|
34
|
+
queue_name,
|
35
|
+
durable: !temporary,
|
36
|
+
auto_delete: temporary,
|
37
|
+
exclusive: exclusive,
|
38
|
+
arguments: args
|
39
|
+
)
|
40
|
+
@exchange = @channel.headers(
|
41
|
+
exchange,
|
42
|
+
durable: true,
|
43
|
+
auto_delete: false,
|
44
|
+
passive: true
|
45
|
+
)
|
46
|
+
Kernel.warn "No bindings were given, the queue is unlikely to receive any messages" if bindings.empty?
|
47
|
+
bindings.each do |binding|
|
48
|
+
@queue.bind(@exchange, arguments: binding)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Defines a new block for handling exceptions which occur when processing an incoming message. Cases where this might occur include:
|
53
|
+
#
|
54
|
+
# * A message which doesn't have a recognised content-type (ie. one which has been 'init'ed)
|
55
|
+
# * An invalid JSON message
|
56
|
+
# * A valid JSON message which doesn't pass schema validation
|
57
|
+
#
|
58
|
+
# @example Sending excepted messages to a log, then nack them
|
59
|
+
# log = Logger.new(STDOUT)
|
60
|
+
# queue = Blinkbox::CommonMessaging::Queue.new("My.Queue")
|
61
|
+
# queue.on_exception do |e, delivery_info, metadata, payload|
|
62
|
+
# log.error e
|
63
|
+
# channel.reject(delivery_info[:delivery_tag], false)
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @yield [exception, channel, delivery)info, metadata, payload] Yields for each exception which occurs.
|
67
|
+
# @yieldparam [Exception] exception The exception which was raised.
|
68
|
+
# @yieldparam [Bunny::Connection] exception The channel this exchnage is using (useful for nacking).
|
69
|
+
# @yieldparam [Hash] delivery_info The RabbitMQ delivery info for the message (useful for nacking).
|
70
|
+
# @yieldparam [Hash] metadata The metadata delivered from the RabbitMQ server (parameters and headers).
|
71
|
+
# @yieldparam [String] payload The message that was received
|
72
|
+
def on_exception(&block)
|
73
|
+
raise ArgumentError, "Please specify a block to call when an exception is raised" unless block_given?
|
74
|
+
@on_exception = block
|
75
|
+
end
|
76
|
+
|
77
|
+
# Emits the metadata and objectified payload for every message which appears on the queue. Any message with a content-type
|
78
|
+
# not 'init'ed will be rejected (without retry) automatically.
|
79
|
+
#
|
80
|
+
# * Returning `true` or `:ack` from the block will acknowledge and remove the message from the queue
|
81
|
+
# * Returning `false` or `:reject` from the block will send the message to the DLQ
|
82
|
+
# * Returning `:retry` will put the message back on the queue to be tried again later.
|
83
|
+
#
|
84
|
+
# @example Subscribing to messages
|
85
|
+
# queue = Blinkbox::CommonMessaging::Queue.new("catch-all", exchange_name: "Marvin", [{}])
|
86
|
+
# queue.subscribe(block:true) do |metadata, obj|
|
87
|
+
# puts "Messge received."
|
88
|
+
# puts "Headers: #{metadata[:headers].to_json}"
|
89
|
+
# puts "Body: #{obj.to_json}"
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# @param [Boolean] :block Should this method block while being executed (true, default) or spawn a new thread? (false)
|
93
|
+
# @param [Array<Blinkbox::CommonMessaging::JsonSchemaPowered>, nil] :accept List of schema types to accept (any not on the list will be rejected). `nil` will accept all message types and not validate incoming messages.
|
94
|
+
# @yield [metadata, payload_object] A block to execute for each message which is received on this queue.
|
95
|
+
# @yieldparam metadata [Hash] The properties and headers (in [:headers]) delivered with the message.
|
96
|
+
# @yieldparam payload_object [Blinkbox::CommonMessaging::JsonSchemaPowered] An object representing the validated JSON payload.
|
97
|
+
# @yieldreturn [Boolean, :ack, :reject, :retry]
|
98
|
+
def subscribe(block: true, accept: nil)
|
99
|
+
raise ArgumentError, "Please give a block to run when a message is received" unless block_given?
|
100
|
+
@queue.subscribe(
|
101
|
+
block: block,
|
102
|
+
manual_ack: true
|
103
|
+
) do |delivery_info, metadata, payload|
|
104
|
+
begin
|
105
|
+
if accept.nil?
|
106
|
+
object = payload
|
107
|
+
else
|
108
|
+
klass = Blinkbox::CommonMessaging.class_from_content_type(metadata[:headers]['content-type'])
|
109
|
+
if accept.include?(klass)
|
110
|
+
object = klass.new(JSON.parse(payload))
|
111
|
+
else
|
112
|
+
response = :reject
|
113
|
+
end
|
114
|
+
end
|
115
|
+
response ||= yield(metadata, object)
|
116
|
+
case response
|
117
|
+
when :ack, true
|
118
|
+
@channel.ack(delivery_info[:delivery_tag])
|
119
|
+
when :reject, false
|
120
|
+
@channel.reject(delivery_info[:delivery_tag], false)
|
121
|
+
when :retry
|
122
|
+
@channel.reject(delivery_info[:delivery_tag], true)
|
123
|
+
else
|
124
|
+
fail "Unknown response from subscribe block: #{response}"
|
125
|
+
end
|
126
|
+
rescue Exception => e
|
127
|
+
(@on_exception || method(:default_on_exception)).call(e, @channel, delivery_info, metadata, payload)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Purges all messages from this queue. Destroys data!
|
133
|
+
#
|
134
|
+
# @return [true] Returns true if the purge occurred correctly (or a RabbitMQ error if it couldn't)
|
135
|
+
def purge!
|
136
|
+
@queue.purge
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
# The default handler for exceptions which occur when processing a message.
|
143
|
+
def default_on_exception(exception, channel, delivery_info, metadata, payload)
|
144
|
+
@logger.error exception
|
145
|
+
channel.reject(delivery_info[:delivery_tag], false)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blinkbox-common_messaging
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- JP Hastings-Spital
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bunny
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ruby-units
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.4'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: json-schema
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Simple helper for messaging around blinkbox Books
|
112
|
+
email:
|
113
|
+
- jphastings@blinkbox.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files:
|
117
|
+
- README.md
|
118
|
+
- CHANGELOG.md
|
119
|
+
files:
|
120
|
+
- CHANGELOG.md
|
121
|
+
- README.md
|
122
|
+
- VERSION
|
123
|
+
- lib/blinkbox/common_messaging.rb
|
124
|
+
- lib/blinkbox/common_messaging/exchange.rb
|
125
|
+
- lib/blinkbox/common_messaging/header_detectors.rb
|
126
|
+
- lib/blinkbox/common_messaging/header_detectors/detect_remote_uris.rb
|
127
|
+
- lib/blinkbox/common_messaging/queue.rb
|
128
|
+
- lib/blinkbox/common_messaging/version.rb
|
129
|
+
homepage: ''
|
130
|
+
licenses: []
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ! '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.4.5
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: Simple helper for messaging around blinkbox Books
|
152
|
+
test_files: []
|