sanger_warren 0.1.0 → 0.3.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 +4 -4
- data/.github/workflows/ruby.yml +39 -0
- data/.rubocop.yml +11 -5
- data/.yardopts +3 -0
- data/CHANGELOG.md +34 -1
- data/Gemfile +6 -1
- data/Gemfile.lock +71 -39
- data/README.md +133 -43
- 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 +35 -0
- data/lib/warren/app/config.rb +110 -0
- data/lib/warren/app/consumer.rb +65 -0
- data/lib/warren/app/consumer_add.rb +131 -0
- data/lib/warren/app/consumer_start.rb +40 -0
- data/lib/warren/app/exchange_config.rb +151 -0
- data/lib/warren/app/templates/subscriber.tt +32 -0
- data/lib/warren/callback.rb +2 -7
- data/lib/warren/callback/broadcast_with_warren.rb +1 -1
- data/lib/warren/client.rb +111 -0
- data/lib/warren/config/consumers.rb +123 -0
- data/lib/warren/delay_exchange.rb +85 -0
- data/lib/warren/den.rb +93 -0
- data/lib/warren/exceptions.rb +15 -0
- data/lib/warren/fox.rb +165 -0
- data/lib/warren/framework_adaptor/rails_adaptor.rb +135 -0
- data/lib/warren/handler.rb +16 -0
- data/lib/warren/handler/base.rb +20 -0
- data/lib/warren/handler/broadcast.rb +54 -18
- data/lib/warren/handler/log.rb +50 -10
- data/lib/warren/handler/test.rb +101 -14
- data/lib/warren/helpers/state_machine.rb +55 -0
- data/lib/warren/log_tagger.rb +58 -0
- data/lib/warren/message.rb +7 -5
- data/lib/warren/message/full.rb +20 -0
- data/lib/warren/message/short.rb +49 -4
- data/lib/warren/message/simple.rb +15 -0
- data/lib/warren/railtie.rb +12 -0
- data/lib/warren/subscriber/base.rb +151 -0
- data/lib/warren/subscription.rb +78 -0
- data/lib/warren/version.rb +2 -1
- data/sanger-warren.gemspec +5 -4
- metadata +49 -6
- data/.travis.yml +0 -6
@@ -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,15 @@
|
|
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'
|
5
|
+
require_relative 'message/simple'
|
10
6
|
|
7
|
+
# Namespace to collect message formats
|
8
|
+
# A Warren compatible message must implement:
|
9
|
+
# routing_key: returns the routing_key for the message
|
10
|
+
# payload: returns the message payload
|
11
|
+
# headers: Returns a headers hash
|
12
|
+
#
|
11
13
|
# Additionally, if you wish to use the Message with the ActiveRecord
|
12
14
|
# helpers, then the initialize should take the ActiveRecord::Base object
|
13
15
|
# as a single argument
|
data/lib/warren/message/full.rb
CHANGED
@@ -10,6 +10,13 @@ module Warren
|
|
10
10
|
@record = record
|
11
11
|
end
|
12
12
|
|
13
|
+
#
|
14
|
+
# The routing key that will be used for the message, not including the
|
15
|
+
# routing_key_prefix configured in warren.yml. If {#record} responds
|
16
|
+
# to `routing_key` will use that instead
|
17
|
+
#
|
18
|
+
# @return [String] The routing key.
|
19
|
+
#
|
13
20
|
def routing_key
|
14
21
|
if record.respond_to?(:routing_key)
|
15
22
|
record.routing_key
|
@@ -18,9 +25,22 @@ module Warren
|
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
28
|
+
#
|
29
|
+
# The payload of the message.
|
30
|
+
# @see https://github.com/intridea/multi_json
|
31
|
+
#
|
32
|
+
# @return [String] The message payload
|
21
33
|
def payload
|
22
34
|
MultiJson.dump(record)
|
23
35
|
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# For compatibility. Returns an empty hash.
|
39
|
+
#
|
40
|
+
# @return [{}] Empty hash
|
41
|
+
def headers
|
42
|
+
{}
|
43
|
+
end
|
24
44
|
end
|
25
45
|
end
|
26
46
|
end
|
data/lib/warren/message/short.rb
CHANGED
@@ -4,18 +4,63 @@ 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
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# For compatibility. Returns an empty hash.
|
60
|
+
#
|
61
|
+
# @return [{}] Empty hash
|
62
|
+
def headers
|
63
|
+
{}
|
19
64
|
end
|
20
65
|
end
|
21
66
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Namespace for Warren message wrappers.
|
5
|
+
module Message
|
6
|
+
# A simple message simply wraps the routing key and payload together
|
7
|
+
# @!attribute [rw] routing_key
|
8
|
+
# @return [String] The routing key of the message
|
9
|
+
# @!attribute [rw] payload
|
10
|
+
# @return [String] The payload of the message
|
11
|
+
# @!attribute [rw] headers
|
12
|
+
# @return [Hash] Hash of header attributes. Can be empty hash.
|
13
|
+
Simple = Struct.new(:routing_key, :payload, :headers)
|
14
|
+
end
|
15
|
+
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,151 @@
|
|
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, :delayed
|
28
|
+
def_delegators :delivery_info, :routing_key, :delivery_tag
|
29
|
+
|
30
|
+
#
|
31
|
+
# Construct a basic subscriber for each received message. Call {#process}
|
32
|
+
# to handle to processing of the message
|
33
|
+
#
|
34
|
+
# @param fox [Warren::Fox] The fox consumer that provided the message. Used to acknowledge messages
|
35
|
+
# @param delivery_info [Bunny::DeliveryInfo] Contains the information necessary for acknowledging the message
|
36
|
+
# @param properties [Bunny::MessageProperties] Contains additional information about the received message
|
37
|
+
# @param payload [String] The message contents
|
38
|
+
#
|
39
|
+
def initialize(fox, delivery_info, properties, payload)
|
40
|
+
@fox = fox
|
41
|
+
@delivery_info = delivery_info
|
42
|
+
@properties = properties
|
43
|
+
@payload = payload
|
44
|
+
@acknowledged = false
|
45
|
+
end
|
46
|
+
|
47
|
+
# Called by {Warren::Fox} to trigger processing of the message and acknowledgment
|
48
|
+
# on success. In most cases the {#process} method should be used to customize behaviour.
|
49
|
+
#
|
50
|
+
# @return [Void]
|
51
|
+
def _process_
|
52
|
+
process
|
53
|
+
ack unless @acknowledged
|
54
|
+
end
|
55
|
+
|
56
|
+
# Triggers processing of the method. Over-ride this in subclasses to customize your
|
57
|
+
# handler.
|
58
|
+
def process
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Reject the message and re-queue ready for
|
63
|
+
# immediate reprocessing.
|
64
|
+
#
|
65
|
+
# @param exception [StandardError] The exception which triggered message requeue
|
66
|
+
#
|
67
|
+
# @return [Void]
|
68
|
+
#
|
69
|
+
def requeue(exception)
|
70
|
+
warn "Re-queue: #{payload}"
|
71
|
+
warn "Re-queue Exception: #{exception.message}"
|
72
|
+
raise_if_acknowledged
|
73
|
+
# nack arguments: delivery_tag, multiple, requeue
|
74
|
+
# http://reference.rubybunny.info/Bunny/Channel.html#nack-instance_method
|
75
|
+
subscription.nack(delivery_tag, false, true)
|
76
|
+
@acknowledged = true
|
77
|
+
warn 'Re-queue nacked'
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reject the message without re-queuing
|
81
|
+
# Will end up getting dead-lettered
|
82
|
+
#
|
83
|
+
# @param exception [StandardError] The exception which triggered message dead-letter
|
84
|
+
#
|
85
|
+
# @return [Void]
|
86
|
+
#
|
87
|
+
def dead_letter(exception)
|
88
|
+
error "Dead-letter: #{payload}"
|
89
|
+
error "Dead-letter Exception: #{exception.message}"
|
90
|
+
raise_if_acknowledged
|
91
|
+
subscription.nack(delivery_tag)
|
92
|
+
@acknowledged = true
|
93
|
+
error 'Dead-letter nacked'
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Re-post the message to the delay exchange and acknowledges receipt of
|
98
|
+
# the original message. The delay exchange will return the messages to
|
99
|
+
# the original queue after a delay.
|
100
|
+
#
|
101
|
+
# @param exception [StandardError] The exception that has caused the
|
102
|
+
# message to require a delay
|
103
|
+
#
|
104
|
+
# @return [Void]
|
105
|
+
#
|
106
|
+
def delay(exception)
|
107
|
+
return dead_letter(exception) if attempt > max_retries
|
108
|
+
|
109
|
+
warn "Delay: #{payload}"
|
110
|
+
warn "Delay Exception: #{exception.message}"
|
111
|
+
# Publish the message to the delay queue
|
112
|
+
delayed.publish(payload, routing_key: routing_key, headers: { attempts: attempt + 1 })
|
113
|
+
# Acknowledge the original message
|
114
|
+
ack
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def max_retries
|
120
|
+
30
|
121
|
+
end
|
122
|
+
|
123
|
+
def attempt
|
124
|
+
headers.fetch('attempts', 0)
|
125
|
+
end
|
126
|
+
|
127
|
+
def headers
|
128
|
+
# Annoyingly it appears that a message with no headers
|
129
|
+
# returns nil, not an empty hash
|
130
|
+
properties.headers || {}
|
131
|
+
end
|
132
|
+
|
133
|
+
# Acknowledge the message as successfully processed.
|
134
|
+
# Will raise {Warren::MultipleAcknowledgements} if the message has been
|
135
|
+
# acknowledged or rejected already.
|
136
|
+
def ack
|
137
|
+
raise_if_acknowledged
|
138
|
+
subscription.ack(delivery_tag)
|
139
|
+
@acknowledged = true
|
140
|
+
end
|
141
|
+
|
142
|
+
def raise_if_acknowledged
|
143
|
+
return unless @acknowledged
|
144
|
+
|
145
|
+
message = "Multiple acks/nacks for: #{payload}"
|
146
|
+
error message
|
147
|
+
raise Warren::Exceptions::MultipleAcknowledgements, message
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
# Configures and wraps up subscriptions on a Bunny Channel/Queue
|
5
|
+
class Subscription
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :channel
|
9
|
+
|
10
|
+
#
|
11
|
+
# Great a new subscription. Handles queue creation, binding and attaching
|
12
|
+
# consumers to the queues
|
13
|
+
#
|
14
|
+
# @param channel [Warren::Handler::Broadcast::Channel] A channel on which to register queues
|
15
|
+
# @param config [Hash] queue configuration hash
|
16
|
+
#
|
17
|
+
def initialize(channel:, config:)
|
18
|
+
@channel = channel
|
19
|
+
@queue_name = config&.fetch('name')
|
20
|
+
@queue_options = config&.fetch('options')
|
21
|
+
@bindings = config&.fetch('bindings')
|
22
|
+
end
|
23
|
+
|
24
|
+
def_delegators :channel, :nack, :ack
|
25
|
+
|
26
|
+
#
|
27
|
+
# Subscribes to the given queue
|
28
|
+
#
|
29
|
+
# @param consumer_tag [String] Identifier for the consumer
|
30
|
+
#
|
31
|
+
# @yieldparam [Bunny::DeliveryInfo] delivery_info Metadata about the delivery
|
32
|
+
# @yieldparam [Bunny::MessageProperties] properties
|
33
|
+
# @yieldparam [String] payload the contents of the message
|
34
|
+
#
|
35
|
+
# @return [Bunny::Consumer] The bunny consumer object
|
36
|
+
#
|
37
|
+
def subscribe(consumer_tag, &block)
|
38
|
+
channel.prefetch(10)
|
39
|
+
queue.subscribe(manual_ack: true, block: false, consumer_tag: consumer_tag, durable: true, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Ensures the queues and channels are set up to receive messages
|
43
|
+
# keys: additional routing_keys to bind
|
44
|
+
def activate!
|
45
|
+
establish_bindings!
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def add_binding(exchange, options)
|
51
|
+
queue.bind(exchange, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def exchange(config)
|
55
|
+
channel.exchange(*config.values_at('name', 'options'))
|
56
|
+
end
|
57
|
+
|
58
|
+
def queue
|
59
|
+
raise StandardError, 'No queue configured' if @queue_name.nil?
|
60
|
+
|
61
|
+
@queue ||= channel.queue(@queue_name, @queue_options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def establish_bindings!
|
65
|
+
@bindings.each do |binding_config|
|
66
|
+
exchange = exchange(binding_config['exchange'])
|
67
|
+
transformed_options = merge_routing_key_prefix(binding_config['options'])
|
68
|
+
add_binding(exchange, transformed_options)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def merge_routing_key_prefix(options)
|
73
|
+
options.transform_values do |value|
|
74
|
+
format(value, routing_key_prefix: channel.routing_key_prefix)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|