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.
@@ -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=
@@ -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
+
@@ -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
@@ -0,0 +1,9 @@
1
+ module Blinkbox
2
+ module CommonMessaging
3
+ VERSION = begin
4
+ File.read(File.join(File.dirname(__FILE__), "../../../VERSION"))
5
+ rescue Errno::ENOENT
6
+ "0.0.0-unknown"
7
+ end
8
+ end
9
+ 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: []