blinkbox-common_messaging 0.5.2
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 +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: []
|