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 +7 -0
- data/README.md +95 -0
- data/Rakefile +8 -0
- data/lib/govuk_message_queue_consumer.rb +4 -0
- data/lib/govuk_message_queue_consumer/consumer.rb +44 -0
- data/lib/govuk_message_queue_consumer/heartbeat_processor.rb +16 -0
- data/lib/govuk_message_queue_consumer/message.rb +30 -0
- data/lib/govuk_message_queue_consumer/test_helpers.rb +1 -0
- data/lib/govuk_message_queue_consumer/test_helpers/shared_examples.rb +9 -0
- data/lib/govuk_message_queue_consumer/version.rb +3 -0
- data/spec/consumer_spec.rb +48 -0
- data/spec/heartbeat_processor_spec.rb +43 -0
- data/spec/message_spec.rb +28 -0
- data/spec/spec_helper.rb +28 -0
- metadata +131 -0
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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|