govuk_message_queue_consumer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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