sanger_warren 0.1.0 → 0.2.0.rc1
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 +4 -4
- data/.github/workflows/ruby.yml +39 -0
- data/.rubocop.yml +11 -5
- data/CHANGELOG.md +9 -1
- data/Gemfile +6 -1
- data/Gemfile.lock +68 -36
- data/README.md +105 -44
- data/bin/console +3 -6
- data/bin/warren +6 -0
- data/lefthook.yml +53 -0
- data/lib/sanger_warren.rb +8 -0
- data/lib/warren.rb +49 -4
- data/lib/warren/app.rb +9 -0
- data/lib/warren/app/cli.rb +34 -0
- data/lib/warren/app/config.rb +100 -0
- data/lib/warren/app/consumer.rb +53 -0
- data/lib/warren/app/consumer_add.rb +122 -0
- data/lib/warren/app/consumer_start.rb +25 -0
- data/lib/warren/app/exchange_config.rb +138 -0
- data/lib/warren/app/templates/subscriber.tt +32 -0
- data/lib/warren/callback.rb +2 -7
- data/lib/warren/client.rb +111 -0
- data/lib/warren/config/consumers.rb +101 -0
- data/lib/warren/den.rb +77 -0
- data/lib/warren/exceptions.rb +15 -0
- data/lib/warren/fox.rb +161 -0
- data/lib/warren/framework_adaptor/rails_adaptor.rb +83 -0
- data/lib/warren/handler/base.rb +20 -0
- data/lib/warren/handler/broadcast.rb +30 -16
- data/lib/warren/handler/log.rb +42 -10
- data/lib/warren/handler/test.rb +102 -14
- data/lib/warren/helpers/state_machine.rb +55 -0
- data/lib/warren/log_tagger.rb +58 -0
- data/lib/warren/message.rb +5 -5
- data/lib/warren/message/short.rb +41 -4
- data/lib/warren/railtie.rb +12 -0
- data/lib/warren/subscriber/base.rb +123 -0
- data/lib/warren/subscription.rb +71 -0
- data/lib/warren/version.rb +2 -1
- data/sanger-warren.gemspec +5 -4
- metadata +48 -8
- data/.travis.yml +0 -6
data/lib/warren/handler/test.rb
CHANGED
@@ -1,10 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'base'
|
4
|
+
|
3
5
|
module Warren
|
4
6
|
module Handler
|
5
7
|
# Class Warren::Test provides provides a dummy RabbitMQ
|
6
|
-
# connection pool for use during testing
|
7
|
-
|
8
|
+
# connection pool for use during testing.
|
9
|
+
#
|
10
|
+
# = Set up a test warren
|
11
|
+
#
|
12
|
+
# By default, the test warren is disabled during testing to avoid storing
|
13
|
+
# messages unnecessarily. Instead you must explicitly enable it when you
|
14
|
+
# wish to test message receipt.
|
15
|
+
#
|
16
|
+
# If using rspec it is suggested that you add the following to your
|
17
|
+
# spec_helper.rb
|
18
|
+
#
|
19
|
+
# config.around(:each, warren: true) do |ex|
|
20
|
+
# Warren.handler.enable!
|
21
|
+
# ex.run
|
22
|
+
# Warren.handler.disable!
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# = Making assertions
|
26
|
+
#
|
27
|
+
# It is possible to query the test warren about the messages it has seen.
|
28
|
+
# In particular the following methods are useful:
|
29
|
+
#
|
30
|
+
# {render:#messages}
|
31
|
+
#
|
32
|
+
# {render:#last_message}
|
33
|
+
#
|
34
|
+
# {render:#message_count}
|
35
|
+
#
|
36
|
+
# {render:#messages_matching}
|
37
|
+
#
|
38
|
+
# = Example
|
39
|
+
#
|
40
|
+
# describe QcResult, warren: true do
|
41
|
+
# let(:warren) { Warren.handler }
|
42
|
+
#
|
43
|
+
# setup { warren.clear_messages }
|
44
|
+
# let(:resource) { build :qc_result }
|
45
|
+
# let(:routing_key) { 'test.message.qc_result.' }
|
46
|
+
#
|
47
|
+
# it 'broadcasts the resource' do
|
48
|
+
# resource.save!
|
49
|
+
# expect(warren.messages_matching(routing_key)).to eq(1)
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
class Test < Warren::Handler::Base
|
53
|
+
# Warning displayed if the user attempts to make assertions against the
|
54
|
+
# handler without having enabled it.
|
8
55
|
DISABLED_WARNING = <<~DISABLED_WARREN
|
9
56
|
Test made against a disabled warren.
|
10
57
|
Warren::Handler::Test must be explicitly enabled to track messages,
|
@@ -23,7 +70,7 @@ module Warren
|
|
23
70
|
|
24
71
|
You can then tag tests with warren: true to enable warren testing.
|
25
72
|
DISABLED_WARREN
|
26
|
-
# Stand in for {
|
73
|
+
# Stand in for {Broadcast::Channel}, provides a store of messages to use
|
27
74
|
# in test assertions
|
28
75
|
class Channel
|
29
76
|
def initialize(warren)
|
@@ -33,7 +80,12 @@ module Warren
|
|
33
80
|
def <<(message)
|
34
81
|
@warren << message
|
35
82
|
end
|
83
|
+
|
84
|
+
def add_exchange(name, options)
|
85
|
+
@warren.add_exchange(name, options)
|
86
|
+
end
|
36
87
|
end
|
88
|
+
|
37
89
|
#
|
38
90
|
# Creates a test warren with no messages.
|
39
91
|
# Test warrens are shared across all threads.
|
@@ -41,45 +93,74 @@ module Warren
|
|
41
93
|
# @param [_] _args Configuration arguments are ignored.
|
42
94
|
#
|
43
95
|
def initialize(*_args)
|
96
|
+
super()
|
44
97
|
@messages = []
|
98
|
+
@exchanges = []
|
45
99
|
@enabled = false
|
46
100
|
end
|
47
101
|
|
48
102
|
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
def connect; end
|
53
|
-
|
54
|
-
def disconnect; end
|
55
|
-
|
56
|
-
#
|
57
|
-
# Yields an exchange which gets returned to the pool on block closure
|
58
|
-
#
|
103
|
+
# Yields a new chanel, which proxies all message back to {messages} on the
|
104
|
+
# {Warren::Handler::Test}
|
59
105
|
#
|
60
106
|
# @return [void]
|
61
107
|
#
|
62
108
|
# @yieldreturn [Warren::Test::Channel] A rabbitMQ channel that logs messaged to the test warren
|
63
109
|
def with_channel
|
64
|
-
yield
|
110
|
+
yield new_channel
|
65
111
|
end
|
66
112
|
|
113
|
+
#
|
114
|
+
# Returns a new chanel, which proxies all message back to {messages} on the
|
115
|
+
# {Warren::Handler::Test}
|
116
|
+
#
|
117
|
+
# @return [Warren::Test::Channel] A rabbitMQ channel that logs messaged to the test warren
|
118
|
+
#
|
119
|
+
def new_channel
|
120
|
+
Channel.new(@logger, routing_key_template: @routing_key_template)
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Clear any logged messaged
|
125
|
+
#
|
126
|
+
# @return [Array] The new empty array, lacking messages
|
127
|
+
#
|
67
128
|
def clear_messages
|
68
129
|
@messages = []
|
130
|
+
@exchanges = []
|
69
131
|
end
|
70
132
|
|
133
|
+
#
|
134
|
+
# Returns the last message received by the warren
|
135
|
+
#
|
136
|
+
# @return [#routing_key#payload] The last message object received by the warren
|
137
|
+
#
|
71
138
|
def last_message
|
72
139
|
messages.last
|
73
140
|
end
|
74
141
|
|
142
|
+
#
|
143
|
+
# Returns the total number message received by the warren since it was enabled
|
144
|
+
#
|
145
|
+
# @return [Integer] The total number of messages
|
146
|
+
#
|
75
147
|
def message_count
|
76
148
|
messages.length
|
77
149
|
end
|
78
150
|
|
151
|
+
#
|
152
|
+
# Returns the total number message received by the warren matching the given
|
153
|
+
# routing_key since it was enabled
|
154
|
+
#
|
155
|
+
# @param routing_key [String] The routing key to filter by
|
156
|
+
#
|
157
|
+
# @return [Integer] The number of matching messages
|
158
|
+
#
|
79
159
|
def messages_matching(routing_key)
|
80
160
|
messages.count { |message| message.routing_key == routing_key }
|
81
161
|
end
|
82
162
|
|
163
|
+
# Enable the warren
|
83
164
|
def enable!
|
84
165
|
@enabled = true
|
85
166
|
clear_messages
|
@@ -91,6 +172,9 @@ module Warren
|
|
91
172
|
clear_messages
|
92
173
|
end
|
93
174
|
|
175
|
+
# Returns an array of all message received by the warren since it was enabled
|
176
|
+
#
|
177
|
+
# @return [Array<#routing_key#payload>] All received messages
|
94
178
|
def messages
|
95
179
|
raise_if_not_tracking
|
96
180
|
@messages
|
@@ -101,6 +185,10 @@ module Warren
|
|
101
185
|
@messages << message if @enabled
|
102
186
|
end
|
103
187
|
|
188
|
+
def add_exchange(name, options)
|
189
|
+
@exchanges << [name, options] if @enabled
|
190
|
+
end
|
191
|
+
|
104
192
|
private
|
105
193
|
|
106
194
|
def raise_if_not_tracking
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Namespace for utility modules
|
5
|
+
module Helpers
|
6
|
+
# Provides an incredibly simple state machine. It merely lets you define
|
7
|
+
# states with {#state} which defines two methods `{state}!` to transition
|
8
|
+
# into the state and `{state}?` to query if we are in the state.
|
9
|
+
#
|
10
|
+
# == Usage:
|
11
|
+
#
|
12
|
+
# @example Basic usage
|
13
|
+
# class Machine
|
14
|
+
# extend Warren::Helpers::StateMachine
|
15
|
+
# states :started, :started
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# machine = Machine.new
|
19
|
+
# machine.started!
|
20
|
+
# machine.started? # => true
|
21
|
+
# machine.stopped? # => false
|
22
|
+
# machine.stopped!
|
23
|
+
# machine.started? # => false
|
24
|
+
# machine.stopped? # => stopped
|
25
|
+
#
|
26
|
+
module StateMachine
|
27
|
+
#
|
28
|
+
# Define a new state, generates two methods `{state}!` to transition
|
29
|
+
# into the state and `{state}?` to query if we are in the state.
|
30
|
+
#
|
31
|
+
# @param state_name [Symbol, String] The name of the state
|
32
|
+
#
|
33
|
+
# @return [Void]
|
34
|
+
#
|
35
|
+
def state(state_name)
|
36
|
+
define_method(:"#{state_name}!") { @state = state_name }
|
37
|
+
define_method(:"#{state_name}?") { @state == state_name }
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Define new states, generates two methods for each state `{state}!` to
|
42
|
+
# transition into the state and `{state}?` to query if we are in the state.
|
43
|
+
#
|
44
|
+
# @overload push2(state_name, ...)
|
45
|
+
# @param [Symbol, String] state_name The name of the state
|
46
|
+
# @param [Symbol, String] ... More states
|
47
|
+
#
|
48
|
+
# @return [Void]
|
49
|
+
#
|
50
|
+
def states(*state_names)
|
51
|
+
state_names.each { |state_name| state(state_name) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Applies a tag to any messages sent to the logger.
|
5
|
+
class LogTagger
|
6
|
+
#
|
7
|
+
# Create a new log tagger, which applies a tag to all messages before
|
8
|
+
# forwarding them on to the logger
|
9
|
+
#
|
10
|
+
# @param logger [Logger] A ruby Logger, or compatible interface
|
11
|
+
# @param tag [String] The tag to apply to each message
|
12
|
+
#
|
13
|
+
def initialize(logger:, tag:)
|
14
|
+
@logger = logger
|
15
|
+
@tag = tag
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Define `name` methods which forward on to the similarly named method
|
20
|
+
# on logger, with the tag applied
|
21
|
+
#
|
22
|
+
# @param name [Symbol] The method to define
|
23
|
+
#
|
24
|
+
# @return [Void]
|
25
|
+
def self.level(name)
|
26
|
+
define_method(name) do |arg = nil, &block|
|
27
|
+
if block
|
28
|
+
@logger.public_send(name, arg) { tag(block.call) }
|
29
|
+
else
|
30
|
+
@logger.public_send(name, tag(arg))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @!method debug(message)
|
36
|
+
# Forwards message on to logger with {#tag} prefix
|
37
|
+
# See Logger::debug
|
38
|
+
level :debug
|
39
|
+
# @!method info(message)
|
40
|
+
# Forwards message on to logger with {#tag} prefix
|
41
|
+
# See Logger::info
|
42
|
+
level :info
|
43
|
+
# @!method warn(message)
|
44
|
+
# Forwards message on to logger with {#tag} prefix
|
45
|
+
# See Logger::warn
|
46
|
+
level :warn
|
47
|
+
# @!method error(message)
|
48
|
+
# Forwards message on to logger with {#tag} prefix
|
49
|
+
# See Logger::error
|
50
|
+
level :error
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def tag(message)
|
55
|
+
"#{@tag}: #{message}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/warren/message.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Namespace to collect message formats
|
4
|
-
# A Warren compatible message must implement:
|
5
|
-
# routing_key: returns the routing_key for the message
|
6
|
-
# payloadL returns the message payload
|
7
|
-
|
8
3
|
require_relative 'message/short'
|
9
4
|
require_relative 'message/full'
|
10
5
|
|
6
|
+
# Namespace to collect message formats
|
7
|
+
# A Warren compatible message must implement:
|
8
|
+
# routing_key: returns the routing_key for the message
|
9
|
+
# payload: returns the message payload
|
10
|
+
#
|
11
11
|
# Additionally, if you wish to use the Message with the ActiveRecord
|
12
12
|
# helpers, then the initialize should take the ActiveRecord::Base object
|
13
13
|
# as a single argument
|
data/lib/warren/message/short.rb
CHANGED
@@ -4,18 +4,55 @@ module Warren
|
|
4
4
|
module Message
|
5
5
|
# Light-weight interim message which can be expanded to a full payload later.
|
6
6
|
class Short
|
7
|
+
begin
|
8
|
+
include AfterCommitEverywhere
|
9
|
+
rescue NameError
|
10
|
+
# After commit everywhere is not included in the gemfile.
|
11
|
+
end
|
12
|
+
|
7
13
|
attr_reader :record
|
8
14
|
|
9
|
-
|
10
|
-
|
15
|
+
#
|
16
|
+
# Create a 'short' message, where the payload is just the class name and id.
|
17
|
+
# Designed for when you wish to use a delayed broadcast.
|
18
|
+
#
|
19
|
+
# @param record [ActiveRecord::Base] An Active Record object
|
20
|
+
#
|
21
|
+
def initialize(record = nil, class_name: nil, id: nil)
|
22
|
+
if record
|
23
|
+
@class_name = record.class.name
|
24
|
+
@id = record.id
|
25
|
+
else
|
26
|
+
@class_name = class_name
|
27
|
+
@id = id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Queues the message for broadcast at the end of the transaction. Actually want to make this the default
|
32
|
+
# behaviour, but only realised the need when doing some last minute integration tests. Will revisit this
|
33
|
+
# in the next version. (Or possibly post code review depending)
|
34
|
+
def queue(warren)
|
35
|
+
after_commit { warren << self }
|
36
|
+
rescue NoMethodError
|
37
|
+
raise StandardError, '#queue depends on the after_commit_everywhere gem. Please add this to your gemfile'
|
11
38
|
end
|
12
39
|
|
40
|
+
# The routing key for the message.
|
41
|
+
#
|
42
|
+
# @return [String] The routing key
|
43
|
+
#
|
13
44
|
def routing_key
|
14
|
-
"queue_broadcast.#{
|
45
|
+
"queue_broadcast.#{@class_name.underscore}.#{@id}"
|
15
46
|
end
|
16
47
|
|
48
|
+
#
|
49
|
+
# The contents of the message, a string in the form:
|
50
|
+
# ["<ClassName>",<id>]
|
51
|
+
#
|
52
|
+
# @return [String] The payload of the message
|
53
|
+
#
|
17
54
|
def payload
|
18
|
-
[
|
55
|
+
[@class_name, @id].to_json
|
19
56
|
end
|
20
57
|
end
|
21
58
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Railtie for automatic configuration in Rails apps. Reduces the need to
|
5
|
+
# modify the application when adding Warren.
|
6
|
+
# @see https://api.rubyonrails.org/classes/Rails/Railtie.html
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
config.to_prepare do
|
9
|
+
Warren.load_configuration
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'warren/exceptions'
|
5
|
+
|
6
|
+
module Warren
|
7
|
+
# Namespace for warren subscriber objects
|
8
|
+
module Subscriber
|
9
|
+
# A message takes a rabbitMQ message, and handles its acknowledgement
|
10
|
+
# or rejection.
|
11
|
+
class Base
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
# @return [Warren::Fox] The fox consumer that provided the message. Used to acknowledge messages
|
15
|
+
attr_reader :fox
|
16
|
+
# @return [Bunny::DeliveryInfo] Contains the information necessary for acknowledging the message
|
17
|
+
attr_reader :delivery_info
|
18
|
+
# @return [Bunny::MessageProperties] Contains additional information about the received message
|
19
|
+
attr_reader :properties
|
20
|
+
# @return [String] The message contents
|
21
|
+
attr_reader :payload
|
22
|
+
|
23
|
+
# We don't add an active-support dependency, so instead use the plain-ruby
|
24
|
+
# delegators (Supplied by Forwardable)
|
25
|
+
# Essentially syntax is:
|
26
|
+
# def_delegators <target>, *<methods_to_delegate>
|
27
|
+
def_delegators :fox, :subscription, :warn, :info, :error, :debug
|
28
|
+
|
29
|
+
#
|
30
|
+
# Construct a basic subscriber for each received message. Call {#process}
|
31
|
+
# to handle to processing of the message
|
32
|
+
#
|
33
|
+
# @param fox [Warren::Fox] The fox consumer that provided the message. Used to acknowledge messages
|
34
|
+
# @param delivery_info [Bunny::DeliveryInfo] Contains the information necessary for acknowledging the message
|
35
|
+
# @param properties [Bunny::MessageProperties] Contains additional information about the received message
|
36
|
+
# @param payload [String] The message contents
|
37
|
+
#
|
38
|
+
def initialize(fox, delivery_info, properties, payload)
|
39
|
+
@fox = fox
|
40
|
+
@delivery_info = delivery_info
|
41
|
+
@properties = properties
|
42
|
+
@payload = payload
|
43
|
+
@acknowledged = false
|
44
|
+
end
|
45
|
+
|
46
|
+
# Called by {Warren::Fox} to trigger processing of the message and acknowledgment
|
47
|
+
# on success. In most cases the {#process} method should be used to customize behaviour.
|
48
|
+
#
|
49
|
+
# @return [Void]
|
50
|
+
def _process_
|
51
|
+
process
|
52
|
+
ack unless @acknowledged
|
53
|
+
end
|
54
|
+
|
55
|
+
# Triggers processing of the method. Over-ride this in subclasses to customize your
|
56
|
+
# handler.
|
57
|
+
def process
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Reject the message and re-queue ready for
|
62
|
+
# immediate reprocessing.
|
63
|
+
#
|
64
|
+
# @param exception [StandardError] The exception which triggered message requeue
|
65
|
+
#
|
66
|
+
# @return [Void]
|
67
|
+
#
|
68
|
+
def requeue(exception)
|
69
|
+
warn "Re-queue: #{payload}"
|
70
|
+
warn "Re-queue Exception: #{exception.message}"
|
71
|
+
raise_if_acknowledged
|
72
|
+
subscription.nack(delivery_tag, false, true)
|
73
|
+
@acknowledged = true
|
74
|
+
warn 'Re-queue nacked'
|
75
|
+
end
|
76
|
+
|
77
|
+
# Reject the message without re-queuing
|
78
|
+
# Will end up getting dead-lettered
|
79
|
+
#
|
80
|
+
# @param exception [StandardError] The exception which triggered message dead-letter
|
81
|
+
#
|
82
|
+
# @return [Void]
|
83
|
+
#
|
84
|
+
def dead_letter(exception)
|
85
|
+
error "Dead-letter: #{payload}"
|
86
|
+
error "Dead-letter Exception: #{exception.message}"
|
87
|
+
raise_if_acknowledged
|
88
|
+
subscription.nack(delivery_tag)
|
89
|
+
@acknowledged = true
|
90
|
+
error 'Dead-letter nacked'
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def headers
|
96
|
+
# Annoyingly it appears that a message with no headers
|
97
|
+
# returns nil, not an empty hash
|
98
|
+
properties.headers || {}
|
99
|
+
end
|
100
|
+
|
101
|
+
def delivery_tag
|
102
|
+
delivery_info.delivery_tag
|
103
|
+
end
|
104
|
+
|
105
|
+
# Acknowledge the message as successfully processed.
|
106
|
+
# Will raise {Warren::MultipleAcknowledgements} if the message has been
|
107
|
+
# acknowledged or rejected already.
|
108
|
+
def ack
|
109
|
+
raise_if_acknowledged
|
110
|
+
subscription.ack(delivery_tag)
|
111
|
+
@acknowledged = true
|
112
|
+
end
|
113
|
+
|
114
|
+
def raise_if_acknowledged
|
115
|
+
return unless @acknowledged
|
116
|
+
|
117
|
+
message = "Multiple acks/nacks for: #{payload}"
|
118
|
+
error message
|
119
|
+
raise Warren::Exceptions::MultipleAcknowledgements, message
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|