govuk_message_queue_consumer 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c236b54beb0a0440047c111caf390c175b9a6a67
4
+ data.tar.gz: 6064d29e67a5156cf20d111d8704045cb8816fac
5
+ SHA512:
6
+ metadata.gz: bb3b285c4210e1cc9bd0c42395ab875391aaa1a94d795bcd1a53d005ddcdaea852bf3f0e59c4f846f63abe5a7d74961d6eb5c20d570e2ee4fdc56246c7afade8
7
+ data.tar.gz: b62511ec795ad7dcda904577f4f3ab2ba2943fbc27a2b2ba4be6c5c292f5f18c146c92d1390022aa3ba9d89096d7bdca6df2da7c198d2b6495ef365a968409d8
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # GOV.UK Message queue consumer
2
+
3
+ Standardise the way GOV.UK consumes messages from RabbitMQ.
4
+
5
+
6
+ ## Nomenclature
7
+
8
+ - **Message queue**: a method of providing asynchronous interprocess
9
+ communication
10
+
11
+
12
+ ## Technical documentation
13
+
14
+ This is a ruby gem that deals with the boiler plate code of connecting,
15
+ subscribing, etc, to [RabbitMQ](https://www.rabbitmq.com/).
16
+
17
+ The user of this gem is left the task of supplying their rabbitmq infrastructure
18
+ configuration and an instance of a class that processes messages.
19
+
20
+ The message format received by the message processor is found in
21
+ `lib/govuk_message_queue_consumer/message.rb`
22
+
23
+ ### Dependencies
24
+
25
+ - **bunny**: to interact with RabbitMQ
26
+ - **activesupport**: use `with_indifferent_access` for Bunny
27
+
28
+
29
+ ### Running the application
30
+
31
+ We recommend creating a rake task like the following example:
32
+
33
+ ```ruby
34
+ namespace :message_queue do
35
+ desc "Run worker to consume messages from rabbitmq"
36
+ task consumer: :environment do
37
+ config = get_rabbitmq_configuration_hash
38
+ # ^ eg YAML.load_file(Rails.root.join('config', 'rabbitmq.yml'))[Rails.env]
39
+ GovukMessageQueueConsumer::Consumer.new(config, MyProcessor.new).run
40
+ end
41
+ end
42
+ ```
43
+
44
+ `govuk_message_queue_consumer` expects configuration and a processor to be supplied:
45
+
46
+ ```ruby
47
+ # example configuration. Could be stored in YAML if preferred
48
+ config = {
49
+ host: 'localhost',
50
+ port: 5672,
51
+ user: rabbitmq_user,
52
+ pass: rabbitmq_pass,
53
+ recover_from_connection_close: true,
54
+ exchange: my_exchange,
55
+ queue: my_queue,
56
+ }
57
+
58
+ # example message processor
59
+ class MyProcessor
60
+ def process(message)
61
+ message.ack
62
+ end
63
+ end
64
+ ```
65
+
66
+ #### Testing your processor
67
+
68
+ This gem provides a test helper for your processor.
69
+
70
+ ```ruby
71
+ require 'test_helper'
72
+ require 'govuk_message_queue_consumer/test_helpers'
73
+
74
+ describe MyProcessor do
75
+ it_behaves_like "a message queue processor"
76
+ end
77
+ ```
78
+
79
+ This will verify that your processor class implements the correct methods. You should add your own tests to verify its behaviour.
80
+
81
+ ### Running the test suite
82
+
83
+ ```bash
84
+ bundle exec rake spec
85
+ ```
86
+
87
+
88
+ ## Licence
89
+
90
+ [MIT License](LICENCE)
91
+
92
+
93
+ ## Versioning policy
94
+
95
+ [Semantic versioning](http://semver.org/spec/v2.0.0.html)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+
4
+ # Load local tasks
5
+ Dir['tasks/**/*.rake'].each { |file| load file }
6
+
7
+ task(:default).clear
8
+ task :default => [:spec]
@@ -0,0 +1,4 @@
1
+ require 'govuk_message_queue_consumer/version'
2
+ require 'govuk_message_queue_consumer/heartbeat_processor'
3
+ require 'govuk_message_queue_consumer/message'
4
+ require 'govuk_message_queue_consumer/consumer'
@@ -0,0 +1,44 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'bunny'
3
+
4
+ module GovukMessageQueueConsumer
5
+ class Consumer
6
+ def initialize(config, processor)
7
+ @processor = HeartbeatProcessor.new(processor)
8
+
9
+ @config = config.with_indifferent_access
10
+ @queue_name = @config.fetch(:queue)
11
+ @bindings = { @config.fetch(:exchange) => "#" }
12
+ @connection = Bunny.new(@config[:connection].symbolize_keys)
13
+ @connection.start
14
+ end
15
+
16
+ def run
17
+ queue.subscribe(:block => true, :manual_ack => true) do |delivery_info, headers, payload|
18
+ begin
19
+ @processor.process(Message.new(delivery_info, headers, payload))
20
+ rescue Exception => e
21
+ $stderr.puts "rabbitmq_consumer: aborting due to unhandled exception in processor #{e.class}: #{e.message}"
22
+ exit(1) # ensure rabbitmq requeues outstanding messages
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def queue
30
+ @queue ||= setup_queue
31
+ end
32
+
33
+ def setup_queue
34
+ @channel = @connection.create_channel
35
+ @channel.prefetch(1) # only one unacked message at a time
36
+ queue = @channel.queue(@queue_name, :durable => true)
37
+ @bindings.each do |exchange_name, routing_key|
38
+ exchange = @channel.topic(exchange_name, :passive => true)
39
+ queue.bind(exchange, :routing_key => routing_key)
40
+ end
41
+ queue
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ module GovukMessageQueueConsumer
2
+ class HeartbeatProcessor
3
+ def initialize(next_processor)
4
+ @next_processor = next_processor
5
+ end
6
+
7
+ def process(message)
8
+ # Ignore heartbeat messages
9
+ if message.headers.content_type == "application/x-heartbeat"
10
+ message.ack
11
+ else
12
+ @next_processor.process(message)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ module GovukMessageQueueConsumer
4
+ # Client code will receive an instance of this
5
+ class Message
6
+ def initialize(delivery_info, headers, payload)
7
+ @delivery_info = delivery_info
8
+ @headers = headers
9
+ @body = payload
10
+ end
11
+
12
+ attr_reader :delivery_info, :headers, :body
13
+
14
+ def body_data
15
+ @body_data ||= JSON.parse(@body)
16
+ end
17
+
18
+ def ack
19
+ @delivery_info.channel.ack(@delivery_info.delivery_tag)
20
+ end
21
+
22
+ def retry
23
+ @delivery_info.channel.reject(@delivery_info.delivery_tag, true)
24
+ end
25
+
26
+ def discard
27
+ @delivery_info.channel.reject(@delivery_info.delivery_tag, false)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1 @@
1
+ require 'govuk_message_queue_consumer/test_helpers/shared_examples'
@@ -0,0 +1,9 @@
1
+ RSpec.shared_examples "a message queue processor" do
2
+ it "implements #process" do
3
+ expect(subject).to respond_to(:process)
4
+ end
5
+
6
+ it "accepts 1 argument for #process" do
7
+ expect(subject.method(:process).arity).to eq(1)
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module GovukMessageQueueConsumer
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Consumer do
4
+
5
+ let(:queue) { instance_double('Bunny::Queue', bind: nil, subscribe: message_values) }
6
+ let(:channel) { instance_double('Bunny::Channel', queue: queue, prefetch: nil, topic: nil) }
7
+ let(:rabbitmq_connecton) { instance_double("Bunny::Session", start: nil, create_channel: channel) }
8
+ let(:client_processor) { instance_double('Client::Processor') }
9
+
10
+ before do
11
+ allow(Bunny).to receive(:new).and_return(rabbitmq_connecton)
12
+ end
13
+
14
+ describe "constructing an instance" do
15
+ let(:rabbitmq_connecton) { instance_double("Bunny::Session", start: nil, create_channel: channel) }
16
+ let(:client_processor) { instance_double('Client::Processor') }
17
+
18
+ it "passes the client processor to the Heartbeat Processor" do
19
+ expect(HeartbeatProcessor).to receive(:new).with(client_processor)
20
+
21
+ Consumer.new(rabbitmq_config, client_processor)
22
+ end
23
+
24
+ it "connects to rabbitmq" do
25
+ expected_options = rabbitmq_config['connection'].symbolize_keys # Bunny requires the keys to be symbols
26
+ expect(Bunny).to receive(:new).with(expected_options).and_return(rabbitmq_connecton)
27
+ expect(rabbitmq_connecton).to receive(:start)
28
+
29
+ Consumer.new(rabbitmq_config, client_processor)
30
+ end
31
+ end
32
+
33
+ describe "running the consumer" do
34
+ it "binds the queue" do
35
+ expect(queue).to receive(:bind)
36
+
37
+ Consumer.new(rabbitmq_config, client_processor).run
38
+ end
39
+
40
+ it "calls the heartbeat processor when subscribing to messages" do
41
+ expect(queue).to receive(:subscribe).and_yield(*message_values)
42
+ expect(Message).to receive(:new).with(*message_values)
43
+ expect_any_instance_of(HeartbeatProcessor).to receive(:process)
44
+
45
+ Consumer.new(rabbitmq_config, client_processor).run
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe HeartbeatProcessor do
4
+ let(:heartbeat_headers) { instance_double("Heartbeat Headers", :content_type => "application/x-heartbeat") }
5
+ let(:heartbeat_message) { instance_double("Heartbeat Message", :headers => heartbeat_headers, :ack => nil) }
6
+ let(:standard_headers) { instance_double("Standard Headers", :content_type => nil) }
7
+ let(:standard_message) { instance_double("Standard Message", :headers => standard_headers, :ack => nil) }
8
+
9
+ let(:next_processor) { instance_double("Client::Processor") }
10
+
11
+ subject {
12
+ HeartbeatProcessor.new(next_processor)
13
+ }
14
+
15
+ context "for a heartbeat message" do
16
+ it "doesn't call the next processor" do
17
+ expect(next_processor).not_to receive(:process)
18
+
19
+ subject.process(heartbeat_message)
20
+ end
21
+
22
+ it "acks the message" do
23
+ expect(heartbeat_message).to receive(:ack)
24
+
25
+ subject.process(heartbeat_message)
26
+ end
27
+ end
28
+
29
+ context "for a content message" do
30
+ it "calls the next processor" do
31
+ expect(next_processor).to receive(:process).with(standard_message)
32
+
33
+ subject.process(standard_message)
34
+ end
35
+
36
+ it "doesn't ack the message" do
37
+ expect(standard_message).not_to receive(:ack)
38
+ expect(next_processor).to receive(:process)
39
+
40
+ subject.process(standard_message)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Message do
4
+ let(:mock_channel) { instance_double("Channel") }
5
+ let(:delivery_info) { instance_double("DeliveryInfo", :channel => mock_channel, :delivery_tag => "a_tag") }
6
+ let(:headers) { instance_double("Headers") }
7
+ let(:body) { {"foo" => "bar"}.to_json }
8
+ let(:message) { Message.new(delivery_info, headers, body) }
9
+
10
+ it "json decodes the body" do
11
+ expect(message.body_data).to eq("foo" => "bar")
12
+ end
13
+
14
+ it "ack sends an ack to the channel" do
15
+ expect(mock_channel).to receive(:ack).with("a_tag")
16
+ message.ack
17
+ end
18
+
19
+ it "retry sends a reject to the channel with requeue set" do
20
+ expect(mock_channel).to receive(:reject).with("a_tag", true)
21
+ message.retry
22
+ end
23
+
24
+ it "reject sends a reject to the channel without requeue set" do
25
+ expect(mock_channel).to receive(:reject).with("a_tag", false)
26
+ message.discard
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../lib/govuk_message_queue_consumer'
2
+
3
+ include GovukMessageQueueConsumer
4
+
5
+ module TestHelpers
6
+ def rabbitmq_config
7
+ {
8
+ "connection" => {
9
+ "hosts" => ["rabbitmq1.example.com", "rabbitmq2.example.com"],
10
+ "port" => 5672,
11
+ "vhost" => "/",
12
+ "user" => "a_user",
13
+ "pass" => "super secret",
14
+ "recover_from_connection_close" => true,
15
+ },
16
+ "queue" => "content_register",
17
+ "exchange" => "published_documents",
18
+ }
19
+ end
20
+
21
+ def message_values
22
+ [:delivery_info1, :headers1, "message1_body"]
23
+ end
24
+ end
25
+
26
+ RSpec.configure do |c|
27
+ c.include TestHelpers
28
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: govuk_message_queue_consumer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - GOV.UK Dev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-21 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: 2.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: gem_publisher
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.5.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.5.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.3.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.3.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: 10.4.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 10.4.2
83
+ description: Avoid writing boilerplate code in order to consume messages from an AMQP
84
+ message queue. Plug in queue configuration, and how to process each message.
85
+ email:
86
+ - govuk-dev@digital.cabinet-office.gov.uk
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - README.md
92
+ - Rakefile
93
+ - lib/govuk_message_queue_consumer.rb
94
+ - lib/govuk_message_queue_consumer/consumer.rb
95
+ - lib/govuk_message_queue_consumer/heartbeat_processor.rb
96
+ - lib/govuk_message_queue_consumer/message.rb
97
+ - lib/govuk_message_queue_consumer/test_helpers.rb
98
+ - lib/govuk_message_queue_consumer/test_helpers/shared_examples.rb
99
+ - lib/govuk_message_queue_consumer/version.rb
100
+ - spec/consumer_spec.rb
101
+ - spec/heartbeat_processor_spec.rb
102
+ - spec/message_spec.rb
103
+ - spec/spec_helper.rb
104
+ homepage: https://github.com/alphagov/govuk_message_queue_consumer
105
+ licenses: []
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.4.5.1
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: AMQP message queue consumption with GOV.UK conventions
127
+ test_files:
128
+ - spec/heartbeat_processor_spec.rb
129
+ - spec/spec_helper.rb
130
+ - spec/message_spec.rb
131
+ - spec/consumer_spec.rb